Alter History with filter-branch

Learn how to rewrite Git history with filter-branch: remove sensitive files, change author info, and more. Step-by-step guide with warnings and safer alternatives.

Focus: alter history with git filter-branch

Sponsored

You finally pushed that commit with the wrong email, a stray debug.log full of secrets, or an accidentally committed node_modules folder — and now it's part of your project's permanent record. The history is wrong, and you need to fix it without rewriting every commit manually. Enter git filter-branch: the Swiss Army knife for rewriting history, letting you scrub files, fix author info, or remove large blobs across your entire repo in one decisive pass. But with great power comes great responsibility — and some serious footguns. Let's master filter-branch so you can clean up your history with confidence, not fear.

The problem this lesson solves

Git's power lies in its immutable history — every commit is a snapshot that depends on the one before it. That immutability is what makes collaboration safe, but it becomes a burden when you need to correct a mistake that's already in the log. Maybe a developer accidentally committed their .env file with production API keys, or a colleague left a 2 GB video file in an initial commit that's now bloating every clone. Manually picking through hundreds of commits is error-prone and tedious, and simply adding a new commit to delete the file leaves the sensitive data in history — anyone with clone access can still find it.

The problem: history is a record, but sometimes you need to rewrite that record. git filter-branch exists precisely for this — it lets you traverse your entire commit history and apply a filter (a shell command or script) that modifies each commit's contents, metadata, or messages. Once you understand it, you can surgically remove files, change emails, or even split a repository into parts — and finally sleep easy knowing the sensitive info is gone.

Core concept / mental model

Think of filter-branch as a time-traveling find-and-replace for your commits. It takes every commit in your history, applies your filter (a command that runs on each commit's snapshot), and then re-parents and re-writes the commits to form a new history. The result is a completely rewritten history with new commit hashes — because any change to a commit's content or metadata cascades to all descendants.

Here's the core principle:

  • You provide one or more filters (e.g., --tree-filter to change the working tree, --env-filter to change author/committer info).
  • filter-branch checks out each commit, runs your filter, stages changes, and creates a new commit.
  • The result is a new branch (by default original is a backup of your old history).

Pro tip: Always think of filter-branch as a history rewrite — the old commits are not destroyed, but they become orphaned and only reachable via the original ref. You must force-push the rewritten branch to your remote and ask all collaborators to re-clone from the new history.

The mental model in action

Imagine your repository's history as a chain of linked lists. Each commit is a node pointing to its parent. filter-branch creates a new chain where each node is a transformed copy of the original. The old chain still exists, but you can choose to abandon it once you're sure the new one is correct.

How it works step by step

The general syntax of git filter-branch is:

git filter-branch [--index-filter | --tree-filter | --env-filter] [filters] -- [--all | <branch>...]

Here's the high-level process:

  1. Backup your original historyfilter-branch automatically saves the original refs under refs/original/, but you should create your own backup branch first.
  2. Choose your filter type — each filter modifies different aspects of each commit: - --tree-filter — runs a shell command on the working directory at each commit; you modify files as if you were editing them. - --index-filter — runs a command against the staging area only (faster, no checkout). - --env-filter — modifies environment variables (author, committer, dates) via a script. - --msg-filter — modifies the commit message. - --parent-filter — rewrites the parent relationships.
  3. Run the commandfilter-branch iterates through all commits reachable from the branches you specify.
  4. Verify the result — inspect the new history with git log, check for leftover secrets, and confirm the tree looks correct.
  5. Force-push and clean up — after pushing the rewritten branch, remove the backup refs and run garbage collection to truly purge old objects.

Hands-on walkthrough

Let's get practical. We'll cover the three most common tasks: removing a file from all commits, changing author email, and stripping large blobs.

Example 1: Remove a sensitive file from all history with --index-filter

The first task is removing a file like secrets.txt from every commit. We use --index-filter because it's faster — it doesn't check out each revision.

# Setup: create a repo with a file we regret committing
git init secret-repo && cd secret-repo
echo "super-secret-key" > secrets.txt
git add secrets.txt && git commit -m "add secrets"
echo "normal code" > app.py && git add app.py && git commit -m "add app"

# Now remove secrets.txt from the entire history
git filter-branch --index-filter 'git rm --cached --ignore-unmatch secrets.txt' -- --all

Expected output:

Rewrite 1234567 (1/2) (0 seconds passed, remaining 0 predicted)
Rewrite 89abcde (2/2) (0 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten

Now check the log — the secrets.txt file is gone from every commit:

git log --oneline --all

Example 2: Fix author name and email throughout history

If a developer's email was wrong in several commits, use --env-filter. The script below changes the author and committer email for a specific old email address.

git filter-branch --env-filter '
if [ "$GIT_AUTHOR_EMAIL" = "old@example.com" ]; then
    export GIT_AUTHOR_EMAIL="new@example.com"
    export GIT_AUTHOR_NAME="New Name"
fi
if [ "$GIT_COMMITTER_EMAIL" = "old@example.com" ]; then
    export GIT_COMMITTER_EMAIL="new@example.com"
    export GIT_COMMITTER_NAME="New Name"
fi
' -- --all

Note: This rewrites all commits where the filter matches, updating both author and committer info.

Example 3: Remove a large file (e.g., a binary blob) with --tree-filter

If you've committed a huge file like data.bin, you can remove it with a simple command:

git filter-branch --tree-filter 'rm -f data.bin' -- --all

--tree-filter checks out each commit, runs rm -f data.bin, and then re-commits. It's slower than --index-filter because it does a checkout each time, but it works for any file-based change.

Compare options / when to choose what

filter-branch is powerful but not the only game in town. Let's compare it with modern alternatives:

Tool / Command Best For Speed Complexity Safety
git filter-branch One-off scripts, traditional workflows Slow (checks out commits) Medium Risky — easy to misuse, no built-in safety
git filter-repo Most history rewrites (recommended by Git) Fast (C-based, optimized) Low — simpler syntax Safer — gives detailed reports and recovery options
BFG Repo-Cleaner Removing large files and secrets quickly Extremely fast Very low (simplistic commands) Safe — designed for specific tasks

When to use filter-branch:

  • When you're in a restricted environment where you can't install third-party tools.
  • When you need a built-in Git command that works everywhere.
  • For learning the mechanics of history rewriting — it's educational.

When to prefer git filter-repo:

  • For any serious production rewrite — the Git community officially recommends it.
  • When you need complex filters (like changing user info) with better performance and safety.

Pro tip: If you're starting fresh on a rewrite, use git filter-repo if you can. But filter-branch is still worth knowing because it's always available in Git,

Troubleshooting & edge cases

"Cannot rewrite branches: You have unstaged changes"

This error means your working tree isn't clean. Commit or stash your changes first:

git stash
git filter-branch --index-filter '...' -- --all
git stash pop

"some refs were not updated" warning

This happens when some refs (like tags) didn't get rewritten because they don't match the branch pattern. Add --tag-name-filter cat to force tags to be rewritten:

git filter-branch --index-filter '...' --tag-name-filter cat -- --all

Merge commits become lost or rewritten incorrectly

filter-branch handles merges, but the --parent-filter is needed if you're changing parent relationships. For most cases, removing files via --index-filter works fine even with merges.

History still contains the old file after rewrite

If you only rewrote a branch (not --all), some commits may still refer to the old content. Always use -- --all to cover all refs.

The rewrite seems to hang

filter-branch can be very slow on large repos. To see progress, add --verbose or use env GIT_FILTER_BRANCH_SQUELCH_WARNING=1 to suppress warnings (not recommended). Better: use git filter-repo for large repos.

Accidentally broke something — how to revert

You can restore from your backup refs:

git reset --hard refs/original/refs/heads/master

Always create a backup branch before rewriting:

git branch backup-before-rewrite
git filter-branch ...
# If something goes wrong:
git checkout backup-before-rewrite

What you learned & what's next

You now understand how to alter history with git filter-branch: the core concept of rewriting commits with filters, how to remove files, fix author info, and clean up blobs. You know the main filters (--tree-filter, --index-filter, --env-filter), how to force-push the rewritten history, and how to recover if things go awry. You also learned that filter-branch is not the only tool — git filter-repo and BFG are modern alternatives that are often safer and faster.

Now that you can rewrite history, the next step is to master interactive rebase to reorder, squash, and edit commits — a more granular way to shape your commit history before you share it. Practice filter-branch on a throwaway repo to build confidence, then move on to the next lesson in the Git Tutorial track.

Practice recap

Create a throwaway repo, commit a file with a secret and a normal file, then use git filter-branch --index-filter to remove the secret entirely from all commits. Check with git log --all --name-only that it's gone, then run git reflog to see the original refs. For an extra challenge, change the author email of your last three commits using --env-filter.

Common mistakes

  • Forgetting to backup your original branch before running filter-branch — if something breaks, you're stuck.
  • Using --tree-filter when --index-filter would be faster — --tree-filter checks out every commit, making it much slower on large repos.
  • Not passing -- --all, so only the current branch gets rewritten, leaving hidden refs with the old history.
  • Rewriting history that has already been pushed and shared — collaborators will hit confusing merge conflicts unless they all re-clone.
  • Assuming filter-branch cleans up old objects — you must remove refs/original and run git gc --prune=now to actually purge the old commits.

Variations

  1. Use git filter-repo instead — faster, safer, and the officially recommended alternative to filter-branch.
  2. Use BFG Repo-Cleaner for quickly stripping out large files or passwords.
  3. For rewriting just the author of a single commit, consider git commit --amend --author='New Author ' instead of a full history rewrite.

Real-world use cases

  • Remove a file containing API keys that was accidentally committed to a public repository and then merge the fixed history into the main branch.
  • Rewrite all commits made by a former employee's email to a new address after a company merge, ensuring accurate attribution in the log.
  • Strip out a 500 MB fixture folder committed early in a project's life to shrink repository size and speed up cloning for new developers.

Key takeaways

  • filter-branch rewrites history by applying a filter to every commit, producing new hashes and an immutable original backup.
  • Choose --tree-filter for file edits, --index-filter for speed (no checkout), and --env-filter for metadata changes.
  • Always back up your original branch and use -- --all to include every ref in the rewrite.
  • After rewriting, force-push to the remote (if shared) and coordinate with collaborators to re-clone.
  • Modern alternatives like git filter-repo and BFG are faster and easier to use — prefer them in new projects.
  • Verify the rewritten history before pushing: check for leftover secrets with grep and test the tree with git log.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.