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 zuvo/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.exampleplaceholder 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:
- Variables defined in
.env*files (names only) - Variables accessed in code (from extraction)
- 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: no .env.example OR multiple critical vars undocumented → triggers FAIL. The gate fires on that FINDING; an ENV1 score of 0 also fires it but is not required.
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:
throwwith env/config message,process.exiton missing env - Raw usage:
process.env.VARwithout validation wrapper - NestJS:
ConfigModule.forRoot()withvalidationSchema
Critical gate: no validation anywhere → triggers FAIL. The gate fires on that FINDING; an ENV3 score of 0 also fires it but is not required.
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
.gitignorefor.envpatterns git ls-filesfor committed.envfiles- Search for
NEXT_PUBLIC_*KEY,NEXT_PUBLIC_*SECRET,VITE_*KEYetc. - Search for hardcoded secrets:
(api_key|secret|password|token)\s*[:=]\s*['"][^'"{}$]excluding test/mock/example files - Check
.dockerignorefor.env
Critical gate: secrets committed or API keys in client-side vars → triggers FAIL. The gate fires on that FINDING; an ENV4 score of 0 also fires it but is not required.
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 — any ONE of these findings triggers FAIL, whatever the numeric score:
| Gate | Fires on |
|---|---|
| ENV1 | no .env.example, OR multiple critical vars undocumented |
| ENV3 | no startup validation anywhere |
| ENV4 | any secret committed to the repo, OR an API key in a client-side (NEXT_PUBLIC_/VITE_) var, OR a hardcoded secret |
These gates fire on the FINDING, not on an aggregate score of zero. Read as "the dimension
scored 0" they are nearly unreachable, because every one of them spreads several checks over its
weight: ENV4 carries five checks across 15 points, so a repo with a committed .env but a correct .gitignore, no client-side leak and no hardcoded key still scores about 12/15 — and the committed secret never blocks the audit. The named condition in each parenthesis IS the trigger — a dimension score of 0
remains sufficient to fire the gate, it is simply not necessary.
| Grade | Percentage |
|---|---|
| HEALTHY | >= 80% |
| NEEDS ATTENTION | >= 60% and < 80% |
| AT RISK | >= 40% and < 60% |
| CRITICAL | < 40% |
Phase 3: Report
Save to: zuvo/audits/env-audit-[YYYY-MM-DD].md — at the project root (zuvo/ resolves via git rev-parse --show-toplevel; override $ZUVO_OUTPUT_DIR. See ../../shared/includes/report-output-location.md).
Report Structure
# 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):
printf '%b\n' "$RUN_LINE" | ~/.zuvo/append-runlog
Run: env-audit - -dimensions
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_ROOTfrom argument parsing - Stack detection auto-selects the correct env access patterns (Node.js, Python, Vite, Next.js)
- NestJS projects: check
ConfigModule.forRoot()andConfigServiceusage patterns specifically - Docker Compose: check both
environment:andenv_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.mdis 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