Bulk-rewrite any part of git commit history: messages, author names, emails, and more.
Tools like git-filter-repo and git filter-branch are incredibly powerful — they give you full control over every commit object in your history. git-rewrite draws inspiration from both and builds on top of them to make common rewrite tasks safer and more approachable:
- preview first — see exactly which commits match before changing anything
- dry-run — validate the full command without rewriting a single commit
- safe pattern embedding — patterns are repr-encoded so backslashes and quotes can't break the generated code
- callback validation — custom scripts are syntax-checked and inspected for
process_commitbefore history is touched - escape hatch — when regex isn't enough, drop down to a plain Python file and get full access to every commit field
Under the hood it uses git-filter-repo when available, falling back to git filter-branch.
pip install -e /path/to/git-rewriteThis installs the git-rewrite console script.
Tab-completion for subcommands, --field choices, and --refs (populated from git branch -a) via argcomplete:
pip install 'git-rewrite[completions]'
# bash (add to ~/.bashrc)
eval "$(register-python-argcomplete git-rewrite)"
# zsh (add to ~/.zshrc)
autoload -U bashcompinit && bashcompinit
eval "$(register-python-argcomplete git-rewrite)"
# fish (add to ~/.config/fish/config.fish)
register-python-argcomplete --shell fish git-rewrite | sourcegit-rewrite <command> [options]
git-rewrite preview "Co-Authored-By: Claude"
git-rewrite preview "Co-Authored-By: Claude" --limit 50
git-rewrite preview "Co-Authored-By: Claude" --refs main
# Machine-readable NDJSON output (one object per match, pipe to jq)
git-rewrite preview "Co-Authored-By: Claude" --format json
git-rewrite preview "Co-Authored-By: Claude" --format json | jq '.sha'# Diff-style dry-run: see exactly what would change before rewriting
git-rewrite strip "Co-Authored-By: Claude.*<noreply@anthropic\.com>" --preview
# Dry run first
git-rewrite strip --dry-run "Co-Authored-By: Claude.*<noreply@anthropic\.com>"
# Remove the lines
git-rewrite strip "Co-Authored-By: Claude.*<noreply@anthropic\.com>"
# Target a different field (requires git-filter-repo)
git-rewrite strip --field author-email "old@example\.com"
# Keep only conventional-trailer lines, strip the rest of the body
git-rewrite strip --invert "^[A-Z][a-z-]+: " --field messageNote: Using
stripon a date field (--field author-dateor--field committer-date) zeroes the field to an empty byte string, producing an invalid date. Usereplaceinstead to rewrite specific parts of the date value while keeping it valid.
# Diff-style dry-run: see before/after before rewriting
git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI" --preview
git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI"
git-rewrite replace --field author-name "Old Name" "New Name"
# Normalize timezone offsets to UTC (requires git-filter-repo)
# Date values are in raw format: "<unix-timestamp> <tz-offset>", e.g. "1700000000 -0700"
git-rewrite replace --field author-date "[-+]\d{4}$" "+0000"
git-rewrite replace --field committer-date "[-+]\d{4}$" "+0000"git-rewrite run my_callback.py --dry-run
git-rewrite run my_callback.py --refs main feature/branch# Run the preset as-is
git-rewrite preset strip-ai
# Dry-run a preset
git-rewrite preset strip-ai --dry-run
# Override a flag from the CLI (CLI always wins over preset)
git-rewrite preset strip-ai --refs main --yes| Flag | Description |
|---|---|
--dry-run |
Show what would happen without modifying history |
--yes / -y |
Skip the confirmation prompt |
--refs REF … |
Limit to specific refs (default: all) |
--field FIELD |
Field to target: message, author-name, author-email, committer-name, committer-email, author-date, committer-date (date fields require git-filter-repo) |
--case-sensitive |
Disable case-insensitive matching |
--preview |
(strip/replace) Diff-style preview of changes — no history rewritten |
--invert |
(strip) Keep only matches; strip everything else |
--format FORMAT |
(preview) text (default) or json (NDJSON, one line per match) |
--no-color |
Disable colored output (also honored via NO_COLOR env var) |
These flags narrow which commits are previewed and counted. DATE accepts any format git log understands (2024-01-01, 6 months ago, yesterday, etc.).
Note: Scoping flags filter which commits are shown/counted, not which commits the rewrite callback runs against. The actual rewrite still processes every commit in the given refs.
| Flag | Description |
|---|---|
--since DATE |
Only consider commits more recent than DATE |
--until DATE |
Only consider commits older than DATE |
--author PATTERN |
Only consider commits whose author name/email matches PATTERN (regex) |
# Preview only Claude co-authorship lines from the last 6 months
git-rewrite preview "Co-Authored-By: Claude" --since "6 months ago"
# Count matches by a specific contributor
git-rewrite strip --dry-run "Co-Authored-By: Claude" --author "alice@example.com"
# Scope by both date range and author
git-rewrite replace "OldOrg" "NewOrg" --since 2024-01-01 --until 2025-01-01 --author "dev@oldorg.com"Create a .py file defining process_commit:
import re
def process_commit(commit):
"""Receives a git-filter-repo commit object. Modify in place."""
commit.message = re.sub(rb"Co-Authored-By: Claude.*\n?", b"", commit.message)
# Also available:
# commit.author_name, commit.author_email
# commit.committer_name, commit.committer_email
# commit.author_date, commit.committer_dateThe tool validates syntax and the presence of process_commit before touching history.
Store default options and named presets in a per-repository config file so teams can run the same cleanup repeatedly without repeating flags.
.git-rewrite.tomlin the repo root[tool.git-rewrite]section inpyproject.toml
# .git-rewrite.toml
# Top-level defaults — applied to every command unless overridden on the CLI
default_refs = ["main", "develop"]
case_sensitive = false
# Named presets — run with: git-rewrite preset <name>
[presets.strip-ai]
command = "strip"
pattern = "Co-Authored-By:.*Claude.*"
field = "message"
[presets.fix-email]
command = "replace"
pattern = "old@example\\.com"
replacement = "new@example.com"
field = "author-email"The same schema works inside pyproject.toml under [tool.git-rewrite]:
[tool.git-rewrite]
default_refs = ["main"]
[tool.git-rewrite.presets.strip-ai]
command = "strip"
pattern = "Co-Authored-By:.*Claude.*"CLI flag → preset value → top-level config default → built-in default
Note on
case_sensitive:--case-sensitiveis a boolean flag with no inverse (--case-insensitive). If you setcase_sensitive = truein the config and need to override it tofalsefor a single run, edit the config temporarily or omit the key.
A malformed TOML file or an unknown preset name exits immediately with a clear error message listing what went wrong and (for unknown presets) the available preset names.
Force-push the affected refs:
git push --force-with-lease --all- Python 3.10+
- git
- git-filter-repo (recommended;
pip install git-filter-repoorbrew install git-filter-repo)- Required for
--dry-runand non-message fields - Falls back to
git filter-branchfor message-only rewrites
- Required for