DEV Community

Nguyễn Công Thuận Huy
Nguyễn Công Thuận Huy

Posted on

I kept deploying broken .env files, so I built a tool to catch them

I kept deploying broken .env files, so I built a tool to catch them

You know the feeling. You push to production, the app crashes, and the logs say:


Error: DATABASE_URL is not defined

You check your .env file. It's there. You check your code. It's there. Turns out you added DATABASE_URL to .env.example but forgot to update the actual .env on the server.

Or worse: you removed a variable from the code but left it in .env, and now you have a mystery secret sitting in production with no idea what it's for.

EnvDoctor is a Rust CLI tool that catches these problems before they reach production.

What it does

ash
cargo install envdoctor
cd your-project
envdoctor scan

`
EnvDoctor Scan Report
????????????????????
Project: .
Scanned: 2026-07-23 07:11:04 UTC
Duration: 0.32s

Summary
???????
Total variables: 47
Defined in .env: 12
Used in code: 84
Warnings: 5

Issues
??????
? DATABASE_URL (warning): Variable 'DATABASE_URL' is used in code but not defined in .env
at src/config.ts:12:21

? OLD_API_KEY (info): Variable 'OLD_API_KEY' is defined in .env but not used in code
at .env:5
`

Four rules out of the box

Rule What it catches
R001 Missing Definition Variable used in code but not in any .env file
R002 Unused Variable Variable defined in .env but never referenced in code
R003 Naming Convention Variable name doesn't match UPPER_SNAKE_CASE (configurable)
R004 Undocumented Variable Variable in .env but missing from .env.example

Why not just use dotenv-linter?

dotenv-linter checks .env file syntax and formatting. EnvDoctor does something different: it cross-references your code with your .env files. It knows which variables you actually use and which ones are missing.

It's like the difference between a spell checker and a linter. One checks the document, the other checks if the document makes sense.

Multi-language support

EnvDoctor uses tree-sitter AST parsing (not regex) for accurate detection across 6 languages:

  • JavaScript/TypeScript - process.env.X, import.meta.env.X
  • Python - os.environ['X'], os.getenv('X')
  • Rust - std::env::var("X"), env!("X")
  • Go - os.Getenv("X"), os.Lookupenv("X")
  • Ruby - ENV['X'], ENV.fetch('X')

Real-world test

I ran it on Payload CMS - a large TypeScript monorepo:

  • 8,666 files scanned
  • 119 unique variables detected
  • 844 code usages found
  • 123 warnings (all legitimate - variables used in examples/templates but not defined at root level)
  • Completed in under 1 second

No false positives. No crashes.

Multiple output formats

`ash

Terminal output

envdoctor scan

CI-friendly (exit code 0 or 1)

envdoctor check

HTML report

envdoctor export --format html -o report.html

JSON for tooling

envdoctor export --format json -o report.json

Explain a specific variable

envdoctor explain DATABASE_URL
`

Configuration

Create an envdoctor.toml in your project root:

` oml
[analysis]
naming_pattern = "^[A-Z][A-Z0-9_]*$"
naming_min_length = 2
strict_mode = false

[scan]
exclude = ["node_modules/", "target/", "dist/**"]
`

How it works


Scanner -> Adapter -> IR -> Analysis -> Report
| | | | |
File Tree- Common 4 rules Text/JSON/HTML
discovery sitter graph

  1. Scanner discovers files with glob filtering
  2. Adapters parse each language with tree-sitter (not regex)
  3. IR Builder normalizes everything into a common graph
  4. Analysis Engine runs rules against the graph
  5. Renderer outputs results

Installation

ash
cargo install envdoctor

Or build from source:

ash
git clone https://github.com/mangodxd/envdoctor
cd envdoctor
cargo build --release

What's next

  • CI pipeline integration (GitHub Actions)
  • More rules (unused imports in .env files, cross-service variable sharing)
  • TUI for interactive exploration

GitHub: github.com/mangodxd/envdoctor
crates.io: crates.io/crates/envdoctor

Built with Rust. MIT licensed.

Top comments (0)