Every team I have watched onboard a new engineer eventually hits the same failure mode: the app starts on one laptop and dies on another because somebody forgot to document REDIS_URL. That should be a ten-second local check, not a Slack archaeology expedition.
So I built envalign — a small, zero-dependency Python CLI that keeps .env, .env.example, and real code usage aligned.
The actual problem
Environment variables fail in three boring, expensive ways:
-
Local drift.
.env.examplehas 40 keys. Your.envhas 35. You find out at 11pm when a background job raisesKeyError. -
Docs drift. Someone adds
process.env.STRIPE_WEBHOOK_SECRETin a PR and never updates the example file. The next hire has no idea it exists. -
Example-file accidents. A real API key gets pasted into
.env.example"just for testing" and lands in git history forever.
Format linters like dotenv-linter are great for spacing and ordering. Secret scanners like gitleaks watch history. Neither answers the question "is this project’s env story actually consistent right now?"
What envalign does
pip install git+https://github.com/SybilGambleyyu/envalign.git
# Diff .env against .env.example
envalign check
# Also scan source for undocumented env vars
envalign check --scan
# List every env reference found in code
envalign scan -v
# Append missing keys from the example into .env
envalign fix
On a deliberately broken demo in the repo:
$ envalign check examples/demo_app --scan
error API_SECRET: in .env.example but missing from .env
error LOG_LEVEL: in .env.example but missing from .env
error FEATURE_FLAG: used in code but not listed in .env.example app.py:11
Exit code 1. Wire it into CI with --strict and PRs that introduce a new os.getenv("...") without documenting it simply cannot merge cleanly.
What it scans
Static string keys only — no heroics, no runtime tracing:
-
JavaScript / TypeScript:
process.env.FOO,import.meta.env.VITE_FOO -
Python:
os.getenv,os.environ[...] - Go, Ruby, PHP, Rust, Java, C#
-
Shell / Compose:
$FOO/${FOO}(ALL_CAPS, with common OS noise filtered)
node_modules, virtualenvs, dist, and similar directories are skipped. Dynamic keys like os.getenv(name) are intentionally ignored — if the name is not a literal, a static tool should not pretend to know it.
Design choices I care about
- Zero dependencies. stdlib only. If you have Python 3.9+, you can run it.
-
CI-native. Exit codes,
--strict,--format json. -
Fix is conservative.
envalign fixcopies safe defaults (ports, flags) but blanks anything that looks secret. -
Not a format linter. It does not reorder your keys or yell about spaces around
=. Alignment, not aesthetics.
A CI snippet you can paste
- name: Check env alignment
run: |
pip install git+https://github.com/SybilGambleyyu/envalign.git
envalign check --scan --strict
Commit .env.example. Never commit .env. Let the bot be the one who notices when they diverge.
Try it
Source, tests, and a runnable demo live at github.com/SybilGambleyyu/envalign. MIT licensed. If it saves you one bad deploy or one confused onboarding day, it did its job.
Feedback via GitHub issues is welcome — especially languages or env access patterns the scanner currently misses.
SOCIAL SHARE CARD GENERATOR