Environment variable and configuration audit. 8 dimensions (ENV1-ENV8): variable completeness, unused vars, startup validation, secret exposure, environment parity, type safety, default values, and documentation. Supports Node.js (.env/process.env), Python (os.environ/settings), and framework-specific patterns (Vite import.meta.env, Next.js NEXT_PUBLIC_). Switches: zuvo:env-audit full | [path] | --secrets-only | --parity
Scanned 5/28/2026
Install via CLI
openskills install greglas75/zuvo---
name: env-audit
description: >
Environment variable and configuration audit. 8 dimensions (ENV1-ENV8):
variable completeness, unused vars, startup validation, secret exposure,
environment parity, type safety, default values, and documentation. Supports
Node.js (.env/process.env), Python (os.environ/settings), and framework-specific
patterns (Vite import.meta.env, Next.js NEXT_PUBLIC_).
Switches: zuvo:env-audit full | [path] | --secrets-only | --parity
codesift_tools:
always:
- analyze_project
- index_status
- index_folder
- index_file
- plan_turn
- scan_secrets # KEY — ENV4 secret exposure (committed .env, hardcoded values)
- get_file_tree # discover .env*, config/*, settings.py
- search_text # process.env.X, os.environ.get, import.meta.env.X
- search_patterns # ENV2 unused, ENV3 missing-validation, ENV6 type-coerce
- get_file_outline
- find_references # ENV2 unused — where is each var actually read
- search_symbols
- get_symbol
- audit_scan
by_stack:
typescript: [get_type_info, resolve_constant_value]
javascript: []
python: [python_audit, analyze_async_correctness, resolve_constant_value]
php: [php_project_audit, php_security_scan, resolve_php_namespace]
kotlin: [analyze_sealed_hierarchy, find_extension_functions, trace_flow_chain, trace_suspend_chain, trace_compose_tree, analyze_compose_recomposition, trace_hilt_graph, trace_room_schema, analyze_kmp_declarations, extract_kotlin_serialization_contract]
nestjs: [nest_audit]
nextjs: [framework_audit, nextjs_route_map]
astro: [astro_audit, astro_actions_audit, astro_hydration_audit, astro_sessions]
hono: [analyze_hono_app, audit_hono_security]
express: []
fastify: []
react: [react_quickstart, analyze_hooks, analyze_renders]
django: [analyze_django_settings, effective_django_view_security, taint_trace]
fastapi: [trace_fastapi_depends, get_pydantic_models]
flask: [find_framework_wiring]
jest: []
yii: [resolve_php_service]
prisma: [analyze_prisma_schema]
drizzle: []
sql: [sql_audit]
postgres: [migration_lint]
---
# zuvo:env-audit
Audit environment variable management for completeness, safety, and consistency
across development, staging, and production environments. Single-pass execution.
**When to use:** After adding new env vars, before production deploy, when
debugging config-related incidents, onboarding new environments, periodic check.
**When NOT to use:** Secret scanning in source code (`/security-audit`), CI
pipeline config (`zuvo:ci-audit`), infrastructure (`/security-audit`).
## Mandatory File Loading
Read every file below before starting. Print the checklist.
```
CORE FILES LOADED:
1. ../../shared/includes/codesift-setup.md -- [READ | MISSING -> STOP]
2. ../../shared/includes/env-compat.md -- [READ | MISSING -> STOP]
3. ../../shared/includes/run-logger.md -- [READ | MISSING -> STOP]
4. ../../shared/includes/retrospective.md -- [READ | MISSING -> STOP]
```
If any file is MISSING, STOP. Do not proceed from memory.
---
## Argument Parsing
| Token | Behavior |
|-------|----------|
| _(empty)_ or `full` | All 8 dimensions, scope = project root |
| `[path]` | Scope to a directory (monorepo package, module) |
| `--secrets-only` | ENV4 only -- focus on secret exposure |
| `--parity` | ENV5 only -- compare environment configurations |
---
## Safety Gates
### GATE 1 -- Read-Only
This audit is **read-only**. The only write target is `audits/`.
FORBIDDEN:
- Modifying any `.env`, config, or source file
- Creating or deleting environment variables
- Accessing external secret managers (analyze config references only)
### GATE 2 -- PII and Secret Censorship
When including env var values in the report:
- Replace actual values with `***`
- Show only variable names, never values
- Exception: `.env.example` placeholder values can be shown as-is
---
## MANDATORY TOOL CALLS — Env Audit Validity Gate
**INVALID if any tool below is skipped.** "DEFERRED", "N/A", "--secrets-only" NOT valid reasons.
| Tool | Trigger | Skip allowed? |
|------|---------|---------------|
| `scan_secrets` | Always | **NO** — KEY: ENV4 secret exposure |
| `search_text` | Always | **NO** — process.env / os.environ / import.meta.env scan |
| `search_patterns` | Always | **NO** — ENV2/ENV3/ENV6 anti-patterns |
| `find_references` | Always | **NO** — ENV2 unused detection |
| `audit_scan` | Always | **NO** — compound check |
Forbidden: `scan_secrets: skipped`, `codesift: unavailable` (when deferred), `retrospective: skipped` — all REJECTED.
POSTAMBLE: report on disk → retro appended → `~/.zuvo/append-runlog` exit 0. Every ENV finding needs `path/to/file.ext:LINE` (verify-audit gate).
```
Mandatory-tools-acknowledgment: I will run scan_secrets + search_text + search_patterns + find_references + audit_scan for this env audit. Every ENV finding will cite a `path/to/file.ext:LINE` resolving in the current tree.
```
**Use the deterministic preload helper FIRST.** Run `~/.zuvo/compute-preload env-audit "$PWD"`. Copy `[CodeSift matching trace]` verbatim, issue printed `ToolSearch(query="select:...")`. Math gate enforced.
---
## Phase 0: Detect and Scope
### 0.1 Target Resolution
Set `TARGET_ROOT` from arguments. All subsequent commands use this path.
### 0.2 Stack Detection
Detect which env access patterns to search for:
| Signal | Stack | Access Pattern |
|--------|-------|----------------|
| `package.json` | Node.js | `process.env.VAR`, `process.env['VAR']` |
| `vite.config.*` | Vite | `import.meta.env.VITE_VAR` |
| `next.config.*` | Next.js | `process.env.NEXT_PUBLIC_VAR` (client-safe) |
| `requirements.txt`, `pyproject.toml` | Python | `os.environ['VAR']`, `os.getenv('VAR')`, `os.environ.get('VAR')` |
| `wrangler.toml` | Cloudflare Workers | `env.VAR` (binding) |
| `docker-compose.*` | Docker Compose | `environment:` / `env_file:` directives |
### 0.3 Variable Name Extraction
Build a comprehensive inventory by scanning code for all env access patterns
detected in 0.2. Collect unique variable names (not values).
### 0.4 Inventory
Cross-reference all sources:
1. Variables defined in `.env*` files (names only)
2. Variables accessed in code (from extraction)
3. Variables in Docker Compose, CI config, or cloud deployment files
Print:
```
ENV VAR INVENTORY
------------------------------------
Target: [TARGET_ROOT]
Config sources: [.env, .env.example, .env.local, ...]
Stack: [Node.js / Python / Vite / Next.js]
Vars defined: [N]
Vars used in code: [N]
------------------------------------
```
---
## Phase 1: Dimension Analysis (ENV1-ENV8)
Single-pass inline execution. No sub-agents required.
### ENV1: Variable Completeness -- Weight 15, Max 15, Critical Gate
Are all required env vars defined and documented?
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Code vs example coverage | All used vars listed in `.env.example` | Var used in code but missing from `.env.example` | CRITICAL |
| Required vars guarded | Startup fails fast on missing required var | App starts, crashes later when var accessed | CRITICAL |
| .env.example exists | Yes, with all vars and placeholder values | No `.env.example` at all | HIGH |
| Stale vars | No vars in `.env.example` that are unused in code | Accumulated dead config | MEDIUM |
**Detection approach:** Compare the set of variables found in code against the
set defined in `.env.example`. Report both directions: used-but-undocumented
and documented-but-unused.
Critical gate: ENV1=0 (no `.env.example` OR multiple critical vars
undocumented) triggers FAIL.
### ENV2: Unused Variables -- Weight 5, Max 5
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Defined but never used | All defined vars referenced in code | Stale vars accumulate | MEDIUM |
| Dead config sections | Removed features cleaned up | Config references removed features | LOW |
**Detection approach:** Check each var in `.env.example` against code access
patterns. A var referenced only in comments or documentation counts as unused.
### ENV3: Startup Validation -- Weight 15, Max 15, Critical Gate
Does the app fail fast on missing or invalid configuration?
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Schema validation | Zod/Joi/Pydantic/envalid validates all vars at startup | No validation, raw access everywhere | CRITICAL |
| Fail-fast behavior | App throws before serving requests on missing var | Starts, breaks when var first accessed | CRITICAL |
| Type coercion | Parsed and validated (parseInt, Number, schema) | Port used as string in numeric context | HIGH |
**What to search for:**
- Validation libraries: `z.object`, `Joi.object`, `BaseSettings`, `envalid`,
`envsafe`, `convict`, `cleanEnv`, `ConfigModule`, `@IsString`
- Fail-fast patterns: `throw` with env/config message, `process.exit` on
missing env
- Raw usage: `process.env.VAR` without validation wrapper
- NestJS: `ConfigModule.forRoot()` with `validationSchema`
Critical gate: ENV3=0 (no validation anywhere) triggers FAIL.
### ENV4: Secret Exposure -- Weight 15, Max 15, Critical Gate
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| .env in .gitignore | `.env` and `.env.local` listed in .gitignore | .env committed to repository | CRITICAL |
| Client-side exposure | Only `NEXT_PUBLIC_`/`VITE_` vars are non-sensitive | API keys in `NEXT_PUBLIC_*` variables | CRITICAL |
| Hardcoded secrets | Secrets only in env vars or secret manager | API keys hardcoded in config files | CRITICAL |
| Docker image leakage | `.env` listed in .dockerignore | .env copied into Docker image | HIGH |
| Git history | No .env files in git history | Past .env commits (secrets may still be in history) | HIGH |
**Detection approach:**
- Check `.gitignore` for `.env` patterns
- `git ls-files` for committed `.env` files
- Search for `NEXT_PUBLIC_*KEY`, `NEXT_PUBLIC_*SECRET`, `VITE_*KEY` etc.
- Search for hardcoded secrets: `(api_key|secret|password|token)\s*[:=]\s*['"][^'"{}$]`
excluding test/mock/example files
- Check `.dockerignore` for `.env`
Critical gate: ENV4=0 (secrets committed or API keys in client-side vars)
triggers FAIL.
### ENV5: Environment Parity -- Weight 10, Max 10
Are dev/staging/production environments consistent?
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Same var set across envs | All environments define the same variables | Prod has vars dev lacks | HIGH |
| Docker Compose alignment | `docker-compose` env matches app config | Docker sets vars not in `.env.example` | MEDIUM |
| Feature flag documentation | Feature flags documented per environment | Undocumented per-env behavior | MEDIUM |
**Detection approach:** Compare variable sets across all `.env*` files found
at the target root. Check Docker Compose `environment:` sections against the
`.env.example` inventory.
### ENV6: Type Safety -- Weight 8, Max 8
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Central config module | Typed config with validated accessors | Raw `process.env.X` scattered across 50+ files | HIGH |
| Boolean parsing | `=== 'true'` or schema-parsed | `if (process.env.FLAG)` (any string is truthy) | HIGH |
| Number parsing | `parseInt` with NaN check or schema | Port used as string | MEDIUM |
| Access centralization | < 3 files access `process.env` directly | 20+ files with raw env access | MEDIUM |
**Detection approach:** Count per-file occurrences of raw env access. Search
for boolean truthy gotchas (`if (process.env.VAR)`). Check for number
conversion patterns.
### ENV7: Default Values -- Weight 7, Max 7
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Dev-friendly defaults | `PORT ?? 3000` for local development | App crashes without complete `.env` | MEDIUM |
| No secret defaults | Secret vars always required, no fallback | `JWT_SECRET ?? 'default-secret'` | CRITICAL |
| Documented defaults | `.env.example` shows which vars have defaults | Developer cannot tell required vs optional | LOW |
**Detection approach:**
- Search for `??` and `||` operators after env access (Node.js)
- Search for `os.getenv('VAR', 'default')` patterns (Python)
- Flag any default on SECRET/KEY/PASSWORD/TOKEN variables
### ENV8: Documentation -- Weight 5, Max 5
| Check | Good | Bad | Severity |
|-------|------|-----|----------|
| Inline comments | Each var in `.env.example` has a comment explaining purpose | Bare variable names only | MEDIUM |
| Required vs optional | Marked with `# Required` / `# Optional (default: 3000)` | No indication which vars are needed | MEDIUM |
| Setup instructions | README covers env setup | New developer has to guess | LOW |
---
## Phase 2: Scoring
```
ENV1 = [0-15] Variable Completeness (critical gate)
ENV2 = [0-5] Unused Variables
ENV3 = [0-15] Startup Validation (critical gate)
ENV4 = [0-15] Secret Exposure (critical gate)
ENV5 = [0-10] Environment Parity
ENV6 = [0-8] Type Safety
ENV7 = [0-7] Default Values
ENV8 = [0-5] Documentation
```
**Score = sum / 80 x 100**
**Critical gates:** ENV1=0 OR ENV3=0 OR ENV4=0 triggers FAIL.
| Grade | Percentage |
|-------|-----------|
| HEALTHY | >= 80% |
| NEEDS ATTENTION | 60-79% |
| AT RISK | 40-59% |
| CRITICAL | < 40% |
---
## Phase 3: Report
Save to: `audits/env-audit-[YYYY-MM-DD].md`
### Report Structure
```markdown
# Environment & Configuration Audit Report
## Metadata
| Field | Value |
|-------|-------|
| Project | [name] |
| Date | [YYYY-MM-DD] |
| Scope | [TARGET_ROOT or full project] |
| Stack | [Node.js / Python / Vite / Next.js] |
| Config sources | [.env, .env.example, config.ts, ...] |
| Vars defined | [N] |
| Vars used in code | [N] |
## Executive Summary
**Score: [N] / 100** -- [HEALTHY / NEEDS ATTENTION / AT RISK / CRITICAL]
| Metric | Count |
|--------|-------|
| CRITICAL findings | N |
| HIGH findings | N |
| MEDIUM findings | N |
[2-3 sentence summary]
## Dimension Scores
| # | Dimension | Score | Max | Notes |
|---|-----------|-------|-----|-------|
| ENV1 | Variable Completeness | [N] | 15 | |
| ENV2 | Unused Variables | [N] | 5 | |
| ENV3 | Startup Validation | [N] | 15 | |
| ENV4 | Secret Exposure | [N] | 15 | |
| ENV5 | Environment Parity | [N] | 10 | |
| ENV6 | Type Safety | [N] | 8 | |
| ENV7 | Default Values | [N] | 7 | |
| ENV8 | Documentation | [N] | 5 | |
| **Total** | | **[N]** | **80** | |
## Variable Coverage Matrix
| Variable | .env.example | Code Usage | Validated | Type | Default | Secret? |
|----------|-------------|------------|-----------|------|---------|---------|
| DATABASE_URL | Y | Y (3 files) | Y (Zod) | string | none (required) | Y |
| PORT | Y | Y (1 file) | N (raw) | number | 3000 | N |
| ... | | | | | | |
## Findings (sorted by severity)
[Per finding: dimension, severity, file:line, description, fix]
## Remediation Roadmap
### Quick Wins (< 1 hour)
### Short-term (1 day)
### Medium-term (1 week)
```
### Report Validation
After writing, verify:
- Dimension scores sum to total
- Finding counts match Executive Summary
- Variable Coverage Matrix includes all discovered vars
- No actual secret values appear anywhere in the report (Gate 2)
---
## Phase 4: Next-Step Routing
```
RECOMMENDED NEXT ACTION
------------------------------------
ENV4 CRITICAL (secrets exposed) -> /security-audit --static
ENV3 = 0 (no validation) -> add config schema (Zod/Joi/Pydantic)
ENV1 = 0 (no .env.example) -> create .env.example from code usage
ENV7 secret defaults -> remove default values from secret vars
Score < 60% -> fix critical gates, re-audit
Score >= 80% -> schedule next audit in 3 months
------------------------------------
```
---
## ENV-AUDIT COMPLETE
Score: [N] / 100 -- [grade]
Stack: [Node.js / Python / Vite / Next.js]
Dimensions: [N scored] | Critical gates: [PASS/FAIL]
Findings: [N critical] / [N total]
### Validity Gate (REQUIRED — print BEFORE Run line, AFTER retro append + append-runlog)
```
VALIDITY GATE
triggers_held: language=<X> framework=<X>
required_tool_calls:
scan_secrets: [<N> hits | NOT_CALLED — VIOLATES_TRIGGER]
search_text: [<N> env-var refs | NOT_CALLED — VIOLATES_TRIGGER]
search_patterns: [<N> hits | NOT_CALLED — VIOLATES_TRIGGER]
find_references: [<N> ref-checks | NOT_CALLED — VIOLATES_TRIGGER]
audit_scan: [<N> findings | NOT_CALLED — VIOLATES_TRIGGER]
postamble:
retros_log_appended: [yes(bytes_added=N) | NOT_APPENDED]
retros_md_appended: [yes(entry_count=N) | NOT_APPENDED]
verify_audit_pass: [yes(<verified>/<total>) | NOT_RUN | REJECTED]
gate_status: [PASS | FAIL — <which gates missing>]
```
If `gate_status = FAIL` → VERDICT = INCOMPLETE.
Append the Run line via the retro-gated wrapper (NOT direct `>> runs.log`):
```bash
echo -e "$RUN_LINE" | ~/.zuvo/append-runlog
```
Run: <ISO-8601-Z> env-audit <project> <N-critical> <N-total> <VERDICT> - <N>-dimensions <NOTES> <BRANCH> <SHA7> <INCLUDES> <TIER>
### Retrospective (REQUIRED)
Follow the retrospective protocol from `retrospective.md`.
Gate check → structured questions → TSV emit → markdown append.
If gate check skips: print "RETRO: skipped (trivial session)" and proceed.
After printing this block, append the `Run:` line value (without the `Run: ` prefix) to the log file path resolved per `run-logger.md`.
VERDICT: PASS (0 critical findings), WARN (1-3 critical), FAIL (4+ critical).
---
## Execution Notes
- Single-pass inline execution, no sub-agents required
- All commands use the resolved `TARGET_ROOT` from argument parsing
- Stack detection auto-selects the correct env access patterns (Node.js,
Python, Vite, Next.js)
- NestJS projects: check `ConfigModule.forRoot()` and `ConfigService` usage
patterns specifically
- Docker Compose: check both `environment:` and `env_file:` directives
- Monorepos: use `[path]` argument to scope to a specific package
- CodeSift is minimally used in this skill (env patterns are best found via
grep), but `codesift-setup.md` is loaded for consistency
- The boolean truthy gotcha (`if (process.env.FLAG)` -- always true for any
non-empty string) is a common source of production bugs; flag it prominently
No comments yet. Be the first to comment!