DEV Community

Amit Feldman
Amit Feldman

Posted on

I built a CLI to stop myself from committing API keys (and broken .env files)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 like your-key-here don'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()
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
theoephraim profile image
Theo Ephraim • Edited

Just use varlock - free and open source. Large active userbase. Plugins for everything. Tons of very cool features.