envfix

A tiny environment configuration doctor โ€” diagnose, fix and generate .env files. Zero dependencies, CI friendly.


Keywords
env, dotenv, environment, env-example, cli, check, validate, ci
License
MIT
Install
npm install envfix@1.2.0

Documentation

๐Ÿฉบ envfix

npm CI license

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.

Quick start

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.

Commands & options

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

Full checkup with doctor

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.

Fill empty values

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-empty requires --copy-values โ€” fill values always come from the example file.
  • Copied values round-trip exactly: reparsing the written .env yields 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 .env but masked as <copied> in terminal output, and appear as key lists ("filled": ["PORT"]) in --json.
  • Works with --dry-run to preview (Would fill โ€ฆ) without writing.

Generate & sync .env.example

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 --sync

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

Multiple environments

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

Check everything at once with --all

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

Use it in CI

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

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

GitHub Actions

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).
  • --annotations forces them on locally, e.g. to preview what CI will show.
  • --no-annotations forces them off, even on a runner.
  • Missing variables become error, empty variables become warning, and extra variables (not in the example) become notice โ€” or error when combined with --strict.
- run: npx envfix --all --strict

That 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

JSON output

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.

What it understands

  • 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='...', and KEY=`...` โ€” # 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 โ€” fix also appends using the file's own line-ending convention, so a CRLF file stays pure CRLF
  • NO_COLOR environment variable (plain output for logs)

Diagnostics

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=x no longer disappears silently
  • Unclosed quotes (error) โ€” a KEY="value without 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.

Why another env tool?

  • Zero dependencies โ€” nothing to audit, nothing to break
  • npx-first โ€” no global install, no project dependency required
  • Append-only fix โ€” your existing .env values are sacred
  • CI friendly โ€” meaningful exit codes out of the box

Safety guarantees

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)

Development

npm test        # runs the built-in node:test suite

Requires Node.js >= 18.

License

MIT ยฉ Gokhan Ozgezer