Skip to content

0xProgress/envdiff

Repository files navigation

envdiff

Compare .env files in one command. Zero config. Security-first.

PyPI version Python 3.8+ License: MIT

The Problem

.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

The Solution

$ 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.

Why envdiff?

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

Quick Start

# 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

Usage

Everyday

# 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

CI/CD

# 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

Security

# 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 json

All Flags

usage: 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

What envdiff checks

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 falsetrue (both are booleans) or localhostmalicious-host (both are strings). It's a helpful signal, not a security audit. Use --missing-only if you find it noisy.

Exit Codes

Code Meaning
0 Clean - all keys present, types match
1 Issues found - missing keys or type mismatches
2 Error - file not found or unreadable

CI Integration

GitHub Actions

- name: Verify env files
  run: |
    pip install envdiff
    envdiff .env.example .env.production --exit-code --format json

GitLab CI

check-env:
  script:
    - pip install envdiff
    - envdiff .env.example .env.staging --exit-code --quiet --missing-only

Pre-commit

repos:
  - repo: local
    hooks:
      - id: envdiff
        name: Check .env files
        entry: envdiff .env.example --exit-code
        language: system
        files: \.env$

Security

  • 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 json shows 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.

Known Trade-offs

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

Philosophy

  • 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.

Development

git clone https://github.com/0xProgress/envdiff.git
cd envdiff
pip install -e ".[dev]"
pytest            # 75 tests
pytest --cov      # with coverage

License

Licensed under MIT

Contributing

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

About

Compare .env files in one command. Zero config.

Resources

License

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors