Env drift hunter
Skill takeaseatventure/devops-skills/skills/env-drift-hunter
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.From its SKILL.md
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.
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.