DEV Community

Cover image for How to Permanently Remove a Leaked .env File from Your Git History (Complete 2026 Guide)
Dhavalkurkutiya
Dhavalkurkutiya

Posted on

How to Permanently Remove a Leaked .env File from Your Git History (Complete 2026 Guide)

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

  1. Why Adding .env to .gitignore Doesn't Fix It
  2. Step 1: Stop Tracking the File
  3. Step 2: Update .gitignore
  4. Step 3: Purge the Secret From History
  5. Step 4: Force-Push the Cleaned History
  6. Best Practice: Ship a .env.example
  7. The Step You Cannot Skip: Rotate Your Keys
  8. 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

If your repo has multiple branches:

git push origin --force --all
git push origin --force --tags
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
git add .env.example
git commit -m "feat: add .env.example template"
git push origin main
Enter fullscreen mode Exit fullscreen mode

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)