A tiny environment configuration doctor for Node.js projects โ zero dependencies, CI friendly.
Every developer knows the pain: you pull the latest code, the app crashes, and twenty minutes later you discover someone added a new environment variable to .env.example that your local .env doesn't have. envfix finds those problems in one second โ and fixes them. It also spots duplicate and malformed declarations, checks that your .env is safely gitignored, and can generate or synchronize your .env.example without ever leaking a secret.
No install needed:
npx envfix๐ฉบ envfix โ .env vs .env.example
โ Missing in .env (2)
STRIPE_SECRET_KEY
REDIS_URL
โ Empty value (1)
SMTP_PASSWORD
โ 3 problem(s) found.
Run npx envfix fix to add missing variables.
Fix it:
npx envfix fixโ Added 2 variable(s) to .env:
STRIPE_SECRET_KEY=
REDIS_URL=
Missing keys are appended to your .env โ existing lines and values are never touched.
| Command / flag | Description |
|---|---|
envfix / envfix check
|
Compare .env with .env.example
|
envfix fix |
Append missing keys to .env (creates the file if needed) |
envfix doctor |
Full health check: files, syntax, consistency, Git safety |
envfix example |
Generate .env.example from .env (values omitted) |
envfix example --sync |
Append new .env keys to the example, report stale ones |
envfix --all |
Check (or doctor) every .env*.example โ env pair in the directory |
--env <path> |
Custom env file path (default: .env) |
--example <path> |
Custom example file path (default: .env.example) |
--copy-values |
With fix: copy values from the example file instead of leaving them empty |
--fill-empty |
With fix: also fill existing empty values (requires --copy-values) |
--dry-run |
fix/example: preview changes, write nothing |
--strict |
treat warnings (extras, duplicates, Git ignore status) as problems |
--json |
print a single JSON document to stdout |
--annotations |
force GitHub annotations on (auto-enabled on GitHub Actions) |
--no-annotations |
force GitHub annotations off |
-h, --help |
Show help |
-v, --version |
Show version |
envfix doctor answers a broader question than check: is my project's environment configuration healthy, consistent and safe?
npx envfix doctor๐ฉบ envfix doctor
Environment
โ .env found
โ .env.example found
โ 14 variable(s) configured
Syntax
โ Duplicate variable: API_URL
lines 7 and 19 in .env
Consistency
โ Missing: REDIS_URL
โ Empty: SMTP_PASSWORD
Git safety
โ .env is not tracked by Git
โ .env is ignored by Git
โ 1 error(s), 2 warning(s)
The Git safety section is read-only and uses your local git (no dependency), evaluated against the repository that contains the env file: it warns when .env is not ignored, and reports an error when .env is tracked โ a tracked env file stays a security risk even after it lands in .gitignore. A "safe" state is only claimed when Git confirms it; if a Git command fails, the state is reported as undetermined rather than green. Outside a Git repository (or without Git installed) the checks are simply skipped:
โ Git checks skipped โ not a Git repository
Doctor severities: missing files, missing variables, syntax errors (invalid names, unclosed quotes) and a tracked .env are errors (exit 1); empty variables, extras, duplicates and an unignored .env are warnings (exit 0, or 1 with --strict). Skipped Git checks never fail the run. doctor --all checks every discovered pair, and doctor --json / annotations work like the other commands.
fix never touches existing lines by default. With explicit opt-in it can fill variables that exist but are empty:
npx envfix fix --fill-empty --copy-valuesโ Filled 2 empty variable(s) in .env:
PORT=<copied>
API_URL=<copied>
-
--fill-emptyrequires--copy-valuesโ fill values always come from the example file. - Copied values round-trip exactly: reparsing the written
.envyields the original value byte-for-byte (quoting is chosen automatically). A value the dotenv grammar cannot represent aborts with an error instead of being written corrupted. - Non-empty values are never overwritten.
- Only truly empty lines (
KEY=) are edited, in place; comments, order, quotes and line endings stay untouched. - Copied values are written to
.envbut masked as<copied>in terminal output, and appear as key lists ("filled": ["PORT"]) in--json. - Works with
--dry-runto preview (Would fill โฆ) without writing.
Create an example file from your real .env โ every value is treated as a potential secret and replaced with an empty placeholder:
npx envfix example# .env # generated .env.example
DATABASE_URL=postgres://user:pw@db DATABASE_URL=
PORT=3000 โ PORT=
JWT_SECRET=super-secret JWT_SECRET=Key order, full-line comments and blank lines are preserved; values are always removed. Duplicates collapse to the first occurrence; CRLF files stay CRLF. If .env.example already exists, envfix example refuses to overwrite it โ use sync instead:
npx envfix example --syncSync appends keys that are new in .env (as empty placeholders, using the same dated append block as fix) and reports keys that no longer exist in .env as stale โ it never deletes them, and a real sync always exits 0 on completion. --dry-run is the full drift check: it exits 1 when keys would be added or when stale keys exist, and 0 only when the two files are perfectly in sync โ which makes envfix example --sync --dry-run a handy CI guard against a drifting example file.
--env and --example accept any pair of files, so every naming convention works:
# .env.prod.example โ .env.prod
npx envfix --env .env.prod --example .env.prod.example
# .env.example โ .env.production
npx envfix --env .env.production --example .env.example
# fix works the same way
npx envfix fix --env .env.prod --example .env.prod.example --copy-values
# files can live in another directory
npx envfix --env config/.env.staging --example config/.env.exampleProjects often carry several env files. --all scans the current directory for every .env*.example file and checks each one against its matching env file:
npx envfix --all๐ฉบ envfix โ checking all env pairs
.env.example โ .env โ healthy
.env.prod.example โ .env.prod โ 2 missing
.env.test.example โ .env.test โ file not found
โ 2 of 3 pair(s) need attention.
It exits with code 1 when any pair needs attention, so a single npx envfix --all step in CI covers all your environments.
A pair with extra variables (present in .env but not in the example) is still reported healthy by default โ you'll see a note like โ healthy ยท 2 extra. Pass --strict and extras turn into failures (โ 2 extra), which also flips that pair's exit contribution.
Note: fix works on a single pair โ run it per environment with --env/--example.
envfix exits with code 1 when variables are missing or empty, so you can catch configuration drift before it hits production:
# GitHub Actions example
- name: Validate environment
run: npx envfix --env .env.production --example .env.exampleExit codes depend on the command:
| Command | 0 |
1 |
2 |
|---|---|---|---|
envfix / envfix check
|
Healthy | Missing/empty (or extra with --strict) found |
Usage or file error |
envfix doctor |
No errors (no warnings either with --strict) |
Errors found (or warnings with --strict) |
Usage or file error |
envfix fix --dry-run |
Healthy | Missing/empty (or extra with --strict) found |
Usage or file error |
envfix fix |
Completed (added variables, or nothing to add) | (never) | Usage or file error |
envfix example |
Completed | (never) | Usage or file error |
envfix example --dry-run |
In sync / nothing to create | Changes pending or stale keys found | Usage or file error |
A real fix never exits 1 โ it either completes or fails with a usage/file error. In every case, missing/empty/extra describe the state before the operation ran (pre-operation detections), so envfix fix --json's document still tells you what was wrong going in.
envfix can speak GitHub Actions workflow-command annotations โ no extra setup required:
- Annotations auto-enable when
GITHUB_ACTIONS=true(set on every GitHub-hosted and self-hosted Actions runner). -
--annotationsforces them on locally, e.g. to preview what CI will show. -
--no-annotationsforces them off, even on a runner. - Missing variables become
error, empty variables becomewarning, and extra variables (not in the example) becomenoticeโ orerrorwhen combined with--strict.
- run: npx envfix --all --strictThat single step is enough: it checks every env pair, fails the build on drift, and surfaces missing/empty/extra variables as annotations in the Actions run summary โ no flags needed beyond --strict. (Annotated files typically aren't part of the PR diff โ .env is almost always gitignored โ so annotations show up on the run summary / Checks tab rather than inline on the "Files changed" tab.) Forcing annotations on locally (envfix check --annotations) produces:
::error file=.env,title=envfix::Missing environment variable: API_KEY
::warning file=.env,title=envfix::Empty environment variable: SMTP_PASSWORD
::notice file=.env,title=envfix::Extra environment variable not in .env.example: EXTRA_VAR
With --json, stdout is always exactly one JSON document โ whether the run succeeded or failed. stderr stays empty in JSON mode. --help and --version bypass this guarantee: they always print plain text, --json or not.
Success example โ envfix check --json on a project with one missing variable:
{
"command": "check",
"strict": false,
"dryRun": false,
"operationSucceeded": true,
"healthyBefore": false,
"healthyAfter": false,
"changed": false,
"wouldChange": false,
"error": null,
"pairs": [
{
"env": ".env",
"example": ".env.example",
"missing": [
"STRIPE_SECRET_KEY"
],
"empty": [],
"extra": [],
"issues": [],
"okCount": 2,
"healthyBefore": false,
"healthyAfter": false,
"added": [],
"wouldAdd": [],
"error": null
}
]
}Error example โ envfix check --json --dry-run (--dry-run is only valid with fix or example, so this is rejected before anything runs):
{
"command": null,
"strict": false,
"dryRun": false,
"operationSucceeded": false,
"healthyBefore": false,
"healthyAfter": false,
"changed": false,
"wouldChange": false,
"error": {
"code": "invalid-arguments",
"message": "--dry-run is only valid with fix or example."
},
"pairs": []
}operationSucceeded: false and a non-null error always mean exit code 2 โ check error first when scripting against the JSON output.
-
KEY=value,export KEY=value - Comments (
# ...) and inline comments (KEY=value #noteโ a#outside quotes ends the value, dotenv-style) - Quoted values:
KEY="hello world",KEY='...', andKEY=`...`โ#inside quotes is part of the value - Multiline quoted values (private keys, certificates):
PRIVATE_KEY="-----BEGIN PRIVATE KEY----- abc -----END PRIVATE KEY-----"
- Values containing
=(URLs, connection strings) - CRLF (Windows) line endings โ
fixalso appends using the file's own line-ending convention, so a CRLF file stays pure CRLF -
NO_COLORenvironment variable (plain output for logs)
Beyond missing/empty/extra variables, envfix reports problems inside the files themselves:
-
Duplicate variables (warning) โ the last declaration wins, but you're told about it:
โ Duplicate variable: API_URL lines 3 and 9 in .env -
Invalid variable names (error) โ
DATABASE-HOST=xno longer disappears silently -
Unclosed quotes (error) โ a
KEY="valuewithout a closing quote is flagged instead of corrupting the parse
Every diagnostic carries a line number, appears in --json output (pairs[].issues), and becomes a GitHub annotation with file and line set. In check, diagnostics are informational for exit-code purposes by default; with --strict, every diagnostic โ duplicates included โ makes the check fail. In doctor, error-severity diagnostics fail by default, while warnings fail only with --strict. fix never rewrites or removes existing lines โ diagnostics are informational, not destructive.
With fix --copy-values, copied values are written to .env but shown masked in the terminal (API_KEY=<copied>) so secrets from the example file don't end up in logs.
- Zero dependencies โ nothing to audit, nothing to break
- npx-first โ no global install, no project dependency required
-
Append-only fix โ your existing
.envvalues are sacred - CI friendly โ meaningful exit codes out of the box
By default envfix never:
- prints environment values to the terminal, JSON, or CI annotations
- overwrites an existing non-empty variable (not even with
--fill-empty) - deletes duplicate lines or stale example keys
- rewrites, reorders, or re-formats your files (changes are append-only or single-line fills you explicitly opted into)
- changes your line endings โ CRLF files stay CRLF
- modifies
.gitignore, commits, or runs any mutating Git command (Git checks are read-only)
npm test # runs the built-in node:test suiteRequires Node.js >= 18.
MIT ยฉ Gokhan Ozgezer