Meta Description: Accidentally pushed your .env file to GitHub? Learn how to permanently purge secrets from your Git history using git filter-repo and BFG Repo-Cleaner, plus how to rotate exposed API keys safely.
Late-night commit. One quick git push. And suddenly your Supabase keys, database password, or Stripe secret token are sitting in plain text on a public GitHub repository — visible to anyone, forever (or so it seems).
If this just happened to you, take a breath. It's one of the most common mistakes in software development, and it's completely fixable. This guide walks you through why it happens, how to permanently scrub the secret from your Git history, and the one step almost every tutorial forgets to mention.
Table of Contents
- Why Adding
.envto.gitignoreDoesn't Fix It - Step 1: Stop Tracking the File
- Step 2: Update
.gitignore - Step 3: Purge the Secret From History
- Step 4: Force-Push the Cleaned History
- Best Practice: Ship a
.env.example - The Step You Cannot Skip: Rotate Your Keys
- FAQ
Why Adding .env to .gitignore Doesn't Fix It {#why-gitignore-doesnt-fix-it}
The most common mistake developers make after a leak is adding .env to .gitignore and committing again. This feels like a fix — but it isn't.
.gitignore only tells Git which untracked files to ignore going forward. Once a file has been committed even a single time, Git has already recorded a permanent snapshot of it in your repository's history. Every past commit containing that file is still sitting there, fully retrievable through git log, the GitHub commit history UI, or a simple clone of the repo.
In other words: .gitignore prevents future leaks. It does nothing to undo a past one. To actually fix this, you need to rewrite Git history itself.
Step 1: Stop Tracking the File {#step-1-stop-tracking-the-file}
First, untrack the file without deleting it from your local machine:
git rm --cached .env
The --cached flag is the important part here — it removes .env from Git's index (tracking system) while leaving the physical file untouched on your disk.
Step 2: Update .gitignore {#step-2-update-gitignore}
Now make sure Git never tracks it again:
echo ".env" >> .gitignore
git add .gitignore
git commit -m "chore: ignore .env file"
Step 3: Purge the Secret From History {#step-3-purge-the-secret-from-history}
This is where the actual cleanup happens. You have two solid options — pick one.
Option A: git filter-repo (Recommended)
Git's own documentation now recommends git filter-repo over the older filter-branch command, since it's significantly faster, safer, and less prone to edge-case bugs on large repositories.
pip install git-filter-repo
git filter-repo --path .env --invert-paths
This tells Git: "Rewrite every commit in this repo, and strip out any reference to .env."
Option B: git filter-branch (Legacy, still widely used)
If you can't install git-filter-repo for some reason, the older built-in command still works:
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
What's actually happening here:
| Flag / Command | Purpose |
|---|---|
--index-filter |
Rewrites commits by editing the Git index directly, without checking out files — much faster than a full working-tree filter |
git rm --cached --ignore-unmatch .env |
Removes .env from every commit; --ignore-unmatch stops it from failing on commits that never had the file |
--prune-empty |
Deletes any commit that becomes empty once .env is removed |
--all |
Applies the rewrite across all branches and tags, not just the current one |
Option C: BFG Repo-Cleaner (Best for Large Repos)
For big repositories with deep history, BFG Repo-Cleaner is often faster than both of the above:
java -jar bfg.jar --delete-files .env
git reflog expire --expire=now --all && git gc --prune=now --aggressive
Step 4: Force-Push the Cleaned History {#step-4-force-push-the-cleaned-history}
Since the commit history has been rewritten, your local branch no longer matches what's on GitHub. A normal push will be rejected — you need to force it:
git push origin main --force
If your repo has multiple branches:
git push origin --force --all
git push origin --force --tags
Heads up: This rewrites history for everyone. If others have cloned the repo, they'll need to re-clone or hard-reset their local copies — a rewritten history will cause conflicts with their existing branches.
Best Practice: Ship a .env.example {#best-practice-ship-an-envexample}
Other developers (and future-you) still need to know which environment variables the project requires. Since .env is now ignored, commit a template with dummy values instead:
# Supabase Configuration
PROJECT_URL="https://your-project-ref.supabase.co"
ANON_KEY="your-anon-key-here"
# Sentry Configuration (Error Tracking)
SENTRY_DSN="https://your-sentry-dsn"
git add .env.example
git commit -m "feat: add .env.example template"
git push origin main
The Step You Cannot Skip: Rotate Your Keys {#rotate-your-keys}
Here's the part most guides treat as an afterthought — but it's actually the most important step in this entire process.
Rewriting Git history does not undo the exposure. The moment your .env file was pushed to a public (or even private) GitHub repo, automated bots and scrapers may have already indexed it. GitHub's own secret-scanning, along with countless third-party scanners, actively crawl public commits looking for exposed API keys — often within seconds of a push.
Treat every exposed credential as compromised, regardless of how quickly you caught it. Immediately:
- Revoke and regenerate API keys (Supabase, Stripe, Razorpay, OpenAI/Anthropic, etc.)
- Rotate database passwords and connection strings
- Reissue any JWT secrets or signing keys
- Check your service dashboards for unusual activity or unexpected usage spikes
Cleaning history protects your repo going forward. Rotating keys protects your actual infrastructure.
FAQ {#faq}
Does deleting the GitHub repository remove the leaked secret?
Not reliably. Forks, cached clones, and crawler indexes may still hold a copy of the history. Rotating the credential is the only guaranteed fix.
Is git filter-branch deprecated?
It still works and ships with Git, but the official documentation recommends git filter-repo for being faster and safer on large histories.
Will force-pushing break my collaborators' local repos?
Yes, potentially. Anyone with the old history will need to re-clone the repo or reset their local branch to match the new, rewritten history.
Can I prevent this from happening again?
Use a pre-commit hook (e.g., git-secrets, gitleaks, or detect-secrets) that scans staged files for credential patterns before every commit.
Conclusion
Leaking a .env file is one of the most common — and most stressful — mistakes a developer can make. The fix is straightforward once you understand it: stop tracking the file, ignore it permanently, rewrite history to erase all trace of it, and force-push the clean version. But none of that matters if you skip the final step — rotate every exposed credential immediately, no exceptions.
Happy (and secure) coding!
Top comments (0)