The 11pm key rotation
Two things have burned me more than once, and I suspect they've burned you too.
Incident one: a deploy that went green in CI and then crashed on boot in production because .env.production was missing REDIS_URL. Nobody caught it, because nothing in the pipeline actually compared the production env file against the keys the app expects. The error message was something unhelpful like undefined is not a function three stack frames away from the real problem.
Incident two: a Stripe test key committed to a repo that was later flipped from private to public. It was a test key, thankfully, but I still spent an evening rotating it, scrubbing history, and writing the postmortem nobody reads.
Both of these are embarrassingly common, and both are cheap to catch — you just have to actually check, every build, forever. Humans are bad at "every build, forever." Scripts are good at it. So I wrote one: envcheck, a zero-dependency Node CLI that does two jobs — validating .env files, and scanning repos for leaked secrets.
Job 1: Is my env config sane?
The simplest mode compares an env file against your committed .env.example:
envcheck check .env --example .env.example
On a deliberately broken file, that prints something like:
.env
✗ missing-key DATABASE_URL is required by .env.example but absent
✗ duplicate-key PORT defined on line 3 and line 9
⚠ empty-value SENTRY_DSN has no value
⚠ placeholder-default API_KEY still equals the example default
1 errors, 2 warnings — 4 keys checked
The placeholder-default warning is my favorite. It catches the case where someone copied .env.example to .env and forgot to fill in a real value — so the app boots with API_KEY=your-key-here and fails somewhere downstream with a confusing auth error.
When you want more than "same keys as the example," there's a JSON schema format with per-key types, patterns, enums, and length rules:
{
"required": ["DATABASE_URL"],
"allowExtra": false,
"properties": {
"PORT": { "type": "port" },
"NODE_ENV": { "values": ["development", "test", "production"] },
"API_KEY": { "type": "string", "minLength": 32 }
}
}
Then envcheck check .env.production --schema env.schema.json --strict fails the build on warnings too. This is the check that would have saved my crashed deploy.
Job 2: Am I about to commit a secret?
envcheck scan .
This recursively scans the repo for two classes of problems: known token formats (AWS access keys, GitHub and GitLab tokens, Stripe keys, Slack tokens, Google API keys, JWTs, -----BEGIN PRIVATE KEY----- blocks, and a dozen more), plus generic password = "…" / api_key: … assignments filtered by Shannon entropy — which catches random-looking tokens nobody wrote a regex for.
Output looks like:
src/config.js:12:15 ✗ github-token possible GitHub token ghp_…89 (40 chars)
deploy/notes.txt:4 ✗ high-entropy high-entropy string 7Kd2…Qz (36 chars)
.env ✗ env-file .env file present in scanned tree
2 errors — 42 files scanned, 3 skipped
A few design decisions that matter in practice:
- Matches are redacted. Reports are safe to archive as CI artifacts; envcheck never prints your full secret.
-
Noise control is built in.
node_modules, lockfiles, minified files, source maps, and binaries are skipped automatically, and placeholder values likeyour-key-heredon't cry wolf. A scanner that floods you with false positives gets disabled within a week. - Exit codes are boring on purpose. 0 = clean, 1 = findings, 2 = usage error. Every CI system on earth understands that.
What CI integration looks like
GitHub Actions, in full:
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npx envcheck check .env.ci --example .env.example --strict
- run: npx envcheck scan . --json > envcheck-report.json
if: always()
That's the whole thing. Zero runtime dependencies, nothing to configure, works offline, Node 18+. It also exposes --json on every command if you want to feed results into your own tooling, and a programmatic API (parseEnv, validateEnv, scanContent) if you want to embed it.
Honest limitations
- It's a scanner, not a vault. It finds secrets in files; it doesn't manage them. If you need secret storage, that's Vault/Doppler/1Password territory.
- Entropy detection has a floor. Short random tokens (under ~20 chars) often don't have enough entropy to distinguish from normal identifiers. The known-format regexes cover the common cases; genuinely obscure short tokens can slip through.
-
It won't catch a secret that's already in git history — it scans working-tree files, not
git log. If a key was committed and deleted, you still need to rotate it. (Rotate it. Scrubbing history is not enough.) - Node-only runtime. The validation logic is plain JS with no dependencies, so porting is feasible, but today you need Node 18+.
Getting it
envcheck is $12, one-time, on Gumroad: https://afeldman2.gumroad.com/l/yxtnc?utm_source=devto&utm_medium=article&utm_campaign=envcheck-launch
That gets you the complete MIT-licensed source (audit every line — it's yours), the schema format docs, example files including a deliberately-broken env file for demos, and the full 65-test suite so you can verify it does what this post claims. Cheaper than one evening of rotating a leaked key.
Top comments (1)
Just use varlock - free and open source. Large active userbase. Plugins for everything. Tons of very cool features.