Skip to content

envdrift diff

Compare two .env files and show differences.

Synopsis

envdrift diff [OPTIONS] {env1} {env2}

Description

The diff command compares two .env files and shows:

  • Added variables - Present in {env2} but not {env1}
  • Removed variables - Present in {env1} but not {env2}
  • Changed variables - Different values between files
  • Unchanged variables - Same in both (with --include-unchanged)

This is useful for:

  • Reviewing differences between development and production
  • Auditing environment changes before deployment
  • Detecting drift between team members' environments

Arguments

Argument Description
{env1} Path to first .env file (baseline)
{env2} Path to second .env file (comparison)

Options

--schema, -s

Schema for sensitive field detection. When provided, sensitive fields are masked in output.

envdrift diff .env.dev .env.prod --schema config.settings:Settings

--service-dir, -d

Directory to add to Python's sys.path for schema imports.

envdrift diff .env.dev .env.prod -s config.settings:Settings -d /app/backend

--format, -f

Output format: table (default) or json. The value is case-insensitive (JSON is accepted); any other value exits with code 1 instead of silently falling back to the table view. In json mode, stdout is always pure JSON — diagnostics such as a failed --schema load are routed to stderr, and errors on the failure path are emitted as a clean {"error": "…"} object (no ANSI, no Rich prose, even under FORCE_COLOR=1) so a --format json > drift.json capture stays parseable.

Bad inputs fail cleanly with a non-zero exit, never a traceback: a directory where a file is expected reports Not a file: <path>, and a binary / non-UTF-8 file reports Could not read <path> as UTF-8 text.

# Human-readable table (default)
envdrift diff .env.dev .env.prod --format table

# Machine-readable JSON
envdrift diff .env.dev .env.prod --format json

--show-values

Show actual values instead of masking them. Use with caution - this may expose secrets!

envdrift diff .env.dev .env.prod --show-values

By default, values are shown but sensitive fields (when schema is provided) are masked.

--include-unchanged

Include variables that are identical in both files.

envdrift diff .env.dev .env.prod --include-unchanged

--exit-on-drift, --ci

Exit with code 1 after rendering the normal table or JSON output when drift is detected. Both option names are aliases.

# Suitable for a CI gate: exits 1 for added, removed, or changed variables
envdrift diff .env.dev .env.prod --exit-on-drift

# Short CI-oriented alias with machine-readable output
envdrift diff .env.dev .env.prod --format json --ci

The flag is opt-in. Without it, detected drift remains a successful comparison and exits 0, preserving the command's existing scripting behavior. Identical files exit 0 with or without the flag.

--normalize, --strict

Normalize values before comparing so trivially-equivalent strings don't show up as drift. Enabled by default; pass --strict to fall back to raw string compare.

When --normalize is in effect, two values are considered equal if:

  • they match after stripping leading/trailing whitespace (DATABASE_URL="foo " vs DATABASE_URL=foo),
  • both look like booleans (true|false|yes|no|on|off|1|0, any case) and share the same truthiness (DEBUG=true vs DEBUG=True), or
  • both look like JSON lists/objects and parse to the same structure, regardless of single- vs double-quote style (CORS_ORIGINS=["http://x"] vs CORS_ORIGINS=['http://x']).

When --schema is also passed, each value is coerced through the matching Pydantic field type (bool, int, list[str], Literal[...], etc.) using the same pydantic-settings semantics validate uses, so the two commands always agree. Complex types (list/dict/nested models) are JSON-decoded first, like the real env source. Schema coercion is definitive in exactly two cases:

  • Both sides coerce to the same value: equal (PORT=1 vs PORT=01).
  • One side coerces and the other fails (it would crash the real app): that is drift — e.g. PORT=1 vs PORT=true under PORT: int is CHANGED; the bool-truthiness rule above never overrules a non-bool schema type.

Everything else — variables not in the schema, values that coerce on both sides but to different values, or values failing coercion on both sides — falls back to the universal rules above (minus the bool rule when a typed field failed coercion on both sides). Note the wider bool spellings Pydantic accepts (t/f/y/n, ...) only compare equal through this schema path; the universal bool rule recognizes just true|false|yes|no|on|off|1|0.

# Default — normalization on
envdrift diff .env.dev .env.prod

# Disable normalization for strict raw-string compare
envdrift diff .env.dev .env.prod --strict

# Schema-aware coercion plus normalization
envdrift diff .env.dev .env.prod --schema config.settings:Settings

Displayed values are always the original ones from the file — normalization only affects the changed / unchanged classification, never what you see in the table or JSON output.

Examples

Basic Comparison

envdrift diff .env.development .env.production

Output:

╭────────────────────── envdrift diff ──────────────────────╮
│ Comparing: .env.development vs .env.production            │
╰───────────────────────────────────────────────────────────╯

┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Variable        ┃ .env.development┃ .env.production ┃ Status   ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ DEBUG           │ true            │ false           │ changed  │
│ LOG_LEVEL       │ DEBUG           │ WARNING         │ changed  │
│ SENTRY_DSN      │ (missing)       │ https://...     │ added    │
│ DEV_ONLY_VAR    │ testing         │ (missing)       │ removed  │
└─────────────────┴─────────────────┴─────────────────┴──────────┘

Summary: 2 changed, 1 added, 1 removed

Drift detected between environments

JSON Output for CI/CD

envdrift diff .env.dev .env.prod --format json

Output:

{
  "env1": ".env.dev",
  "env2": ".env.prod",
  "summary": {
    "added": 1,
    "removed": 1,
    "changed": 2,
    "has_drift": true
  },
  "differences": [
    {
      "name": "DEBUG",
      "type": "changed",
      "value_env1": "true",
      "value_env2": "false",
      "sensitive": false
    },
    {
      "name": "SENTRY_DSN",
      "type": "added",
      "value_env1": null,
      "value_env2": "https://...",
      "sensitive": false
    }
  ]
}

With Schema for Sensitive Detection

envdrift diff .env.dev .env.prod --schema config.settings:Settings

Sensitive fields (marked with json_schema_extra={"sensitive": True}) are labeled in output.

Show All Variables

envdrift diff .env.dev .env.prod --include-unchanged

Expose Values (Use with Caution)

envdrift diff .env.dev .env.prod --show-values

CI/CD Drift Detection

To fail a CI job when drift exists, enable the opt-in exit gate:

- name: Gate on environment drift
  run: envdrift diff .env.development .env.production --exit-on-drift

If a later step needs to parse and comment on the report, the default exit-0 behavior remains available:

# GitHub Actions - Comment on PR with drift report
- name: Check drift
  id: drift
  run: |
    envdrift diff .env.development .env.production --format json > drift.json

- name: Comment on PR
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const drift = JSON.parse(fs.readFileSync('drift.json', 'utf8'));

      if (drift.summary.has_drift) {
        github.rest.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: `## Environment Drift Detected\n\n` +
                `- Added: ${drift.summary.added}\n` +
                `- Removed: ${drift.summary.removed}\n` +
                `- Changed: ${drift.summary.changed}`
        });
      }

Exit Codes

Code Meaning
0 Comparison completed; this remains the default even when drift exists
0 --exit-on-drift / --ci: files match
1 --exit-on-drift / --ci: drift was detected after output
1 Invalid format or unreadable, missing, or invalid input

Diff Types

Type Description
added Variable exists in {env2} but not {env1}
removed Variable exists in {env1} but not {env2}
changed Variable exists in both but with different values
unchanged Variable is identical in both files

Use Cases

Pre-Deployment Review

Before deploying, compare staging and production:

envdrift diff .env.staging .env.production

Onboarding New Team Members

Compare your .env with the template:

envdrift diff .env.example .env

Detecting Configuration Drift

Monitor differences across environments:

for env in staging production; do
  echo "=== Development vs $env ==="
  envdrift diff .env.development .env.$env
done

See Also