DEV Community

ke jia
ke jia

Posted on

The .env.example Is a Contract: How I Stop Secret Leaks Before They Exist

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

Three properties make it a contract instead of a suggestion:

  1. Complete. Every variable the code reads appears in it. A new developer never guesses DATABASE_URL vs DB_URL — the contract tells them.
  2. 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.
  3. 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!
Enter fullscreen mode Exit fullscreen mode

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:

  1. Dev A adds PAYMENT_KEY to the code, reads it from the environment.
  2. Dev A creates it locally, never writes it into any example file (there isn't one).
  3. Dev B joins, runs the app, gets a cryptic null-pointer, finds the variable name in a Slack message from Dev A.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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)