Compare .env files in one command. Zero config. Security-first.
.env.example says your app needs 12 keys. Your staging .env has 9. Production has 7, and someone changed PORT to a string. This breaks at 3 AM.
# .env.example # .env.production (oops)
DATABASE_URL=... DATABASE_URL=postgres://prod/db
SECRET_KEY=... # ← MISSING - will crash
REDIS_URL=... # ← MISSING - will crash
PORT=8080 PORT="8080" # ← quoted, different type$ uvx envdiff .env.example .env.production
❌ Missing in .env.production:
SECRET_KEY
REDIS_URL
🔧 Type mismatch:
PORT: expected int (***), found string (***)Values are masked by default. No secrets in your terminal or CI logs. Add --show-values if you need to see them.
| envdiff | dotenv-linter | Manual grep | |
|---|---|---|---|
| Config required | ❌ None | ✅ YAML file | ❌ None |
| Catches missing keys | ✅ | ✅ | ❌ |
| Type checking | ✅ | ❌ | ❌ |
| Values masked by default | ✅ | ❌ | ❌ |
| CI-ready (JSON + exit codes) | ✅ | ❌ | ❌ |
| Zero install | ✅ uvx |
❌ | ✅ |
# No install needed
uvx envdiff .env.example .env.production
# Or install once
pipx install envdiff
envdiff .env.example .env
# Or pip
pip install envdiff
envdiff .env.example# Compare with default .env
envdiff .env.example
# Compare two specific files
envdiff .env.example .env.staging
# Skip type checking (only missing keys)
envdiff .env.example --missing-only
# Ignore noisy keys
envdiff .env.example --ignore NODE_ENV --ignore LOG_LEVEL# Fail on missing keys, JSON output
envdiff .env.example .env.production --exit-code --format json
# Quiet mode - only missing keys, no extra warnings
envdiff .env.example .env.production --exit-code --quiet --missing-only# Default: values masked
$ envdiff .env.example .env.production
⚠️ Extra in .env.production:
SECRET_KEY=***SENSITIVE***
PORT=***present***
# With --show-values: values visible (be careful!)
$ envdiff .env.example .env.production --show-values
⚠️ Extra in .env.production:
SECRET_KEY=sk-live-abc123
PORT=8080
# JSON always shows values (for programmatic use)
$ envdiff .env.example .env.production --format jsonusage: envdiff [-h] [--version] [--exit-code] [--quiet]
[--format {text,json}] [--ignore KEY]
[--missing-only] [--show-values]
example [target]
positional arguments:
example Example .env file (usually .env.example)
target Target file to compare (default: .env)
options:
--version, -V Show version and exit
--exit-code Exit 1 if issues found (for CI)
--quiet, -q Suppress "extra keys" warnings
--format text (default) or json
--ignore KEY Skip a key (repeatable)
--missing-only Only check for missing keys
--show-values Show actual values in output
| Check | What it catches | Example |
|---|---|---|
| Missing keys | Keys in example but not in target | SECRET_KEY exists in .env.example but not .env |
| Extra keys | Keys in target but not in example | DEBUG=true in .env but not in template (warning only) |
| Type mismatches | Value changed type | PORT=8080 vs PORT="8080" (int vs string) |
Type checking is heuristic, not semantic. It catches 8080→"8080" but won't catch false→true (both are booleans) or localhost→malicious-host (both are strings). It's a helpful signal, not a security audit. Use --missing-only if you find it noisy.
| Code | Meaning |
|---|---|
| 0 | Clean - all keys present, types match |
| 1 | Issues found - missing keys or type mismatches |
| 2 | Error - file not found or unreadable |
- name: Verify env files
run: |
pip install envdiff
envdiff .env.example .env.production --exit-code --format jsoncheck-env:
script:
- pip install envdiff
- envdiff .env.example .env.staging --exit-code --quiet --missing-onlyrepos:
- repo: local
hooks:
- id: envdiff
name: Check .env files
entry: envdiff .env.example --exit-code
language: system
files: \.env$- Values masked by default - No accidental secret exposure in terminals or CI logs
- Sensitive keys detected - Keys containing
SECRET,TOKEN,PASSWORD,KEY, etc. show***SENSITIVE***instead of masked values - JSON exposes values -
--format jsonshows actual values. This is intentional for CI tooling that needs to parse output. Use only in secure CI environments.
Masking is heuristic, not cryptographic. Value prefixes (like sk_live_ for Stripe) may be visible. This is a terminal display tool, not a redaction engine. Don't pipe output to untrusted systems.
| Trade-off | Why |
|---|---|
| Type checking is on by default | Catches real config issues; use --missing-only if noisy |
KEY substring matches MONKEY |
Over-masking is safer than under-masking |
| Value prefixes may be visible | 4-char mask reveals sk_***ef for Stripe keys |
| JSON always shows values | CI tooling needs real values to parse |
- Zero config - No
.envdiffrc, no YAML, no JSON. CLI flags only. - Security-first defaults - Values masked. Secrets protected. Explicit opt-in to see them.
- One thing well - Compare env files. Not a linter, not a validator, not a manager.
- CI-native - Exit codes, JSON output, quiet mode. Works in pipelines.
- Honest about limitations - Type checking is heuristic. Masking is best-effort. Documented, not hidden.
git clone https://github.com/0xProgress/envdiff.git
cd envdiff
pip install -e ".[dev]"
pytest # 75 tests
pytest --cov # with coverageLicensed under MIT
Issues and PRs welcome. Before adding features:
- Does it require a config file? → No.
- Does it work in CI? → Must support exit codes and JSON.
- Does it keep the tool under 500 lines? → If not, it better be worth it.
- For more info click here