DEV Community

날다람쥐
날다람쥐

Posted on

I got tired of deploying broken configs, so I built dotenv-scan

Every team I've worked on has had this incident at least once.

Friday afternoon deploy. CI is green. You push to production. Five minutes later, someone's pinging you in Slack: the app is crashing on startup. You SSH in, check the logs, and there it is:

Error: DATABASE_URL is not defined
Enter fullscreen mode Exit fullscreen mode

You forgot to add the new environment variable to production. The .env file on your machine has it. The .env.example in the repo doesn't. Nobody noticed during review.

Half an hour of your Friday afternoon gone.

The fix is always the same — add the variable, redeploy, move on. But the problem keeps coming back. Because there's no automated check. You're relying on code review and memory.

And it's not just missing variables. The opposite problem is just as real: .env files that accumulate junk. OLD_REDIS_URL from a migration you finished six months ago. LEGACY_API_KEY for a service you sunset last quarter. They sit there, silently, because nobody wants to delete something they're not 100% sure is unused.

I've fixed this manually too many times. So I built dotenv-scan to do it automatically.


What it does

dotenv-scan scans your codebase for every env variable your code actually uses, then compares that against what's in your .env and .env.example. One command, three answers.

npx dotenv-scan scan
Enter fullscreen mode Exit fullscreen mode
dotenv-scan v1.0.0  ·  scanned 47 files in 284ms

❌  Missing  (3)
   DATABASE_URL      src/db.ts:12, src/config.ts:8
   JWT_SECRET        src/auth/middleware.ts:4
   STRIPE_API_KEY    src/payments/stripe.ts:22

⚠️  Undocumented  (2)
   INTERNAL_API_KEY
   DEBUG_MODE

🗑️  Unused  (1)
   OLD_REDIS_URL

✅  OK  (8)
   PORT, NODE_ENV, API_BASE_URL, ... (and 5 more)

────────────────────────────────────────
Run `dotenv-scan generate` to update .env.example
Enter fullscreen mode Exit fullscreen mode

No config file. No setup. Just npx and go.


The three problems it catches

Missing — your code does process.env.DATABASE_URL but .env doesn't have it. This is the Friday deploy problem. dotenv-scan catches it before you push.

Unused.env has OLD_REDIS_URL but no file in your codebase references it. Safe to delete. dotenv-scan tells you which ones.

Undocumented.env has INTERNAL_API_KEY but .env.example doesn't mention it. The next developer to clone your repo has no idea this variable needs to exist. dotenv-scan flags it.


Plugging it into CI

The check command is designed for pipelines. It exits with code 1 if any variables are missing:

# GitHub Actions
- name: Check env variables
  run: npx dotenv-scan check
Enter fullscreen mode Exit fullscreen mode

That's the whole setup. Now your Friday deploy problem becomes a PR-time failure instead.

If you want to be strict — fail on unused and undocumented too:

dotenv-scan check --strict
Enter fullscreen mode Exit fullscreen mode

Auto-generating .env.example

The part I actually use the most is generate. It writes (or updates) .env.example from your scan results:

dotenv-scan generate
Enter fullscreen mode Exit fullscreen mode

It's idempotent. If a key already exists in .env.example, it's preserved. New variables found in the scan get added. Variables in .env.example that are no longer referenced in your code get flagged.

One command to keep your docs in sync with reality.


Multi-language support

It's not just JavaScript. The scanner understands:

Language Patterns
JavaScript / TypeScript process.env.VAR, process.env['VAR']
Python os.environ['VAR'], os.getenv('VAR'), os.environ.get('VAR')
Go os.Getenv("VAR")
Ruby ENV['VAR'], ENV.fetch('VAR')

So if you have a monorepo with a Node.js API and a Python worker, one scan covers both.


The interesting part: detecting dynamic access

The hardest case to handle was process.env[dynamicKey].

Static analysis can't tell you which variable this reads. The key is computed at runtime — maybe it comes from a config file, maybe it's user input, maybe it's constructed from an enum. You can't enumerate it.

I made a deliberate call here: don't try to be clever. Instead, detect the pattern and warn the user:

⚠️  Dynamic access detected
   process.env[key]   src/config/loader.ts:34

   Static analysis can't determine which variables are accessed here.
   Make sure these variables are covered in your .env.example manually.
Enter fullscreen mode Exit fullscreen mode

Silently ignoring it would be worse — you'd get a false "all clear" and miss variables. Failing hard would be too noisy for codebases that use this pattern intentionally. A warning with the exact location felt right.


Architecture in one diagram

CLI (Commander.js)
    │
    ├── Scanner ── Walker (fast-glob) ── Extractors (per-language regex)
    │                                              │
    │                                     EnvRef[] (used variables)
    │
    ├── Parser ── dotenv parser ── EnvDef[] (defined variables)
    │
    └── Analyzer ── compares used ↔ defined ↔ documented
                         │
                    AnalysisResult
                         │
                    ┌────┴────┐
                 Reporter   Generator
                (text/json)  (.env.example)
Enter fullscreen mode Exit fullscreen mode

Each layer is independently testable. The scanner doesn't know about .env files. The analyzer doesn't know about file systems. 88 tests, zero mocks of the actual fs calls in integration tests — they run against real fixture files.


Zero runtime dependencies (almost)

Three runtime deps, all small:

  • chalk — terminal colors
  • commander — CLI argument parsing
  • fast-glob — file walking

That's it. No dotenv library — the parser is custom because I needed comment-preservation and multiline value support that dotenv packages tend to strip. No framework. Ships as a standalone CLI that you can npx without worrying about what it pulls in.


Getting started

# No install required
npx dotenv-scan scan

# Or install globally
npm install -g dotenv-scan
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/flyingsquirrel0419/dotenv-scan

If dotenv-scan saves you from a bad deploy, a ⭐ on the repo goes a long way. It's also the best way to let me know it's useful so I keep working on it.


The thing I keep coming back to is how small the fix is relative to how painful the problem is. One npx command in CI, and the whole class of "missing env variable in production" goes away. I wish I'd built this years ago.

Happy to dig into any of the implementation details in the comments — the multi-language extractor design and the dynamic access detection decision both have interesting tradeoffs worth talking through.

Top comments (4)

Collapse
 
theoephraim profile image
Theo Ephraim
Collapse
 
flyingsquirrel0419 profile image
날다람쥐

Great

Collapse
 
kushal1o1 profile image
KUSHAL BARAL • Edited

Nice :) like bro what's under the hood ,are we scanning all files for ENV using fast glob grapping with regex :)
Does it detect destructuring vars like const { DB_URL } = process.env
and Is it aware of .env.production, .env.local

Collapse
 
flyingsquirrel0419 profile image
날다람쥐

For .env.production / .env.local — currently you point it at a single
file via --dotenv .env.local.

But the parser already has a parseDotenvFiles() function built in that
reads multiple files and lets later ones override earlier ones (matching
how frameworks like Next.js merge .env → .env.local → .env.production).

The multi-file auto-discovery is on the v2.0 roadmap under
"Monorepo multi-.env support".

For now, you can run it multiple times or use the library API directly
with parseDotenvFiles().

I will fix it soon :>