TL;DR
Add .env* to .gitignore before your first commit. If you already pushed a .env, rotate every secret in it immediately, then scrub git history. Committing a .env.example is fine and encouraged.
Step 1 — Set up .gitignore correctly
Add these lines to the .gitignore at your project root:
# Env files
.env
.env.local
.env.*.local
.env.development
.env.production
.env.test
# Keep the example — it's safe
!.env.exampleThe ! prefix is an explicit allow — it ensures .env.example still gets committed even though .env* is ignored.
Modern frameworks ship with this pattern pre-configured. Next.js, Create React App, Nuxt, and Rails all add .env*.local to .gitignore on create. Double-check anyway — one missing line can cost you an afternoon of rotation work.
Step 2 — Untrack an already-tracked .env
If you accidentally added .env to git (even before ignoring it), it's still tracked. Remove it from the index but keep the local file:
git rm --cached .env
git commit -m "stop tracking .env"
git pushThis stops future changes from being committed, but the file is still in your git history. Anyone who clones the repo can check it out from any previous commit. If the .env had real secrets, see Step 4.
Step 3 — Commit a .env.example instead
Take your .env, strip the values, keep the keys and comments, and commit the result as .env.example. You can do this in the browser — paste your .env, download the example, commit it.
# .env.example
DATABASE_URL=
STRIPE_SECRET_KEY=
JWT_SECRET=
NEXT_PUBLIC_SITE_URL=New hires clone the repo, copy .env.example → .env.local, fill in values. Your structure is documented, your secrets are not.
Step 4 — You already pushed secrets. Do this now.
4a. Rotate every secret immediately
Git history is effectively permanent once pushed. Assume the secret is compromised. For each credential in the leaked file:
- AWS keys → IAM → delete, create new
- Stripe keys → Dashboard → Developers → API keys → roll
- GitHub tokens → Settings → Personal access tokens → revoke + regenerate
- OpenAI / Anthropic → dashboard → revoke + create
- Database passwords → change the DB user password
- JWT secrets → generate a new one → redeploy (every user will be logged out)
Scan the file first with the leak checker so you don't miss anything. It flags 15+ known secret patterns in seconds.
4b. Remove the file from git history
Use git filter-repo (recommended) or the older BFG:
# One-time install
brew install git-filter-repo
# Remove the file from every commit in history
git filter-repo --path .env --invert-paths
# Force-push (destructive — inform your team first)
git push origin --force --all
git push origin --force --tagsWarning: force-push rewrites history. Anyone who cloned the repo before this point needs to re-clone, or their copy will conflict.
4c. Audit access logs
For each rotated secret, check usage logs for the period between the leak and the rotation. If you see requests from unfamiliar IPs or at unusual times, treat it as a real incident — check for data exfiltration, new resources created, billing anomalies.
Step 5 — Automate detection
Two tools worth adding:
- gitleaks — pre-commit hook + CI scanner. Blocks commits that contain known secret patterns.
- trufflehog — scans entire git history. Use in CI to catch older leaks.
GitHub also has native secret scanning — it automatically alerts you (and sometimes the upstream provider) when known secret patterns land in a public repo.
Checklist
- [ ]
.env*in.gitignore - [ ]
.env.examplecommitted - [ ]
git ls-files | grep .envreturns only the example - [ ] pre-commit hook (gitleaks / trufflehog) installed
- [ ] host dashboard (Vercel, Railway) has all required keys set
- [ ] If a secret ever leaked: rotated, history scrubbed, access logs reviewed
FAQ
I already committed my .env — is deleting it enough?
No. Git retains the file in history. Anyone who clones the repo or checks older commits can still read it. Rotate the leaked secrets immediately, then remove the file from history with git filter-repo or BFG.
Does .gitignore work retroactively?
No. .gitignore only prevents future commits. Files already tracked must be untracked explicitly (git rm --cached .env) and the history must be rewritten if you want to remove them from past commits.
Can I commit .env.example?
Yes — and you should. It shows structure without secrets. Generate one from your live .env with the example generator.