Rewriting an entire repository's history
An old committed secret lives in every clone. git filter-repo rewrites all history to strip a path (--path X --invert-paths) or replace text. Every commit hash changes, so you force-push and re-clone everywhere — and a leaked secret must still be rotated: it is already public.
Someone committed config/prod.env with a live AWS key eight months and four hundred commits ago. Deleting it in a new commit does nothing: the key still sits in the old commit, in every clone, in every fork, and in the remote’s history. git revert and even git rebase only touch recent commits — this is buried deep, in every version of the tree since. To truly remove it you must rewrite the entire history, every commit from that point to HEAD.
By the end of this lesson you will be able to strip a file or a secret from a repository’s whole history with git filter-repo, and you will know the two things that must follow — a coordinated re-clone and rotating the leaked credential — or the cleanup is theatre.
After this lesson you can rewrite a repository’s entire history to purge a leaked secret or a large file using git filter-repo, explain why every commit hash changes, and carry out the mandatory aftermath: force-push, force everyone to re-clone, and rotate the exposed secret.
The problem is depth: a secret in an old commit is in every version of history, so you must rewrite all of it. Git history is a chain where each commit’s hash is computed from its content and its parent’s hash. A secret committed long ago is present in that commit’s tree and inherited forward by every descendant. The everyday undo tools do not reach it — git revert adds a new commit, git rebase -i edits recent commits, but neither erases content from hundreds of commits back. You need a tool that walks the whole commit graph and rebuilds every commit without the offending content.
git filter-repo is the modern, recommended tool for whole-history rewrites. It is a separate install (pip install git-filter-repo), not built into Git, and it operates on a fresh clone by design. To remove a path from all of history:
# Strip a file from every commit it ever appeared in
git filter-repo --path config/prod.env --invert-paths--path selects the file; --invert-paths inverts the selection to mean “keep everything except this.” To scrub a secret value wherever it appears (even in files you keep), use a replacement file:
# secrets.txt contains: AKIA...EXAMPLE==>REMOVED
git filter-repo --replace-text secrets.txtfilter-repo rewrites every commit, dropping the file or redacting the text, and is dramatically faster and safer than the old alternatives.
git filter-branch is deprecated; BFG Repo-Cleaner is the other accepted option. You will still find filter-branch in old answers — Git’s own documentation now warns against it: it is glacially slow on real repos and riddled with foot-guns (it silently mishandles edge cases and can leave a half-rewritten history). The Git project explicitly recommends filter-repo instead. For the narrow job of deleting big blobs or known secrets, BFG Repo-Cleaner is a fast, simple Java tool (bfg --delete-files prod.env, bfg --replace-text secrets.txt). Reach for filter-repo for general rewrites and BFG when its narrower feature set fits; avoid filter-branch.
Every commit hash changes, so the rewrite is only half the job — the aftermath is mandatory. Because each hash depends on content and parent, rebuilding a commit produces a new SHA, and every commit after it gets a new SHA too. The rewritten history is a different chain that has diverged from what the remote and every clone hold. Three things must follow:
# 1. Force the new history onto the remote (overwrite the old chain)
git push --force-with-lease --all
git push --force-with-lease --tags-
Everyone must re-clone or hard-reset. Anyone with an old clone still has the secret and the old hashes; a normal
git pullwill try to merge the old history back in. Teammates must discard their copies and clone fresh. -
Rotate the secret. This is non-negotiable and independent of Git. The key was exposed — it may be cached on GitHub, in forks, in CI logs, in someone’s terminal scrollback, or already scraped by a bot. Rewriting history removes it going forward but cannot un-leak what was public. Revoke and reissue the credential.
▸Common mistake
The most dangerous mistake is treating history rewrite as the fix and skipping rotation. Secret-scanning bots crawl public pushes within seconds; a leaked cloud key is often used for crypto-mining before the human even notices. By the time you run filter-repo, assume the secret is already compromised. The correct order is actually: rotate first (kill the live credential immediately), then rewrite history to stop re-leaking it. History surgery without rotation is cleaning a window while the door is wide open.
An AWS key leaked into a public repo — the full response.
A scanning alert fires: config/prod.env with a live AKIA... key is in the public main, committed months ago. The lead does not start with Git.
Minute 0 — rotate. They open the AWS console and deactivate the key immediately, issuing a new one and updating the secret store and CI. The blast radius is now closed regardless of what Git does next; the old key is dead.
Then — rewrite. On a fresh clone they run pip install git-filter-repo, then git filter-repo --path config/prod.env --invert-paths. Every commit is rebuilt without that file; all SHAs change. They git push --force-with-lease --all --tags to overwrite the remote, then ask GitHub support to purge cached views and check for forks that still carry the blob.
Finally — coordinate. They message the team: delete your local clones and re-clone, do not git pull. Two developers who had open branches re-create them on top of the new history. A week later the key is long dead, the file is gone from every reachable commit, and the postmortem notes one rule: secrets go in the secret manager, never in a committed file. Rotation made the leak harmless in minutes; the rewrite made it tidy.
▸Why this works
Why is force-push normally forbidden but required here? A normal push must be a fast-forward — it only adds commits on top of what the remote has, so no one’s history is overwritten. A history rewrite diverges from the remote: the new chain is not a descendant of the old one, so a fast-forward is impossible and Git refuses an ordinary push. --force overrides that safety, and --force-with-lease is the safer form that aborts if someone else pushed in the meantime, so you do not silently clobber a colleague’s new work. Force-push is dangerous precisely because it can erase shared history — which is exactly, and only, what a history rewrite intends.
You used git filter-repo to remove a leaked API key from all of history and force-pushed. What else is strictly required for the leak to be handled?
A secret or large file committed long ago lives in every commit since and in every clone, so removing it means rewriting the whole history — revert and rebase cannot reach that deep. git filter-repo is the modern, recommended tool: --path X --invert-paths strips a file from all history, --replace-text redacts a value everywhere. git filter-branch is deprecated (slow, foot-guns) and BFG Repo-Cleaner is the narrower alternative. Because every commit hash is rebuilt, the history diverges: you must git push --force-with-lease, make everyone re-clone, and — most importantly — rotate the leaked secret, since it was already public and rewriting cannot un-leak it. Rotate first, then rewrite. Next you will learn to prove who actually wrote a commit, with cryptographic signing.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.