git-rewrite
Bulk-rewrite any part of git commit history: messages, author names, emails, and more.
Why
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.
Installation
pip install -e /path/to/git-rewrite
This installs the git-rewrite console script.
Shell completions
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 | source
Usage
git-rewrite <command> [options]
Commands
preview — find matching commits (read-only)
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'
strip — remove matching lines from commit messages
# 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 message
Note: 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.
replace — substitute a pattern with a replacement
# 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"
run — execute a custom Python callback
git-rewrite run my_callback.py --dry-run
git-rewrite run my_callback.py --refs main feature/branch
preset — run a named preset from the repo config
# 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
Common flags
| 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) |
Scoping flags (strip, replace, preview)
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"
Custom callbacks (run)
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_date
The tool validates syntax and the presence of process_commit before touching history.
Config file
Store default options and named presets in a per-repository config file so teams can run the same cleanup repeatedly without repeating flags.
Config locations (checked in order)
.git-rewrite.tomlin the repo root[tool.git-rewrite]section inpyproject.toml
Schema
# .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.*"
Precedence
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.
Config parse errors
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.
After rewriting
Force-push the affected refs:
git push --force-with-lease --all
Requirements
- 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
Release files for git-rewrite-history 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| git_rewrite_history-0.1.3.tar.gz | 29.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| git_rewrite_history-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 48.0 kB
Release files / git_rewrite_history-0.1.3.tar.gz
| Download URL | git_rewrite_history-0.1.3.tar.gz |
|---|---|
| Size | 29.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ab6edd28599a603c8800b5ec8734a5425ce4d7d27696753664f98f42188603cb
|
|
BLAKE2b-256 checksum How to use checksums |
a685b4ed093287269282ec30c5108d6f3b2623c6ab4af3f154471d5c8935430e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency logRelease files / git_rewrite_history-0.1.3-py3-none-any.whl
| Download URL | git_rewrite_history-0.1.3-py3-none-any.whl |
|---|---|
| Size | 18.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2bf79cf9c631de07873734a4d6b1a1b214e6dfa877ceb560b1b9441cfbedc0ff
|
|
BLAKE2b-256 checksum How to use checksums |
1b4d2fedb00d56e39436f927679a3015f5551dc90dfeec6958c1820a632ab1bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency log