Rewriting history with git filter-repo Jump to heading
Sometimes a repositoryβs history has to change: a directory of generated binaries bloats every clone, a vendored library must move to its own repository, a confidential file was committed years ago. git filter-branch used to be the tool, and it was slow, easy to misuse and full of traps. git filter-repo replaced it and is now what the Git project itself recommends. It rewrites every commit in one fast pass, refuses to run on a repository that is not a fresh clone, and removes the leftovers that made old rewrites leak. The tool is the easy part; the difficult parts are choosing the right operation and coordinating every clone that holds the old history. This page covers both, within history rewriting and recovery.
When to use this approach Jump to heading
- A path must disappear from all history β large binaries, generated output, a file that should never have been committed.
- File contents must change throughout history, such as replacing a leaked value with a placeholder.
- A repository is being restructured: a subdirectory becomes its own repository, or the whole tree moves under a prefix for a merge into a monorepo.
- For a leaked credential, revoke it first; rewriting is cleanup, as explained in removing a leaked secret from Git history.
Step 1 β Install it and start from a fresh mirror clone Jump to heading
git filter-repo is a single Python script, packaged by most distributions. It refuses to run on a repository with existing work, to stop you rewriting your only copy. Work in a fresh mirror clone, and keep a second, untouched backup.
python3 -m pip install --user git-filter-repo # or your package manager
git clone --mirror [email protected]:acme/app.git app-rewrite.git
git clone --mirror [email protected]:acme/app.git app-backup-$(date +%F).git
cd app-rewrite.git Step 2 β Analyse before you rewrite Jump to heading
Generate a report of what the history contains. It lists the largest paths, deleted files that still occupy space, and renames β usually enough to decide exactly what to remove.
git filter-repo --analyze
ls .git/filter-repo/analysis/ 2>/dev/null || ls filter-repo/analysis/
head -20 filter-repo/analysis/path-deleted-sizes.txt
head -20 filter-repo/analysis/directories-all-sizes.txt For a detailed walk-through of finding what is large, see auditing a repository for large blobs.
Step 3 β Choose the operation Jump to heading
Most rewrites are one of four operations. Pick the narrowest that does the job.
# Remove a path from all history
git filter-repo --invert-paths --path build/artifacts/
# Remove files by glob, wherever they are
git filter-repo --invert-paths --path-glob '*.psd'
# Replace text in every version of every file (one rule per line in the file)
printf 'literal:hunter2==>REDACTED\nregex:AKIA[0-9A-Z]{16}==>AKIA_REDACTED\n' > replacements.txt
git filter-repo --replace-text replacements.txt
# Keep only a subdirectory, and make it the new root
git filter-repo --subdirectory-filter services/billing β οΈ SAFETY WARNING: Every one of these changes the hash of every commit from the first affected commit onwards. All existing clones, forks, open pull requests, CI caches and links to commit hashes become references to history that no longer exists on the server. Do not run the rewrite against your only copy, and do not push it until every step of the coordination plan in Step 5 is ready. The untouched mirror from Step 1 is your recovery path:
git push --mirror --forcefrom it restores the original history.
Step 4 β Verify the result before pushing Jump to heading
Check that the unwanted content is gone, that nothing else changed, and that the repository is the size you expect.
# The path no longer appears in any commit
git log --all --oneline -- build/artifacts/ | wc -l # expect 0
# Replaced text no longer appears anywhere
git grep -n 'hunter2' $(git rev-list --all) | head -1 # expect no output
# Commit count is unchanged (unless the rewrite emptied some commits)
git rev-list --all --count; git -C ../app-backup-*.git rev-list --all --count
# Size after repacking
git count-objects -vH | grep size-pack Commits that only touched removed paths become empty and are pruned by default, so the count may drop slightly. A large drop means the filter removed more than intended.
Step 5 β Coordinate the push and every other clone Jump to heading
The rewrite is local until pushed. Plan the cut-over: freeze pushes, push the rewritten history, and have everyone re-clone. Old clones that push again will reintroduce the removed history.
# filter-repo removes the origin remote as a safety measure β add it back deliberately
git remote add origin [email protected]:acme/app.git
git push --force --mirror origin After the push, hosted forges may still serve the removed content through cached views of old pull requests or forks until their own garbage collection runs; contact the forgeβs support for removal if the content is sensitive.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Can I rewrite only one branch? Jump to heading
filter-repo can be limited with --refs, but a partial rewrite leaves the unwanted content reachable from the other branches and tags. For removals that matter, rewrite everything.
What happens to signed commits? Jump to heading
Rewritten commits are new objects, so their signatures no longer apply and are dropped. If your gates require signatures, plan for that: either re-sign the rewritten history with a dedicated key, or record the rewrite as an exception in your verification policy.
Is BFG Repo-Cleaner still a reasonable choice? Jump to heading
It is fast and focused on removing large files and secrets, and many teams have used it successfully. filter-repo covers the same cases and many more, and is maintained alongside Git itself, so it is the better default today.
Related Jump to heading
- History Rewriting & Recovery β the parent topic.
- Splitting a Directory into Its Own Repository β the subdirectory filter in a full migration.
- Rotating Credentials After a History Rewrite β what to do with credentials the rewrite touched.
- Recovering from an Accidental Force-Push β undoing a rewrite pushed by mistake.