Use code SUMMERCODING at checkout for one month free Get Started →
All posts
SecurityJul 18, 20268 min read

How to Remove Secrets From Your Git History

Step-by-step guide to removing exposed API keys, tokens, and credentials from your git commit history using git filter-repo and BFG.

Deleting a file or removing a secret from your latest commit does not remove it from git history. Anyone with access to your repo can still find it by browsing old commits. This guide walks through how to actually remove secrets from your git history permanently.

First, Rotate the Secret Immediately

Before doing anything with git, go to the source and revoke the exposed credential. The AWS IAM console, the Stripe dashboard, the OpenAI API keys page, wherever the key was issued. Do this first, before you touch history at all.

Treat the credential as already compromised, regardless of how long it has been sitting in the repo or whether the repo is public or private. Automated scanners monitor commits continuously, and a private repo can still leak through a fork, a CI log, or a former collaborator's local clone.

Cleaning git history does not undo any damage that may already have occurred. It stops the secret from being visible going forward. It does not un-leak it.

Why git rm and git commit Do Not Work

Git history is append-only by design. Every commit is a snapshot, and once you push a commit, that snapshot exists permanently in the repo's history unless you deliberately rewrite it. Running git rm secrets.env and committing the change only removes the file from the current snapshot going forward. It does nothing to the commit where you first added it.

That old commit is still reachable. git log -p will show it, git show <commit>will print it, and GitHub's commit browser will happily display the diff where the secret was introduced. If someone has the commit SHA or just scrolls back through history, they see the original value. Deleting the file from HEAD is not deleting the secret. You need to rewrite history, not just update the latest state.

Option 1: git filter-repo (Recommended)

git filter-repo is the officially recommended tool for this, and it replaces the older git filter-branch, which is slower and easier to use incorrectly. Install it with:

pip install git-filter-repo

To remove a specific file from every commit in history:

git filter-repo --path secrets.env --invert-paths

To replace a specific string across all of history, instead of removing an entire file, create a text file with the pattern to replace:

# expressions.txt
EXPOSED_KEY==>REDACTED

Then run:

git filter-repo --replace-text expressions.txt

After the rewrite completes, force push the result:

git push origin --force --all

This rewrites every commit SHA after the point where the secret was introduced. That is expected, but it affects everyone who has a local clone. Anyone who pulls normally after this will hit a diverged history error. They need to re-clone or reset to the new history, not merge it in.

Option 2: BFG Repo Cleaner

BFG Repo Cleaner is faster than git filter-repo on large repos and has simpler syntax for the common cases. It requires Java to run. To delete a specific file from history:

bfg --delete-files secrets.env

To replace a list of known secret values across all commits, put the values in a text file and run:

bfg --replace-text passwords.txt

BFG does not rewrite history in place immediately. After it finishes, you still need to clean up the old objects and finalize the rewrite:

git reflog expire --expire=now --all
git gc --prune=now --aggressive

After Rewriting History

Rewriting history locally does nothing on its own. You need to push the result and make sure everyone else is working off the new history, not the old one:

  • Force push all branches: git push origin --force --all
  • Force push all tags: git push origin --force --tags
  • Notify every collaborator to re-clone the repo. Their local copies still have the old history with the secret in it, and a normal git pull will not fix that.
  • If the repo is public, contact GitHub support to clear cached views and any forks or pull request references that might still point at the old commits.
  • Rotate the secret again if there was any delay between discovery and cleanup. A key that sat exposed for even a few hours should be treated as burned.

How to Stop This Happening Again

Getting the history clean is the reactive part. The real fix is not needing to do this again:

  • Add sensitive files to .gitignore before writing any secrets to them, not after the first commit.
  • Use pre-commit hooks to block credential patterns from being committed in the first place, so the secret never makes it into a commit at all.
  • Run regular scans on your repo and commit history. Scan your GitHub repo for secrets covers the tools and approach in detail.
  • Store secrets in proper secret managers, not in code or config files that live in the repo.

For the specific patterns that give away exposed keys, see GitHub secret scanning. For the rest of your pre-launch security surface, the GitHub repo security checklist covers what else to check before you ship. And if you want to confirm your history is actually clean after a rewrite, scan your repo for exposed secrets and verify nothing was missed.

Rewriting git history is a last resort, but sometimes it is the only option. The real fix is catching secrets before they ever get committed. Once your history is clean, put the prevention steps in place so you are not doing this again in six months.

Verify the cleanup

Confirm your git history is actually clean

GitDoctor scans your GitHub repository, including commit history, for exposed secrets, missing auth checks, and dependency vulnerabilities. Each finding comes with an AI prompt to fix it. First scan is free.

Scan your repo