Env drift hunter
Skill takeaseatventure/devops-skills/skills/env-drift-hunter
Agent skills that make Claude Code (and other AI coding agents) genuinely better at infrastructure, scheduling, and configuration work. Battle-tested, zero-dependency.
npx -y skills add takeaseatventure/devops-skills --skill env-drift-hunterAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Detect environment variable drift across .env files before deploy. Use when a user has multiple .env files (.env, .env.local, .env.staging, .env.production), when debugging "works on my machine" config issues, or when setting up a new environment. Scans 12 languages (Node, Python, Ruby, Go, Rust, Java, PHP, Docker, shell, YAML, TOML, JS/TS) to find where env vars are actually used and flags missing/extra/changed values across environments.
SKILL.md
7.5 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
env-drift-hunter
When to use this skill
- A user has two or more
.envfiles and wants to compare them. - A user is debugging "works locally, breaks in prod" — almost always env drift.
- A user is deploying to a new environment and needs to verify all required vars are set.
- A user is onboarding a new developer and wants to generate a
.env.example. - A user asks "what environment variables does this project actually use?"
Environment drift is the #1 cause of "works on my machine" failures. A variable
that's set in .env.local but missing in .env.production — or worse, set to a
different value — causes silent failures, wrong API endpoints, and 3am pages.
How env drift happens
.env.local .env.staging .env.production
───────────── ───────────── ─────────────────
DATABASE_URL=... DATABASE_URL=... DATABASE_URL=... ✓ same
REDIS_URL=... REDIS_URL=... (missing) ✗ MISSING in prod
LOG_LEVEL=debug LOG_LEVEL=info LOG_LEVEL=warn ~ CHANGED
API_KEY=dev_xxx API_KEY=stg_xxx (missing) ✗ MISSING in prod
STRIPE_KEY= STRIPE_KEY=sk_... STRIPE_KEY=sk_live ~ CHANGED
NEW_FEATURE_FLAG=1 (missing) (missing) ✗ ONLY in local
The dangerous ones are missing in prod (the app crashes) and changed (the app points to the wrong service). Extra-in-local is less dangerous but causes confusion.
How to hunt drift
Step 1: Find all .env files
# All env files in the project (excluding node_modules, .git)
find . -name ".env*" -not -path "*/node_modules/*" -not -path "*/.git/*" | sort
Common patterns: .env, .env.local, .env.development, .env.staging,
.env.production, .env.test, .env.example, .env.defaults.
Step 2: Parse and compare
For each .env file, extract the variable names and values. Compare them:
- Missing: present in one file but not another → potential crash in the missing env.
- Extra: present only in one file → may be a stale var or a new var not yet deployed.
- Changed: same name, different value → verify this is intentional (e.g. different API endpoints per environment is normal; different LOG_LEVEL might be a mistake).
Parsing rules:
KEY=VALUE— standardKEY="VALUE"/KEY='VALUE'— quoted values (strip quotes)export KEY=VALUE— shell-style (stripexport)# comment— ignore (but note commented-out vars, they may be intentionally disabled)- Empty value
KEY=is different from missingKEY— an empty value means "explicitly set to nothing", missing means "not set at all."
Step 3: Find what the code actually uses (12-language scan)
Don't just compare files — find which env vars the codebase actually references. A var
in .env that no code reads is dead config. A var that code reads but isn't in .env
will crash at runtime.
Language-specific env var reference patterns:
| Language | Pattern | Example |
|---|---|---|
| Node.js | process.env.KEY | process.env.DATABASE_URL |
| Python | os.environ['KEY'] / os.getenv('KEY') | os.getenv('API_KEY') |
| Ruby | ENV['KEY'] | ENV['STRIPE_SECRET'] |
| Go | os.Getenv("KEY") | os.Getenv("PORT") |
| Rust | env::var("KEY") / env!("KEY") | env::var("DATABASE_URL") |
| Java | System.getenv("KEY") | System.getenv("JWT_SECRET") |
| PHP | getenv('KEY') / $_ENV['KEY'] | getenv('API_KEY') |
| Dockerfile | ENV KEY=value / ARG KEY | ENV NODE_ENV=production |
| Shell | $KEY / ${KEY} | ${DATABASE_URL} |
| YAML/CI | ${{ secrets.KEY }} / ${KEY} | ${{ secrets.DOCKER_PASSWORD }} |
| TOML | in [env] or inline | database_url = "..." |
| JS/TS (Vite) | import.meta.env.KEY / VITE_KEY | import.meta.env.VITE_API_URL |
Regex for finding all env var references across a codebase:
process\.env\.([A-Z_][A-Z0-9_]*)
os\.(?:environ|getenv)\(?['"]([A-Z_][A-Z0-9_]*)['"]\)?
ENV\[(['"]?[A-Z_][A-Z0-9_]*)['"]?\]
os\.Getenv\("([A-Z_][A-Z0-9_]*)"\)
env(?:!)?::var\("([A-Z_][A-Z0-9_]*)"\)
System\.getenv\("([A-Z_][A-Z0-9_]*)"\)
getenv\(['"]([A-Z_][A-Z0-9_]*)['"]\)
\$\{?\{([A-Z_][A-Z0-9_]*)\}?
Step 4: Generate the drift report
Present the results as a clear diff table:
Variable .env.local .env.staging .env.production Status
───────────────────── ────────────── ────────────── ────────────────── ────────
DATABASE_URL ✓ postgres://… ✓ postgres://… ✓ postgres://… ✓ same
REDIS_URL ✓ redis://… ✓ redis://… ✗ MISSING ⚠️ MISSING
LOG_LEVEL = debug = info = warn ~ changed
API_KEY ✓ dev_xxx ✓ stg_xxx ✗ MISSING ⚠️ MISSING
SENTRY_DSN ✗ missing ✗ missing ✓ https://… ◯ prod-only
NEW_FEATURE_FLAG = 1 ✗ missing ✗ missing ◯ local-only
DEPRECATED_VAR ✓ (commented) ✓ old_value ✓ old_value 💀 dead code
Code references found but not in ANY .env file:
→ JWT_SECRET (used in src/auth.js:42)
→ SMTP_PASSWORD (used in src/email.js:18)
In .env files but no code reference found:
→ LEGACY_API_KEY (dead config, safe to remove)
Common pitfalls to flag
-
Secrets in
.env.example:.env.exampleshould have placeholder values (STRIPE_KEY=sk_test_xxx), never real secrets. If you see a real-looking key, flag it — it may have been committed by mistake. -
Production secrets in local
.env: if.env.localcontainssk_live_...(live Stripe key) or a production database URL, that's a security risk. Developers should use test/sandbox keys locally. -
Commented-out vars:
# DATABASE_URL=...means the var is disabled. This is different from the var being absent. Commented vars may indicate a feature that was toggled off and forgotten. -
.envcommitted to git: check.gitignore. If.envis NOT in.gitignore, real secrets may be in git history. Rungit log --all -- .envto check. -
Empty vs missing:
KEY=(empty string) behaves differently from the key being entirely absent. Code likeif (process.env.FEATURE_FLAG)will be falsy for empty string, butprocess.env.FEATURE_FLAG ?? 'default'will use empty string not default.
Generating .env.example
When generating a .env.example from a real .env:
- Copy all variable names, keep the structure.
- Replace all values with descriptive placeholders:
DATABASE_URL=postgres://user:pass@host:5432/db - Detect sensitive keys by name pattern:
*KEY*,*SECRET*,*PASSWORD*,*TOKEN*,*CREDENTIAL*and add a comment:# Keep this secret — never commit the real value - Add type hints as comments:
# boolean: set to "true" or "false" - Group related vars: database, cache, auth, feature-flags.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.