A .env file holding defaults belongs in Git 1,932 . One holding a live database password is a breach waiting for the day the repository is made public, forked, or cloned onto a laptop that gets stolen. Commit nothing you would not paste into a public issue; let the platform supply the rest.
.env
.env.*
!.env.example
*.pemThe ! line matters: ignoring .env.* and then re-allowing .env.example gives contributors a file to copy with no chance of a real value beside it. Generate that template from the live file, writing only the keys.
import { readFileSync, writeFileSync } from 'node:fs';
import { parseEnv } from 'node:util';
const keys = Object.keys(parseEnv(readFileSync('.env', 'utf8')));
writeFileSync('.env.example', keys.map((k) => `${k}=`).join('\n') + '\n');
console.log(`wrote .env.example with ${keys.length} keys`);It prints wrote .env.example with 4 keys for the file above. Deleting a committed secret in a later commit fixes nothing: the blob stays in the history, in every clone, and in any fork. Rotate the credential first — assume it is burned the moment it reaches a remote — then rewrite history with git-filter-repo 13,338 (github.com/newren/git-filter-repo (https://github.com/newren/git-filter-repo 13,338 )) and force-push.
| Tool | License | What it does | Where it runs |
|---|---|---|---|
| gitleaks 8.30.1 29,524 | MIT | Entropy scan of diffs | pre-commit hook, CI |
| TruffleHog 3.97.5 28,147 | AGPL-3.0 | Verifies found keys | CI sweeps |
| SOPS 3.13.3 23,231 | MPL-2.0 | Encrypts file values | dev machines, CD |
| dotenvx 2.28.0 5,814 | BSD-3-Clause | Encrypts .env | local, production |
gitleaks protect --staged in a pre-commit hook takes well under a second on a normal diff, and gitleaks detect in CI catches what a machine that skipped the hook let through. SOPS and dotenvx go further: commit the file encrypted, and give the servers only the decryption key.