A staging API key got into a public repo last month. Not in a config file, not in a log — in a .env that someone committed with a "wip" message and never cleaned up. The key sat in the history for eleven days. Eleven days of webhook calls from a machine I don't own.
The embarrassing part: our pipeline had a linter, a type checker, and a dependency audit. It had nothing that said "hey, this file contains a live secret."
The four-line fix
The cheapest security control I've added in years is a CI step that refuses to build when a .env file contains something that looks like a real credential.
I use dotguard for it. It's a zero-dependency Node script that scans every .env* file in the repo for hardcoded passwords, API keys, tokens, private keys, and database URLs. The whole scan is regex-based, runs locally, and takes under a second on a mid-size monorepo.
Here's what it found in a test project:
$ npx @wuchunjie/dotguard ./
Scanning: .
.env (5 issues)
L 1 | Hardcoded password
DB_PASSWORD=***
L 2 | API key
STRIPE_API_KEY=***
L 4 | API key
OPENAI_API_KEY=***
L 3 | Access token
GITHUB_TOKEN=***
L 5 | Database URL
DATABASE_URL=postgres://user:***@localhost:5432/app
--------------------------------
5 potential secrets exposed!
Add .env to .gitignore & use .env.example instead.
Note what it does not do: it doesn't ring any SaaS bell. No upload, no account, no telemetry. The scanner never sees your file — it reads it from your own disk. That matters more than it should in 2026, and I'll write a separate post about why I stopped trusting cloud-based secret scanners.
The CI gate
The tool exits 1 when it finds a potential secret. That single fact is the whole gate:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Scan for exposed secrets
run: npx @wuchunjie/dotguard .
That's it. Four meaningful lines. If a PR introduces STRIPE_SECRET_KEY=sk_live_..., the build goes red before a human ever reviews the diff.
I deliberately keep it as a hard failure, not a warning. A warning becomes background noise inside a month. A red build gets fixed in the same sitting.
What it catches (and what it doesn't)
Worth being honest about the limits:
-
It scans
.env*files only. A secret pasted into a README or a test fixture is out of scope — that's a different scanner's job. -
It's pattern-based. It flags things that look like secrets by key name and value shape. A credential stored under
FROB_CONFIG_BLOBwon't trip it. -
It doesn't touch git history. If the key was committed last week, dotguard on the current tree won't find it. For history you want a proper secret-rotation workflow and something like
git log -Sor a dedicated history scanner.
For the specific failure mode that hurt me — a fresh .env entering the repo in a PR — it does exactly one job and does it before merge.
The part nobody warns you about
The first week of a hard secret gate is uncomfortable. People commit .env.local because it's local. People paste "just for testing" credentials. The gate catches all of it, and every catch is a five-minute conversation that would have been a two-hour incident otherwise.
By the second week, cp .env.example .env became muscle memory on the team. That's the actual product. The regexes are the least of it.
Install it, wire it into CI today, and let the first red build be your proof of concept.
npx @wuchunjie/dotguard
More Tools
| Tool | What it does | Command |
|---|---|---|
| scaffoldx-cli | 12 production-ready project templates in 3 seconds | npx scaffoldx-cli |
| dotguard | Scan .env files for exposed secrets | npx @wuchunjie/dotguard |
| gitpulse | Git repo analytics in your terminal | npx @wuchunjie/gitpulse |
| snippetx | Terminal code snippet manager | npx @wuchunjie/snippetx |
If these save you time, consider buying me a coffee. All tools are MIT-licensed, zero-dependency, and run fully offline.
Top comments (0)