/repo-scrub
/repo-scrub repo and safety
Scan a GitHub repo's full history for secrets and large files, scrub what the user picks, rewrite history safely, then flip it private → public.
- invocation user only
- prereqs gh (authenticated), gitleaks, git-filter-repo,…
- tested lightly used
- suggested model · effort opus · high
Install
Install just this, from a clone of the repo. Anything it needs comes with it.
./scripts/install-skills.sh --skill repo-scrub # bash, Git Bash, macOS, Linux
.\scripts\install-skills.ps1 -Skill repo-scrub # Windows PowerShell
Works on its own.
What it does
Scans a GitHub repo's full git history for secrets and oversized files.
- Lets you choose exactly what to scrub.
- Rewrites history safely in a throwaway clone.
- Force-pushes.
- Only then flips the repo private → public.
- Every step with side effects has an explicit confirmation gate.
When to use
- Making a private repo public / open-sourcing it.
- Cleaning secrets or huge blobs out of history before publishing.
How to invoke
/repo-scrub [owner/repo]
Defaults to the current repo. Only you can run it (disable-model-invocation: true), because it rewrites history and force-pushes.
How it works
Prereqs and target — version-checks each tool (stops with the install command if one is missing), then
gh repo view … --json name,owner,isPrivate,defaultBranchRefand confirms you accept a history rewrite + force-push.<tmp>is onemktemp -dpath, echoed once.- Lists every other remote in your clone. Each holds its own copy of the history, so it is scanned, rewritten and pushed to on a yes.
- Lists every submodule. Each is a separate repo to scrub on its own; relative URLs are flagged.
Git LFS bail-out — stops with a warning if the root
.gitattributesshows LFS-tracked paths;git filter-repohas no LFS awareness.- The same check runs over all history in step 3 to catch nested files and other branches.
Scan (throwaway clone #1)
gitleaks git --log-opts="--all -m"(ordetect --sourcepre-8.19) over all history, merge commits included (a secret added only in a conflict resolution is invisible without-m).- Other remotes' branches fetched in,
git filter-repo --analyze. scripts/summarize_findings.pymerges them intofindings.json, written to a work dir outside the clone. Secrets get IDsS1…, deduped by rule and value (or by location when the value is empty). Only a fingerprint is stored; no raw value goes to disk. Large blobs get IDsL1…, sized by the bytes of every version of that path in history added up.- Personal strings handed over by
repo-showcase(or named by you) get agit log --all -m -S'<string>'and--grepcheck each, since gitleaks has no rule for names or emails; hits becomeP1…. - Emails in commit messages and author/committer identities other than yours are found the same way (
git log --all --format=…). - Signed commits and tags are counted: a rewrite drops every signature.
- The clone is then discarded.
Report — a table of every finding (no raw secret values, just fingerprints) and a summary line.
Select — you pick per ID or in bulk; Claude echoes back scrub vs keep lists.
Gates before any rewrite — both are hard gates.
- Rotate first (mandatory): rotate/revoke every selected secret at its source; a rewrite doesn't un-leak a secret that was ever pushed.
- Checklist by type (cloud keys, API tokens, private keys/certs, DB passwords, webhook URLs).
- Then collaborator / open-PR safety: lists collaborators, invitations, open PRs, fork count; drafts a re-clone notice.
Rewrite (fresh clone #2)
scripts/build_replace_text.pyre-derives each secret's value from the gitleaks report by fingerprint and writesexpressions.txtwithout ever echoing a value.- One
git filter-repocall.--replace-textand--path …--invert-pathsare each included only if that category was selected. - A selected
Pfinding adds aliteral:<string>==>REDACTEDrule (--replace-messagetoo for message hits). - A selected author identity goes through
--mailmapto the replacement you pick, and one you keep is recorded as "author identities stay". - For large files you choose: strip entirely, or strip history and re-add the current version.
Verify
- Re-run gitleaks (
-magain). git log --all -m -Sper scrubbed personal string plus a grep of every message and identity (must print nothing).- Spot-check sizes, before/after
git count-objects -vH. - Confirm gate.
- Offers to re-sign each previously signed tag with your own key; commits stay unsigned.
- Re-run gitleaks (
Force-push
- Re-adds
origin, checksgit branchlists every branch to keep (the fresh clone'sorigin/*became local branches, which is what--allpushes). - Then
push --force --alland--tags; each other remote gets its branches on its own yes. - If a hook blocks the push, it gives you the two commands to run yourself.
- Re-adds
Post-push — every local copy, yours included, still holds the old history.
- Re-clone (after copying out WIP, stashes and unpushed branches and tags), or reset it in place per
POST-PUSH.md. - The in-place reset, in order:
- Stash your WIP first.
- Save every stash entry (untracked files included) and every local-only or unpushed branch and tag as patches/bundle outside the repo.
git fetch && git reset --hard origin/<branch>per pushed branch.- Rebase the unpushed branches you choose onto the new history (an LFS branch only with git-lfs installed, with a recovery path when the rebase or its abort fails).
- Re-point the tags you choose to their rewritten commits (annotated message kept, old signature left out, re-signed on your yes).
- Re-create the stashes and check each against its saved patch.
- Delete a branch or tag only on your yes.
- Then expire the reflog and
gc; nevergit pull.
- Finally it warns about every branch or tag you kept as is, or didn't answer for. It still holds the old history, secret included; never push it.
- It offers to delete the save folder, showing its path and size; it deletes only on your yes, else tells you the folder still holds the old secret.
- Lists PRs that must be recreated.
- Notes that old commits can still be reachable on GitHub via
refs/pull/*/cached views until GitHub Support garbage-collects them.
- Re-clone (after copying out WIP, stashes and unpushed branches and tags), or reset it in place per
Visibility flip —
gh repo edit --visibility public …after a separate gate explaining the consequences; verifiesisPrivate.Cleanup — deletes the work dir (gitleaks report, findings.json, expressions.txt) and leftover clones.
- Bails out on Git LFS repos (steps 2-3).
- Scanning and rewriting never happen in your working clone.
- Step 10 does change it if you choose the in-place reset: it resets your pushed branches to the new history, and it deletes local branches and tags only when you say yes, after saving them.
Design principles
- Going public is one-way. Forks, caches and scrapers copy history within minutes and GitHub has no hook on the visibility toggle, so every cleanup happens before the flip, and the flip is its own gate.
- Rotation before rewrite. A rewrite doesn't un-leak a secret that was ever pushed; rotating at the source is what protects you, the history rewrite is hygiene.
- Complete report, no raw values. Every finding is shown, including ones you keep, but secrets appear only as fingerprints; the helper scripts never print a value.
- Throwaway clones only. Scanning and rewriting happen in fresh clones under a temp work dir, never your working clone.
Shared context for these principles: Matt Pocock's video.
Use cases
- Open-sourcing a repo that started private.
- Removing a committed
.envor API key from every commit before publishing. - Stripping large binaries (demo videos, datasets) out of history to shrink the repo, while optionally keeping the current file.
- A pre-publish audit where you scrub nothing: the full report still tells you what is in history.
Tips
- Have rotation access ready (cloud console, API dashboards) before you start; the rewrite waits on it.
ROTATE.mdhas the checklist by secret type. - Selecting nothing is not "scrub everything": you pick per ID or in bulk ("all secrets", "over 50MB", a path glob) and Claude echoes the final lists.
- Collaborators and open PRs need to re-clone or recreate after the force-push; let Claude draft the notice before the push.
- Old commits can stay reachable on GitHub (
refs/pull/*, direct SHA, cached views) until GitHub Support garbage-collects them. - Repos using Git LFS are out of scope: it stops before scanning.
- Want the working tree tidied first (README, stale docs)? Run
repo-showcasebefore this.
Example
- Finds
S1(an API key inconfig/dev.env, 3 commits) andL1(a 340 MB demo video, accumulated across history). - You scrub both.
- Claude tells you to rotate the API key first and waits for confirmation.
- Then it warns about 1 open PR, you approve, it rewrites clone #2, verifies gitleaks is clean, force-pushes, and lists the PR to recreate.
- It asks separately before making the repo public.
- Then it deletes the work dir holding the gitleaks report and findings.json.
Related
repo-showcasetidies the working tree first, then hands off here for history and visibility.
Prereqs
gh(authenticated),gitleaks,git-filter-repo, and Python for the helper scripts.- Install commands are in the top README; the skill also prints the right one if a tool is missing.
- No separate sizing tool: before/after repo size comes from
git count-objects -vH.
Sample workflows
- Take a repo public
repo-showcase → you → you → repo-scrub → you
Install just this one:./scripts/install-skills.sh --workflow go-publicor.\scripts\install-skills.ps1 -Workflow go-public
Use cases & tips from the community