Remove a Leaked .env File From Git History

Suman SharmaSuman Sharma

You ran git add ., committed your .env, and pushed. Breathe. Then do these steps in order.

Step 1: Rotate every secret in the file (do this first)

Removing the file from history does not un-leak it. If it was pushed to a public repo, bots scan GitHub for keys continuously and can find them within minutes. Even in a private repo, every collaborator and every clone already has it.

For each value in the file:

  1. Create a new key in the provider's dashboard (AWS, Stripe, OpenAI, your database, and so on).
  2. Update it wherever it's used: hosting env vars, CI secrets, teammates' local files.
  3. Revoke the old key.
  4. Check the provider's logs for use you don't recognise.

Only once the old secrets are dead does cleaning history become a tidiness job rather than an emergency.

Step 2: Stop tracking the file

echo ".env*" >> .gitignore echo "!.env.example" >> .gitignore git rm --cached .env git commit -m "Stop tracking .env"

--cached removes it from Git but keeps your local copy.

If you haven't pushed yet, you can stop here and fix the last commit instead:

git rm --cached .env git commit --amend --no-edit

Step 3: Remove it from all history with git filter-repo

git filter-repo is the tool the Git project recommends for rewriting history. Install it:

brew install git-filter-repo # macOS pip install git-filter-repo # anywhere with Python

Work on a fresh clone. filter-repo insists on it, as a safety measure:

git clone https://github.com/you/repo.git repo-clean cd repo-clean git filter-repo --invert-paths --path .env

That rewrites every commit as if .env never existed. For several files, or a pattern:

git filter-repo --invert-paths --path .env --path config/secrets.json git filter-repo --invert-paths --path-glob '*.pem'

If the secret was pasted into a source file rather than living in its own file, replace the text instead:

# replacements.txt: one secret per line # sk_live_abc123==>REMOVED git filter-repo --replace-text replacements.txt

Check it worked:

git log --all --oneline -- .env # should print nothing

Step 4: Force-push the rewritten history

filter-repo removes the origin remote as a safety measure. Add it back and push every branch and tag:

git remote add origin https://github.com/you/repo.git git push origin --force --all git push origin --force --tags

You'll need to temporarily allow force-pushes if branch protection blocks them.

Step 5: Deal with everyone else's copies

Rewriting history changes every commit hash after the leak.

  • Teammates must re-clone, or reset hard to the new history. If they merge their old branches in, the file comes back.
  • Open pull requests may still reference old commits.
  • Forks and caches keep the old data. On GitHub, cached views of old commits can stay reachable by hash. GitHub's docs on removing sensitive data explain how to contact support to purge them.

This is why Step 1 matters most. You can't guarantee every copy is gone, but you can make sure the leaked keys are useless.

The BFG alternative

BFG Repo-Cleaner is an older, Java-based tool that does the same job:

bfg --delete-files .env git reflog expire --expire=now --all && git gc --prune=now --aggressive

Either works. filter-repo is more flexible and actively maintained.

Make sure it never happens again

  1. .gitignore from the first commit. Add .env* and commit a .env.example with placeholder values.
  2. A pre-commit check. Tools like gitleaks or git-secrets block commits that contain keys. Run them with Husky and lint-staged.
  3. GitHub push protection. Secret scanning with push protection rejects pushes containing known key formats.
  4. Use prefixed keys in your own APIs so scanners can recognise them. See How to Generate Strong API Keys.
  5. Stop using git add . without checking git status first.

Frequently asked questions

Is deleting the .env file in a new commit enough?

No. The file is still in every earlier commit, and anyone can check one out. You have to rewrite history to remove it, and rotate the secrets regardless.

Do I need to rotate keys if the repo is private?

Yes. Everyone with access, every CI system, and every clone already has the secret. Treat any committed secret as exposed.

Should I use git filter-branch?

No. The Git project itself recommends git filter-repo instead. It's much faster and avoids many of filter-branch's pitfalls.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.