@adamwade2384/envcheck
v0.1.0
Published
Lint, diff, sync, and mask .env files. Keep .env and .env.example honest.
Downloads
59
Maintainers
Readme
envcheck
Lint, diff, sync, and mask .env files. Keep .env and .env.example honest.
The pain
You pull main, the app crashes with undefined is not a string, and twenty
minutes later you find out a teammate added SENDGRID_API_KEY last week.
It is in .env.example. It never made it to your .env. Nobody told you,
because nobody knew they had to.
The reverse happens too: your .env grows keys that never get documented in
.env.example, the file collects duplicates and dead lines, and one day
someone pastes the whole thing into Slack to debug something, secrets and all.
envcheck is a small CLI that makes env files boring again:
- lint catches duplicates, malformed lines, and footguns before they bite
- diff shows exactly how
.envand.env.exampledrifted apart - sync copies missing keys (with their comments) into your
.env - check is a one-word command for git hooks and CI
- mask prints an env file with secrets redacted so you can share it
No runtime config, no daemon, two tiny dependencies, a hand-written parser
that understands real-world env files (quotes, escapes, export, CRLF,
multiline values, inline comments).
Install
npm install -g envcheck-cli # installs the `envcheck` command
# or run it without installing:
npx envcheck-cli checkRequires Node 20 or newer.
Quick start
envcheck check # diff .env against .env.example in the cwd
envcheck lint .env # find problems in a single file
envcheck sync .env --from .env.example # add the keys you are missingCommands
Every command supports --json for machine-readable output. Colors are used
only when stdout is a TTY and NO_COLOR is unset.
envcheck lint <file>
Parses the file and reports problems. Errors (duplicate keys, malformed lines) exit with code 1; warnings alone exit 0.
$ envcheck lint fixtures/messy.env
fixtures/messy.env
line 3 error "API_KEY" is defined more than once (first defined on line 2) duplicate-key
line 4 warning "DB_HOST" has an empty value empty-value
line 5 warning "GREETING" has an unquoted value containing spaces (quote it to avoid surprises) unquoted-space
line 7 warning "apiToken" is not UPPER_SNAKE_CASE key-case
line 8 error missing '=' separator malformed-line
2 errors, 3 warningsRules:
| Rule | Severity | What it catches |
| --- | --- | --- |
| duplicate-key | error | the same key defined twice (the later one silently wins in most loaders) |
| malformed-line | error | no =, invalid key, unterminated quote, junk after a closing quote |
| empty-value | warning | KEY= with nothing after it |
| unquoted-space | warning | unquoted values containing spaces |
| trailing-whitespace | warning | invisible trailing spaces or tabs |
| key-case | warning | keys that are not UPPER_SNAKE_CASE |
envcheck diff <a> <b>
Compares two env files by key. Values are masked by default; pass
--show-values to see them. Built for .env vs .env.example.
$ envcheck diff fixtures/.env fixtures/.env.example
Comparing fixtures/.env (A) vs fixtures/.env.example (B)
Missing in fixtures/.env:
- SENDGRID_API_KEY
- LOG_LEVEL
Missing in fixtures/.env.example:
+ DEBUG
Different values:
~ PORT A=****** B=******
~ DATABASE_URL A=po******ev B=po******pp
~ STRIPE_SECRET_KEY A=sk******KE B=sk******me
Out of sync: 2 missing in A, 1 missing in B, 3 different (2 keys match)Exits 0 when the files are in sync, 1 when they are not.
envcheck sync <file> --from <example>
Appends keys that exist in the example but are missing from the target.
The target file is never rewritten: existing lines, comments, and key order
are preserved byte for byte. Missing keys are appended in example order,
carrying over the comments that sit directly above them in the example.
Use --dry-run to preview.
$ envcheck sync .env --from .env.example --dry-run
Would add 2 keys to .env:
+ SENDGRID_API_KEY=changeme
+ # How loud to log. One of: debug, info, warn, error.
+ LOG_LEVEL=info
(dry run, nothing written)Drop --dry-run to write. Running it again is a no-op:
$ envcheck sync .env --from .env.example
.env already has every key from .env.exampleenvcheck check
Zero-argument mode for hooks and CI. Finds .env and .env.example
(falling back to .env.sample or .env.template) in the current directory
and diffs them. Same output and exit codes as diff; exits 2 if either
file cannot be found.
$ envcheck check
Comparing .env (A) vs .env.example (B)
Missing in .env:
- SENDGRID_API_KEY
- LOG_LEVEL
...
Out of sync: 2 missing in A, 1 missing in B, 3 different (2 keys match)envcheck mask <file>
Prints the file with secret-looking values redacted, safe to paste into an
issue or a chat. A value is masked when its key name contains SECRET,
TOKEN, KEY, PASSWORD, PASSWD, PRIVATE, or CREDENTIAL, or when
the value itself is a long high-entropy blob (Shannon entropy of at least
3.7 bits per character, length 16+, no whitespace).
$ envcheck mask .env
# App
NODE_ENV=development
PORT=4000
# Database
DATABASE_URL=po******ev
DB_POOL_SIZE=10
# Third-party APIs
STRIPE_SECRET_KEY=sk******KE
DEBUG=trueNote that DATABASE_URL was caught by the entropy heuristic even though
its key name looks innocent: connection strings embed passwords.
Exit codes
| Code | Meaning |
| --- | --- |
| 0 | success; nothing to report (warnings do not fail lint) |
| 1 | problems found: lint errors, or files out of sync (diff, check) |
| 2 | usage error, unreadable file, or check could not find the files |
CI usage
GitHub Actions
jobs:
envcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# Fail the build if .env.example has lint errors.
- run: npx envcheck-cli lint .env.exampleCI usually has no .env (and should not), so lint the example file there.
Use envcheck check locally, where both files exist.
husky pre-commit hook
# .husky/pre-commit
npx envcheck-cli check || {
echo "Your .env is out of sync with .env.example."
echo "Run: npx envcheck-cli sync .env --from .env.example"
exit 1
}JSON output
Pass --json to any command. Shapes:
// envcheck lint <file> --json
{
"file": "fixtures/messy.env",
"errors": 2,
"warnings": 3,
"issues": [
{
"severity": "error", // "error" | "warning"
"rule": "duplicate-key",
"line": 3,
"key": "API_KEY", // null for issues without a key
"message": "\"API_KEY\" is defined more than once (first defined on line 2)"
}
]
}// envcheck diff <a> <b> --json (envcheck check --json is identical)
{
"a": "fixtures/.env",
"b": "fixtures/.env.example",
"missingInA": ["SENDGRID_API_KEY", "LOG_LEVEL"],
"missingInB": ["DEBUG"],
"changed": [
{ "key": "PORT", "a": "******", "b": "******" }
],
"equal": ["NODE_ENV", "DB_POOL_SIZE"],
"inSync": false,
"valuesMasked": true // false when --show-values is passed
}// envcheck sync <file> --from <example> --json
{
"file": ".env",
"from": ".env.example",
"dryRun": false,
"added": [
{ "key": "SENDGRID_API_KEY", "value": "changeme" }
]
}// envcheck mask <file> --json
{
"file": ".env",
"maskedCount": 2,
"entries": [
{ "key": "DATABASE_URL", "value": "po******ev", "masked": true, "line": 6 }
]
}What the parser understands
The .env parser is written from scratch (no dotenv dependency) and
handles the messy reality of env files:
export KEY=valueprefixes- single quotes (literal) and double quotes (with
\n,\t,\",\\and friends expanded) - multiline quoted values (PEM certificates, JSON blobs)
- full-line comments and inline comments (
KEY=value # note), including the distinction betweenvalue # commentandp#ssword - CRLF line endings
- duplicate keys (reported by
lint, last one wins fordiff)
Development
npm install
npm test # vitest, table-driven tests for parser/lint/diff/sync/mask
npm run typecheck
npm run build # tsc to dist/License
MIT, see LICENSE.
