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
- Scanner discovers files with glob filtering
- Adapters parse each language with tree-sitter (not regex)
- IR Builder normalizes everything into a common graph
- Analysis Engine runs rules against the graph
- 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)