Git Reattribute
A small, cross-platform CLI for replacing one Git identity with another
across a repository's history, safely — without requiring you to hand-write
a git-filter-repo invocation. Also includes guard,
a prevention companion that blocks a denied identity in CI or a local hook
before it ever lands, so you don't need to run the remediation flow again.
⚠️ This tool rewrites Git history. Rewriting history changes commit SHAs, invalidates existing signatures on rewritten commits, and requires a force push to update any already-published branch. A force push can disrupt collaborators and invalidate their existing clones. Only use this on repositories and history you are authorized to modify. This tool does not modify GitHub (or any other host's) account data or contributor database directly — hosts may independently recompute contributor attribution from the resulting commit metadata.
Why?
A Git "identity" isn't always a person — it can be a placeholder, a bot
account, a shared machine account, or an AI agent that paired on the work.
Correcting a mis-attributed one usually means hand-writing a
git-filter-repo invocation. Git Reattribute turns that into one guided
command.
Features
- Interactive branch, contributor, and replacement-identity selection.
- Non-interactive / scriptable usage via flags.
- Replaces author, committer, or both (default: both).
- Removes matching
Co-authored-by:trailers for the replaced identity. - Dry-run mode — preview the impact with no changes made.
- Creates a backup ref before every rewrite.
- Mandatory post-rewrite verification.
--force-with-leasepush only, never--force, never silent.- Warns about signed commits and shallow repositories before rewriting.
guardsubcommand: prevent denied identities from ever being committed or merged, via a GitHub Action and/or a localpre-commithook — seeguard.
Requirements
- Python 3.10+
- Git 2.x
git-filter-repo— installed automatically as a dependency when youpip install git-reattribute; you don't need to install it separately.
Installation
python -m pip install git-reattribute
Quick Start
cd your-repo
git-reattribute --version
git-reattribute --help
git-reattribute
Example
$ git-reattribute
Git Reattribute
Repository: /home/alice/projects/example
Remote: origin
Branch: main
Contributors on main:
1. Claude <claude@example.com> 47 commits
2. Alice <alice@example.com> 18 commits
3. Bob <bob@example.com> 6 commits
Select contributor to replace: Claude <claude@example.com> 47 commits
Replace with:
Current Git identity
Alice <alice@example.com>
Bob <bob@example.com>
Enter a custom identity
Select replacement: Alice <alice@example.com>
Summary
-------
Branch: main
From: Claude <claude@example.com>
To: Alice <alice@example.com>
Author commits affected: 47
Committer commits affected: 47
Co-authored-by trailers to remove: 3
WARNING: This operation rewrites Git history.
Type REWRITE to continue: REWRITE
History rewrite completed.
Recovery reference:
refs/backup/git-reattribute/2026-08-22T10-30-00
To restore the original branch:
git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00
Keep this reference until you have verified the rewritten history.
Verification
Old identity commits before: 94
Remaining old identity commits: 0
New identity commits: 94
Remaining Co-authored-by trailers: 0
Verification: PASS
Push rewritten branch to origin with --force-with-lease? [y/N]
Interactive Usage
Running git-reattribute with no flags walks you through: select a branch,
see its contributors, pick who to replace, pick the replacement, preview the
change, then type REWRITE to confirm.
Non-Interactive Usage
git-reattribute \
--branch main \
--from-email claude@example.com \
--to-name Alice \
--to-email alice@example.com \
--identity-type both \
--yes --push
--yes does not imply --push — pass both explicitly when you want a
scripted rewrite-and-push.
Dry Run
git-reattribute --branch main --from-email claude@example.com --to-current-user --dry-run
Shows affected author/committer commit counts and any Co-authored-by:
trailers that would be removed. No history or remote is touched.
Branches
Local branches are listed and selectable. Remote-tracking branches are not rewritten directly — check out a local branch first. The rewrite scope is the entire reachable history of the selected branch — every commit an ancestor of that branch's tip, not just commits made while that branch was checked out.
Authors vs Committers
Every commit has an author and a committer, and they can differ. Use
--identity-type author, --identity-type committer, or the default
--identity-type both to control which field(s) get rewritten. The
interactive UI shows both roles whenever they differ for a given contributor.
Co-authored-by Trailers
When an identity (for example an AI agent that paired with the human author)
appears as a Co-authored-by: trailer in a commit message, that trailer is
removed (not replaced) for the identity being replaced, by default. This
matters because rewriting only the author field of a commit like:
Author: Claude <claude@example.com>
Co-authored-by: Claude <claude@example.com>
would otherwise leave the old identity behind in the message body even after the commit object's author is fixed.
This runs alongside the author/committer rewrite for the same source
identity — there is no separate replacement target for trailers, and the
trailer is deleted, not rewritten to a new co-author. Disable it with
--no-strip-coauthor-trailers.
Matching rule: a trailer line matches when it starts with
Co-authored-by: (case-insensitive) followed by the source identity's name
and <email>, with any amount of whitespace tolerated around the colon,
name, and angle brackets — but the name and email themselves must match the
source identity's recorded name/email (case-insensitively), not a partial or
fuzzy match. Only matching trailer lines are removed; other trailers (e.g.
Signed-off-by:) and body text that merely mentions the same name are left
untouched.
History Rewriting Warning
This tool rewrites Git history using git-filter-repo.
Two different --force flags matter here, and they are not the same thing:
git-filter-repo --force(used internally, always) only permits the local rewrite to run on a non-fresh-clone repository.git-filter-reponormally refuses to touch anything but a fresh clone as a generic safety net; this tool creates its own backup ref (see Recovery below) before that rewrite ever runs, so that backup is the safety net instead, and the--forceflag just lets the local rewrite proceed.git push --forceis never used by this tool, anywhere. Publishing a rewritten branch always usesgit push --force-with-lease(see Push Behavior), and only after you explicitly confirm it.
Contributor discovery uses Git's raw %an/%ae/%cn/%ce fields, not
mailmap-resolved fields — a repository's .mailmap is intentionally ignored
so what you see and rewrite always matches the actual commit-object bytes.
Signed commits lose their signatures when rewritten — a warning is shown
before you confirm. Merge commits are rewritten natively by git-filter-repo
as part of a full-branch rewrite; this requires no special handling in v1
since there is no commit-range filtering yet.
Push Behavior
Pushing always uses git push --force-with-lease, never git push --force.
Nothing is pushed unless you pass --push (scripted) or confirm the
interactive prompt. If the remote branch changed since the rewrite began,
the push is rejected and nothing is retried automatically.
Recovery
Before every rewrite — and before git-filter-repo is ever invoked — a
backup ref is created pointing at the original branch tip:
refs/backup/git-reattribute/<timestamp>
The rewrite is scoped (via --refs <branch>) to only the selected branch,
so the backup ref itself is never touched or rewritten by the same
operation. It is never deleted automatically. After a rewrite, the tool
prints the exact recovery command:
Recovery reference:
refs/backup/git-reattribute/2026-08-22T10-30-00
To restore the original branch:
git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00
Keep the backup ref until you've verified the rewritten history (and pushed,
if applicable) — then it's safe to delete with
git update-ref -d refs/backup/git-reattribute/<timestamp>.
Limitations
- Shallow clones may show an incomplete contributor list and rewrite scope;
run
git fetch --unshallowfirst. - Submodules, Git LFS objects, and annotated tag signatures are not specially handled in v1.
- Branch-protection or remote-policy push rejections are surfaced as-is; this tool never attempts to bypass them.
- One source identity → one target identity per run.
guard: prevent denied identities before they land
Everything above is remediation — fixing history that already has a bad
identity in it. git-reattribute guard is the prevention half: it never
touches history, it only scans and reports (exit 0 clean, exit 1 on a
match), so it's safe to run unattended in a hook or CI job.
Create .git-reattribute-guard.yml at your repo root:
deny:
- name: Claude
email: claude@example.com
- email: "*@bots.example.com" # glob supported on email only
check_coauthors: true # also check Co-authored-by trailers (default: true)
CI (the actual enforcement point)
name: Guard commit identities
on: [pull_request]
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: drk1rd/git-reattribute/.github/actions/guard@v1
This runs git-reattribute guard check against every commit in the PR's
range, server-side — regardless of what a contributor did locally. This is
the layer that actually matters for keeping history clean.
Local hook (fast feedback, skippable)
git-reattribute guard check-local checks the identity Git is about to use
right now — your user.name/user.email, plus any Co-authored-by:
trailer in the message you're writing. Wire it up via the pre-commit
framework's local hook type (no separate hook-repo needed — it just runs
the already-installed git-reattribute command):
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: git-reattribute-guard
name: git-reattribute-guard
entry: git-reattribute guard check-local
language: system
stages: [commit-msg]
pre-commit install --hook-type commit-msg
This is a convenience for the person committing — it's skippable with git commit --no-verify, so it's not a substitute for the CI check above.
Direct CLI use
git-reattribute guard check --base origin/main --head HEAD
git-reattribute guard: found 1 violation(s) in origin/main..HEAD:
a1b2c3d4 author: Claude <claude@example.com>
Fix with:
git-reattribute --branch main --from-email claude@example.com --to-current-user
Every violation names the exact git-reattribute command to fix it — the
two halves of this tool are meant to be used together: guard stops it from
happening again, git-reattribute cleans up what's already there.
Development
python -m pip install -e ".[dev]"
Testing
pytest
Tests build temporary Git repositories per-test; nothing is ever run against a real/shared repository.
Release Process
See CHANGELOG.md. Versioning follows SemVer (MAJOR.MINOR.PATCH).
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_reattribute-0.2.0.tar.gz.
File metadata
- Download URL: git_reattribute-0.2.0.tar.gz
- Upload date:
- Size: 26.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54eea05de890d937f9fcdcbf217318ad37f345e5acd7d1990f080a8f74445a14
|
|
| MD5 |
beae364f1539207a158e82216281ad52
|
|
| BLAKE2b-256 |
03f7f1dba070bc0b9b265acd07ac2c6da4c13176e7f61337b6c8267b9c69cdb5
|
Provenance
The following attestation bundles were made for git_reattribute-0.2.0.tar.gz:
Publisher:
publish.yml on drk1rd/git-reattribute
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_reattribute-0.2.0.tar.gz -
Subject digest:
54eea05de890d937f9fcdcbf217318ad37f345e5acd7d1990f080a8f74445a14 - Sigstore transparency entry: 2564348635
- Sigstore integration time:
-
Permalink:
drk1rd/git-reattribute@b7368669a5bdeb724603c0463b8e7729d7930348 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/drk1rd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7368669a5bdeb724603c0463b8e7729d7930348 -
Trigger Event:
release
-
Statement type:
File details
Details for the file git_reattribute-0.2.0-py3-none-any.whl.
File metadata
- Download URL: git_reattribute-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db716c55b47ec33983c5d81a4d72b978de3e855023dfb584fdf1985fb4ce4f72
|
|
| MD5 |
f7875b6a28333c476f7d133a55059ea9
|
|
| BLAKE2b-256 |
c30b53b8d468acf99d3ba32a5a54fb3fa9859a00f8aea0768b30c1af03c95c2f
|
Provenance
The following attestation bundles were made for git_reattribute-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on drk1rd/git-reattribute
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_reattribute-0.2.0-py3-none-any.whl -
Subject digest:
db716c55b47ec33983c5d81a4d72b978de3e855023dfb584fdf1985fb4ce4f72 - Sigstore transparency entry: 2564349311
- Sigstore integration time:
-
Permalink:
drk1rd/git-reattribute@b7368669a5bdeb724603c0463b8e7729d7930348 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/drk1rd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7368669a5bdeb724603c0463b8e7729d7930348 -
Trigger Event:
release
-
Statement type: