git filter-repo: Rewriting History
git filter-repo is the modern, recommended tool for rewriting Git repository history. It replaces the old git filter-branch command, which was notoriously slow, error-prone, and deprecated since Git 2.36. With git filter-repo you can remove sensitive files that were accidentally committed, rename paths, change author emails, extract a subdirectory into its own repository, and much more — all in a fraction of the time.
Installation
Install git-filter-repo
# Using pip (recommended, works everywhere) pip install git-filter-repo # macOS with Homebrew brew install git-filter-repo # Verify installation git filter-repo --version # git-filter-repo 2.38.0
Remove a File from Entire History
The most common use case: a secret key, password, or large binary was committed and you need to scrub it from every commit in history.
Remove a file from all commits
# First, make a backup of your repo! cp -r my-repo my-repo-backup cd my-repo # Remove secrets.env from every commit git filter-repo --path secrets.env --invert-paths # Remove multiple files git filter-repo --path .env --path config/secrets.json --path private-key.pem --invert-paths
Terminal output
Parsed 1847 commits New history written in 3.21 seconds; now repacking/cleaning... Repacking your repo and cleaning out old unneeded objects HEAD is now at a9b8c7d Add authentication module Enumerating objects: 1502, done. ... git-filter-repo Statistics: Commits: 1847 (changed: 1847, removed: 0) Blobs: 3012 (changed: 0, removed: 1) ← 1 blob removed Tags: 12 (changed: 0, removed: 0)
Verify the file is gone from all history
# Should return nothing git log --all --full-history -- secrets.env # Force push to remote (WARNING: destructive) git push origin --force --all git push origin --force --tags
Remove Files Matching a Pattern
Remove files using glob patterns
# Remove all .env files anywhere in the repo git filter-repo --path-glob '**/.env' --invert-paths # Remove all files in a secrets/ directory git filter-repo --path-glob 'secrets/*' --invert-paths # Remove using a regex pattern git filter-repo --path-regex '.*.(pem|key|pfx)$' --invert-paths
Rename a Path in History
If you renamed a directory or file and want the old history to reflect the new structure:
Rename a directory in all history
# Rename "old/" to "new/" throughout all commits git filter-repo --path-rename old/:new/ # Rename a specific file git filter-repo --path-rename src/utils.js:src/helpers.js # Move everything into a subdirectory (useful before merging repos) git filter-repo --to-subdirectory-filter mypackage/
Fix Author Email Across History
Common after merging work done under a personal email that should have been a work email:
Replace email address in all commits
# Using --email-callback with Python git filter-repo --email-callback ' return email.replace(b"personal@gmail.com", b"work@company.com") '
Fix both name and email
git filter-repo --name-callback ' return name.replace(b"Old Name", b"Correct Name") ' --email-callback ' return email.replace(b"old@email.com", b"new@email.com") '
Complex remapping using a mailmap file
# Create a mailmap file cat > mailmap.txt << 'EOF' Correct Name <correct@email.com> Old Name <old@email.com> Correct Name <correct@email.com> <typo@emial.com> EOF git filter-repo --mailmap mailmap.txt
Extract a Subdirectory as a New Repository
When a monorepo package grows large enough to deserve its own repository, you can extract it with full history intact:
Extract packages/ui into a standalone repo
# Clone the monorepo first (don't work on the original!) git clone git@github.com:org/monorepo.git ui-library cd ui-library # Filter to only the packages/ui directory # and move its contents to the root git filter-repo --subdirectory-filter packages/ui # Now the repo root IS packages/ui, with all its history ls # Button.tsx Input.tsx index.ts package.json ... git log --oneline | head -5 # 8f7e6d5 Fix Button accessibility # 4c3b2a1 Add Input validation # ... # Create a new remote and push git remote add origin git@github.com:org/ui-library.git git push -u origin main
Keep Only Specific Paths
Keep only certain files (opposite of invert-paths)
# Keep ONLY the src/ directory and README.md git filter-repo --path src/ --path README.md # Keep only files matching a pattern git filter-repo --path-glob 'packages/api/**'
Strip Large Files from History
Remove all blobs larger than 10MB
# Remove any blob larger than 10 megabytes git filter-repo --strip-blobs-bigger-than 10M # Remove large files and show what was removed git filter-repo --strip-blobs-bigger-than 1M --dry-run
Analyzing History Before Filtering
Analyze repo to find large files
# Show the largest objects in history git filter-repo --analyze # Creates .git/filter-repo/analysis/ with reports: ls .git/filter-repo/analysis/ # blob-shas-and-paths.txt # directories-all-sizes.txt # extensions-all-sizes.txt # filenames-all-sizes.txt # Find the biggest files sort -k1 -rn .git/filter-repo/analysis/blob-shas-and-paths.txt | head -10
Comparison: git filter-repo vs git filter-branch
Feature | git filter-repo | git filter-branch |
|---|---|---|
Speed | 10–100x faster | Very slow (shell subprocess per commit) |
Status | Actively maintained, recommended | Deprecated since Git 2.36 |
Safety | Refuses to run on repos with remotes by default | No such protection |
API style | Python callbacks | Shell scripts |
Dry run support | Yes (--dry-run) | No |
Analysis tools | Yes (--analyze) | No |
Error handling | Clear, actionable messages | Cryptic shell errors |
Handles binary files | Yes | Problematic |
Installed with Git | No (pip install) | Yes (but deprecated) |
After Rewriting: Force Push Protocol
Complete post-filter-repo checklist
# 1. Verify the local result looks correct git log --oneline -10 git show HEAD # 2. Check that the sensitive file is gone git log --all --full-history -- secrets.env # should be empty # 3. Run garbage collection to expire old objects git reflog expire --expire=now --all git gc --prune=now --aggressive # 4. Force push ALL branches and ALL tags git push origin --force --all git push origin --force --tags # 5. Tell every collaborator: echo "IMPORTANT: Delete your local clone and re-clone from origin. Your local clone has stale commit hashes that no longer exist on remote." # 6. Rotate any secrets that were exposed # (a secret in git history is compromised — rotate it regardless)
Common Errors
"Refusing to destructively overwrite repo history since this does not look like a fresh clone" — git filter-repo detected a configured remote. Clone fresh first, or use
--force."No refs were changed" — the path you specified does not exist in history. Check spelling with
git log --all -- path/to/file.Collaborators see merge conflicts after force push — they must delete and re-clone, not just pull.
git filter-repo: command not found— not installed. Runpip install git-filter-repo.Tags still reference old commits — always include
--force --tagswhen force-pushing after filter-repo.