Most secret leak postmortems end with the same sentence: "the developer committed the .env by accident."
I disagree with that framing. The developer was doing exactly what the repo invited them to do: create a file the app needs, fill it in, run the app. The accident was structural, and it was set up in the first commit.
The fix I've standardized across my projects is a contract, not a policy:
Every repo ships .env.example on day one, with every variable the app reads, in the exact order the app reads them, with safe placeholder values. And .env.example is the only env file that ever gets committed.
A real example from an Express API project:
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:password@localhost:5432/myapp
JWT_SECRET=change-me-to-a-long-random-string
STRIPE_API_KEY=sk_test_...
Three properties make it a contract instead of a suggestion:
-
Complete. Every variable the code reads appears in it. A new developer never guesses
DATABASE_URLvsDB_URL— the contract tells them. -
Safe values only.
change-me,sk_test_...,password@localhost. If a real value ever appears in.env.example, that's a P1, not a code review nit. - Reviewed like code. It goes through the same PR process as anything else. Adding a new secret variable means touching the example file in the same PR — the diff is where reviewers actually see it.
Where the scanner fits
dotguard is the enforcement layer for this contract. It does two jobs:
Job 1: catch real values that slipped into env files.
$ npx @wuchunjie/dotguard .
.env (3 issues)
L 5 | Hardcoded password
DB_PASSWORD=***
L 8 | API key
STRIPE_API_KEY=***
L 11 | Database URL
DATABASE_URL=postgres://user:***@localhost:5432/app
3 potential secrets exposed!
Exit code 1, build fails, secret never merges.
Job 2: catch drift in the contract itself. dotguard checks that env files define the variables a healthy app is expected to have — it flags a missing NODE_ENV or PORT as an info-level note. That sounds trivial until a staging deploy dies at 2am on "PORT is not set." The missing-variable check turned that into a one-line diff in review.
The failure mode this kills
Without the contract, the sequence is:
- Dev A adds
PAYMENT_KEYto the code, reads it from the environment. - Dev A creates it locally, never writes it into any example file (there isn't one).
- Dev B joins, runs the app, gets a cryptic null-pointer, finds the variable name in a Slack message from Dev A.
- Someone, eventually, commits
.env"just for testing."
With the contract, step 3 is impossible (the example file documents every variable) and step 4 is caught by the scanner before merge.
The policy says "never commit secrets." The contract makes committing secrets both unnecessary and detectable. Policies rot; files in the repo don't.
Write the .env.example in the first PR. Wire the scanner into CI. Let the contract do the training.
npx @wuchunjie/dotguard
More Tools
| Tool | What it does | Command |
|---|---|---|
| scaffoldx-cli | Production-ready project templates in 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)