git-scrub
CLI tool to scan GitHub organizations and user profiles for a committed email address, then rewrite it out of history across every affected repo.
Why this exists
git filter-repo handles the local rewriting step well, but provides no tooling to:
- Discover which repos across a GitHub org or user profile contain a given email
- Clone, rewrite, and force-push each one automatically
- Verify the result via the GitHub API after each push
- Report a clean summary across dozens or hundreds of repos
git-scrub automates the entire workflow. Point it at an org or a username, give it the old and new email, and it handles the rest.
Common use case: you've left a job or graduated, and you want to scrub your old institutional email (you@university.edu) from every repo you've ever touched — replacing it with your personal GitHub no-reply address.
How it works
1. Discover → list every repo in the target org(s) / user profile(s)
2. Scan → check each repo's commit history for the old email
(both author and committer fields)
3. Report → show a table of dirty repos and commit counts
4. Rewrite → for each dirty repo: clone → git filter-repo → force-push
5. Verify → confirm via the GitHub API that the email is gone
6. Clean up → temp clone is deleted after each repo
Steps 4–6 only run with --fix. Everything up to step 3 is always safe.
Prerequisites
- Python 3.9+
git-filter-repobrew install git-filter-repo # or pip install git-filter-repo
- GitHub CLI (
gh) authenticated or aGITHUB_TOKENenv var withreposcope
Installation
pipx install git-scrub # recommended
# or
pip install git-scrub
Usage
Scan (dry-run, always safe)
# Scan a whole org
git-scrub --old-email old@work.edu --org my-org
# Scan all repos under a GitHub username (public + private)
git-scrub --old-email old@work.edu --user myusername
# Multiple targets at once
git-scrub --old-email old@work.edu --org org1 --org org2 --user myusername
# Target a single specific repo
git-scrub --old-email old@work.edu --repo owner/repo-name
# Scan a non-default branch
git-scrub --old-email old@work.edu --org my-org --branch develop
Rewrite and push (--fix)
git-scrub \
--old-email old@work.edu \
--new-email 123456+myusername@users.noreply.github.com \
--org my-org \
--fix
# Skip per-repo confirmation prompts
git-scrub ... --fix --yes
JSON output (for scripting / CI)
git-scrub --old-email old@work.edu --org my-org --json
{
"scanned": 42,
"dirty": [
{ "repo": "my-org/some-repo", "commits": 7 }
]
}
Token
# Via env var (useful in CI)
GITHUB_TOKEN=ghp_... git-scrub ...
# Via flag
git-scrub --token ghp_... ...
If neither is provided, git-scrub calls gh auth token automatically.
Options
| Flag | Description |
|---|---|
--old-email |
Email to find (required) |
--new-email |
Replacement email (required with --fix) |
--org |
GitHub org to scan. Repeatable. |
--user |
GitHub username to scan all their repos. Repeatable. |
--repo |
Specific owner/repo to target. Repeatable. |
--branch |
Branch to scan (default: each repo's default branch) |
--fix |
Rewrite history and force-push |
--dry-run |
Explicit scan-only mode (already the default) |
--yes / -y |
Skip per-repo confirmation prompts |
--json |
Output results as JSON |
--token |
GitHub token (or set GITHUB_TOKEN) |
-V / --version |
Print version and exit |
Notes
- Dry-run is the default. Nothing is written without
--fix. - Both
authorandcommitteremail fields are checked and rewritten. - The scan checks each repo's default branch (or
--branch).git filter-reporewrites all branches and tags when fixing, so the full history is cleaned regardless. - Force-pushing rewrites history for all collaborators on a repo — coordinate with your team before running on shared repos.
- GitHub's commit attribution UI may take a short time to update after a push.
License
MIT
Metadata
Release files for git-scrub 0.1.1
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_scrub-0.1.1.tar.gz | 9.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| git_scrub-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.6 kB
Release files / git_scrub-0.1.1.tar.gz
| Download URL | git_scrub-0.1.1.tar.gz |
|---|---|
| Size | 9.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
92645bd10a5ff99338689bb6fcbc0da0b4d3555ce80e5dbc51ca5274deabf99b
|
|
BLAKE2b-256 checksum How to use checksums |
791944ccafedc582ef6777ca14373a7a7b11b3e86c59ab57c9248f836b263b55
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 28, 2026.
Transparency logRelease files / git_scrub-0.1.1-py3-none-any.whl
| Download URL | git_scrub-0.1.1-py3-none-any.whl |
|---|---|
| Size | 10.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e24a408f14e9e03a794394bacbf4a355671381a97dedbdd63d3babec003bec31
|
|
BLAKE2b-256 checksum How to use checksums |
0d965f5dbd99dc6eeaec5158802f881734cd07cf098de60fe8349de0165ae920
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 28, 2026.
Transparency log