Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to rewrite Git history with filter-branch

git filter-branch rewrites every commit in your repository’s history according to filters you define, which is necessary when you need to remove a large file, scrub sensitive data, or correct commit author information across all commits. As the creator of CoreUI with 25 years of open-source development experience, I’ve used this tool to purge accidentally committed credentials and remove build artifacts that bloated repository size. Note that git filter-branch is the legacy approach — git filter-repo is faster and safer for most use cases — but understanding filter-branch is still valuable for environments where installing additional tools is restricted. Both tools permanently rewrite history and require all collaborators to re-clone or re-base after the operation.

Remove a specific file from every commit in history.

# Remove secrets.env from every commit
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch secrets.env' \
  --prune-empty --tag-name-filter cat -- --all

--index-filter runs a command against the index (staging area) of each commit without checking out the working tree, making it much faster than a tree filter. --ignore-unmatch prevents errors if the file doesn’t exist in some commits. --prune-empty removes commits that become empty after the file is deleted. --all rewrites all branches and tags.

Remove a Directory from History

Delete an entire folder and its contents from every commit.

git filter-branch --force --index-filter \
  'git rm -r --cached --ignore-unmatch dist/' \
  --prune-empty --tag-name-filter cat -- --all

This is useful for removing accidentally committed dist/, node_modules/, or build/ directories that inflated the repository size. After running this, the directory is gone from all historical commits.

Update Author Email Across All Commits

Fix incorrect author emails in the entire history.

git filter-branch --env-filter '
  OLD_EMAIL="[email protected]"
  NEW_NAME="Correct Name"
  NEW_EMAIL="[email protected]"

  if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_COMMITTER_NAME="$NEW_NAME"
    export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
  fi

  if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_AUTHOR_NAME="$NEW_NAME"
    export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
  fi
' --tag-name-filter cat -- --all

--env-filter sets environment variables for each commit. Git uses GIT_COMMITTER_EMAIL, GIT_AUTHOR_EMAIL, and their _NAME variants to identify commit authorship.

Clean Up After filter-branch

Remove old references and garbage collect to free space.

# Remove old refs created by filter-branch
git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin

# Remove local ref log
git reflog expire --expire=now --all

# Garbage collect and prune unreachable objects
git gc --prune=now --aggressive

# Force push all branches and tags
git push origin --force --all
git push origin --force --tags

After rewriting, the original commits still exist as “loose objects” until garbage collection runs. These steps remove them permanently. Force-pushing updates the remote — all collaborators must then git clone fresh or run git fetch --force and reset their branches.

Best Practice Note

For new projects, prefer git filter-repo over filter-branch — it’s 10-50x faster and has cleaner syntax. See how to rewrite Git history with git filter-repo for the modern approach. For removing sensitive data specifically, see how to remove sensitive data from Git history which covers both tools. Always work on a backup clone when rewriting history — this operation cannot be undone once pushed.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author