A zero-dependency TypeScript CLI that compares two .env files and tells you
exactly which keys were added, removed, or changed — without ever
printing the values.
.envfiles are secrets by default. A diff tool for them has to be default-deny: no values in the output unless you opt in with a loud flag.
For every key that exists in both files, env-diff hashes the value with
SHA-256 and keeps the first 8 hex characters (32 bits). If the two
truncated hashes match, the values are the same. If they differ, the values
differ. The raw value never appears in stdout, stderr, or any formatter
output, even when --format json — unless you explicitly pass
--show-values, which prints a loud warning first.
That means you can safely pipe env-diff .env.staging .env.prod into a CI
log and the log is still safe to store in Datadog, ship to Slack, or paste
in a PR comment.
docker run --rm -v "$PWD":/work ghcr.io/sen-ltd/env-diff \
/work/.env.staging /work/.env.prodgit clone https://github.com/sen-ltd/env-diff
cd env-diff
npm install
npm run build
node dist/main.js .env.staging .env.prodRequires Node 20+. Runtime dependencies: zero.
env-diff <a.env> <b.env> [options]
Compare two env files and see what drifted:
$ env-diff .env.staging .env.prod
env-diff .env.staging .env.prod
~ DATABASE_URL (2f99fe02 -> 146593f6)
~ LOG_LEVEL (06271baf -> 0b8e9e99)
+ NEW_FEATURE_FLAG (b5bea41b)
summary: +1 added, -0 removed, ~2 changed, =1 same, 0 excludedMachine-readable for piping into jq:
$ env-diff a.env b.env --format json | jq '.counts'
{
"added": 1,
"removed": 0,
"changed": 2,
"same": 1,
"excluded": 0
}Fail a CI job when env files drift:
$ env-diff .env.staging .env.prod --fail-on-diff
# exit 1 if any add/remove/changeGitHub Actions annotations (visible on the PR "Files changed" view):
- name: check env drift
run: env-diff .env.example .env.ci --format githubIgnore keys that are expected to differ (build timestamps, CI job IDs):
$ env-diff a.env b.env --exclude TIMESTAMP,BUILD_ID
$ env-diff a.env b.env --exclude-pattern '^CI_'Really, truly need to see the values (local debugging only, never CI):
$ env-diff a.env b.env --show-values
WARNING: --show-values is enabled. Actual .env values will be printed. ...| Flag | Effect |
|---|---|
--format text|json|github |
Output format. Default text. |
--show-values |
Print actual values. Emits a loud stderr warning first. |
--exclude KEY1,KEY2 |
Ignore these literal keys. |
--exclude-pattern REGEX |
Ignore keys matching this regex. |
--fail-on-diff |
Exit 1 if anything changed. |
--include-same |
Also show keys that are identical. |
--help / --version |
Self-explanatory. |
| Code | Meaning |
|---|---|
0 |
No differences, or differences found without --fail-on-diff. |
1 |
Differences found and --fail-on-diff was set. |
2 |
Bad command-line arguments or IO error. |
| Feature | Supported |
|---|---|
KEY=value |
✅ |
export KEY=value |
✅ |
# full-line comment |
✅ |
KEY=value # trailing comment |
✅ (unquoted only) |
KEY="double quoted" with \n \r \t \\ \" escapes |
✅ |
KEY='single quoted' (literal) |
✅ |
Empty values (KEY=, KEY="") |
✅ |
| CRLF line endings | ✅ |
Variable interpolation (KEY=$OTHER) |
❌ — deliberate |
Multi-line values (\ continuation, heredocs) |
❌ — deliberate |
Malformed lines are reported to stderr with their 1-indexed line number; the rest of the file still parses.
Why SHA-256 truncated to 8 hex chars?
- Full SHA-256 is noise in the terminal. You don't need 64 characters to compare two fingerprints at a glance; 8 is enough.
- 32 bits is plenty for this job. For a typical env file with <100 keys,
the birthday collision probability per pair is
≈ n² / 2^33, which is "you should worry about cosmic rays first" territory. - It's a fingerprint, not a commitment. The tool's safety property is
"values are never printed". The hash is not defending against preimage
attacks (which would be silly anyway — for a value like
true, 8 hex chars of SHA-256 is trivially rainbow-tableable). It's just a witness.
npm install
npm test # 47 vitest tests
npm run build # emit dist/main.jsMIT