You've found the bad commit. Reverting it isn't enough — the secret is still readable in the diff of any older clone. To erase it from history you have to rewrite every commit from that point forward.
git filter-repo is the modern tool. git filter-branch is deprecated (slow, buggy, footgun-laden) and the BFG Repo-Cleaner is a fast JVM-based alternative for the common cases. Install:
pip install git-filter-repo
Remove a file entirely from history:
git filter-repo --path config/secrets.json --invert-paths
Replace a literal string everywhere it appears:
echo 'AKIA1234EXAMPLE==>REDACTED' > replacements.txt
git filter-repo --replace-text replacements.txt
Strip files matching a pattern:
git filter-repo --path-glob '*.pem' --invert-paths
Each form walks every commit, rewrites its trees, and produces new commit hashes for every affected commit (and every descendant). After it finishes, your repo is locally clean — but the published version isn't yet.
The aftermath checklist — this is where most teams get burned:
git push --force --all and git push --force --tags. Every branch and tag is now a different commit chain.
- Every collaborator must re-clone. Their local refs still point at the old, secret-containing history. If they push from a stale clone, the leaked commits come right back.
- GitHub/GitLab cached views. Old commit URLs may remain accessible for hours or days; contact support to expedite purge for genuinely sensitive content.
- Open PRs become broken. They were based on old commits. Recreate or rebase them.
- The secret is still compromised. Rotating it is non-negotiable; history rewriting is hygiene, not containment.
filter-repo refuses to run in a non-fresh clone by default (it wants --force if you've got remotes), to stop accidental destruction. That guardrail exists for a reason — read the warnings before flagging through them.