repo-scrub
Skill37 starsChanged 47 days agoMITView on GitHub
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.
- Reads credentials
- Deletes or force-pushes
- Installs packages
- Commits and pushes
1 other project uses a version of this.
Adopted by
Most recently taken up by
- chryzsh/claude-skillstheir own version, first seen Oct 17, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- chryzsh/claude-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository23 other files it gives its agents.
- dotfiles/CLAUDE.mdCLAUDE.md
- find-me-a-skillSkill
- gap-scanSkill
- grill-docsSkill
- grillingSkill
- grill-meSkill
- handoff-autoSkill
- handoffSkill
- lanesSkill
- meta-orchestratorSkill
- pairSkill
- ponytailSkill
- progress-reportSkill
- pr-watchSkill
- remindSkill
- repo-showcaseSkill
- score-loopSkill
- session-bridgeSkill
- sitrepSkill
- spend-gateSkill
- switchSkill
- teammate-briefSkill
- wayfinderSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 12
Complete report: every secret and large blob found is shown, including ones the user keeps.
- Very specific: line 47
Run the LFS history check from step 2 here, in scan-clone, before trusting the scan.
- Very specific: line 117
Only after the step-6 notice went out (or was confirmed unnecessary).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Going public is one-way: forks, caches and scrapers copy history within minutes, and GitHub offers no hook on its visibility toggle. So all cleanup happens before the flip, and you flip it yourself with gh.
Two rules hold throughout:
- Gate: each numbered gate below needs the user's explicit go-ahead.
- Complete report: every secret and large blob found is shown, including ones the user keeps.
<tmp> = $(mktemp -d), created once and echoed to the user; reuse that same path in every step (Git Bash's /tmp and Windows %TEMP% differ for Python). <tmp>/scrub-work holds secret-bearing files. Delete it (step 12) on every exit, early ones included.
1. Prereqs and target
gh auth status; else user runsgh auth login.gitleaks version; missing → Windowswinget install --id Gitleaks.Gitleaks -e, mac/linuxbrew install gitleaks. v8.19+ usesgitleaks git; older usesgitleaks detect --source . --log-opts="--all -m".git filter-repo --version; missing →pip install git-filter-repo(mac: or brew). Trust the version command, not pip's exit code: a--userinstall can land off PATH.
Missing tool → give the install command and stop.
<repo> = the argument, else origin's repo per the next skill's BOARD.md, Target repo section (never a bare gh call: on a fork it hits the parent). gh repo view <repo> --json name,owner,isPrivate,defaultBranchRef → confirm the repo, and that a rewrite + force-push is OK if anything gets scrubbed.
In the user's clone:
- Other remotes:
git remote -v; per remote other thanorigin,git ls-remote --heads --tags <remote>. Each is its own copy of the history: its branches are scanned (step 3) and rewritten (step 7), and it gets the rewritten history (step 9, each push on a yes). A branch only there is not local-only. List each remote with its branches. - Submodules:
git log --all -m -p -- .gitmodules | grep -E '^\+\s*url'. Each is a separate repo whose history this scan never enters: tell the user to/repo-scrubeach one before this repo goes public (a private one breaks--recurse-submodulesclones). Flag a relative URL (./,../): it resolves against wherever the superproject was cloned from, so public clones need that sibling repo public at the same path; else use an absolute URL.
2. LFS bail-out
gh api repos/<o>/<r>/contents/.gitattributes --jq .content | base64 -d | grep -i lfs (404 = no LFS). LFS-tracked paths → stop: filter-repo has no LFS awareness; this needs manual handling. This only sees the root file on the default branch, so repeat the check on clone #1 (step 3): git log --all -m -p -- '*.gitattributes' | grep -i filter=lfs catches nested files and other branches. A hit there → stop too.
3. Scan (clone #1)
mkdir -p <tmp>/scrub-work
git clone <url> <tmp>/scan-clone && cd <tmp>/scan-clone
git fetch <remote-url> '+refs/heads/*:refs/remotes/<remote>/*' # per other remote (step 1)
git count-objects -vH > <tmp>/scrub-work/before-size.txt
gitleaks git . --log-opts="--all -m" --report-format json --report-path <tmp>/scrub-work/gitleaks-report.json --no-banner
git filter-repo --analyze
python <skill-dir>/scripts/summarize_findings.py --gitleaks <tmp>/scrub-work/gitleaks-report.json \
--analysis-dir .git/filter-repo/analysis --out <tmp>/scrub-work/findings.json- Run the LFS history check from step 2 here, in
scan-clone, before trusting the scan. - gitleaks exit 1 = findings, not failure.
-mon--log-optsand on everygit log -S/--grephere and in step 8: without it a change made only in a merge commit (a conflict resolution) is never diffed, so a secret added there scans clean.summarize_findings.pymerges gitleaks +.git/filter-repo/analysis/intofindings.json: secretsS1…(deduped by rule + value, or by location when the value is empty; fingerprintsecret_sha256only), large blobsL1…(bytes accumulated across every version). Use it; the analysis tables are easy to misread.- Personal strings (names, emails) from
/repo-showcaseor the user: gitleaks has no rule for them. Per string,git log --all -m -S'<string>' --oneline(blob hits) andgit log --all -m -F -i --grep='<string>' --oneline(commit-message hits). Each hit is a findingP1…(Detail:personal string, plus the commit count); refer to it by ID from here on. - Identities and messages:
git log --all --format='%an %ae %cn %ce%n%B' | grep -Eo '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]+' | sort -uandgit log --all --format='%an <%ae>%n%cn <%ce>' | sort -u. Drop the owner's own (ask). Each other email is checked as a personal string; each other author/committer identity is aPfinding (Detail:author identity), unless/repo-showcasehanded over "author identities stay". - Signatures:
git log --all --format='%h %G?' | grep -v ' N$'(signed commits) andgit for-each-ref refs/tags --format='%(if)%(contents:signature)%(then)%(refname:short)%(end)'(signed tags). filter-repo drops every signature it rewrites. --analyzespends this clone (filter-repo refuses a second run). Deletescan-cloneoncefindings.jsonexists; keepgitleaks-report.jsonuntil step 12 (step 7 recovers values from it).
4. Report
3 secrets across 2 files, 2 large blobs totaling 410 MB (accumulated across history)
ID | Type | Path(s) | Detail | Commits
S1 | secret | config/dev.env (+1 more) | AWS Access Key | 3
L1 | large | assets/demo.mov | 340 MB (all versions) | -
P1 | personal | src/cli.js | personal string | 2
P2 | personal | (commit identity) | author identity | 4
Remotes: origin, mirror (branch only there: release/x). Submodules: vendor/lib (../lib.git, relative).
Signed: 2 commits, 1 tag (v1.0.0). A rewrite drops them: tags can be re-signed (step 8), commits stay unsigned.Raw secret values stay out of the table. Reveal one only on request, looked up by fingerprint in gitleaks-report.json.
5. Select
Per ID or bulk ("all secrets", "over 50MB", a path glob). Echo back the final scrub and keep lists. Silence is not "scrub everything".
6. Gates before any rewrite
- Rotation (any secret selected): walk the user through ROTATE.md. Gate: rotation confirmed done.
- Collaborators:
Anyone affected → draft a notice (what, why, re-clone from scratch, which PRs need recreating); user posts it or skips. Forks can't be messaged; report the count. Gate: go-ahead to rewrite.gh api repos/<o>/<r>/collaborators --jq '.[].login' gh api repos/<o>/<r>/invitations gh pr list -R <o>/<r> --state open --json number,title,headRefName,author gh api repos/<o>/<r>/forks --jq length
7. Rewrite (clone #2)
git clone <url> <tmp>/rewrite-clone && cd <tmp>/rewrite-clone
git fetch <remote-url> '+refs/heads/<b>:refs/remotes/origin/<b>' # per branch only on another remote
git repack -ad # after any such fetch: filter-repo wants one fresh pack
python <skill-dir>/scripts/build_replace_text.py --gitleaks <tmp>/scrub-work/gitleaks-report.json \
--findings <tmp>/scrub-work/findings.json --ids S1 S3 --out <tmp>/scrub-work/expressions.txtFetching into origin/<b> lets filter-repo turn it into a local branch like the rest; a name on two remotes with different tips → ask which one. The script writes --replace-text expressions without printing a value; no S selected → drop --ids and it writes an empty file for the P lines. Each selected P finding adds one line, literal:<string>==>REDACTED, appended to expressions.txt by a file edit (no echo of the string); a commit-message hit also needs --replace-message <tmp>/scrub-work/expressions.txt in the call below. literal: never touches author/committer fields: each selected author identity gets a line New Name <new@email> Old Name <old@email> (names bare, emails in <>; the user picks the replacement) in <tmp>/scrub-work/mailmap, and the call adds --mailmap <tmp>/scrub-work/mailmap. An identity the user keeps → record author identities stay: <identity> in the step-8 summary. Location-only findings (secret_sha256: null) have no value to replace; it skips them, so scrub those by --path or by hand. Large blobs, per finding: strip entirely, or strip history but keep the current file (re-add it in a commit after the rewrite: path/size stripping removes HEAD's copy too).
One combined call, only the flags for what was selected:
git filter-repo --replace-text <tmp>/scrub-work/expressions.txt --path <paths> --invert-paths(--strip-blobs-bigger-than <size> when a threshold describes the selection better.)
8. Verify (still reversible)
git log --oneline -10, gitleaks git . --log-opts="--all -m" --no-banner, git count-objects -vH. Spot-check stripped paths are gone. Per scrubbed P finding, git log --all -m -S'<string>' --oneline (and the --grep form for message hits) prints nothing, and so does git log --all --format='%an %ae %cn %ce %B' | grep -F -i '<string>' per scrubbed string or identity email (-S never sees identities). Show before/after commit count and size. Gate.
Per tag signed before (step 3), ask to re-sign it with the user's own key (user.signingkey): git tag -l --format='%(contents:subject)%0a%0a%(contents:body)' <tag> > <tmp>/scrub-work/tag.msg && git tag -f -s <tag> '<tag>^{commit}' -F <tmp>/scrub-work/tag.msg, then git tag -v <tag>. No → it ships unsigned; say so.
9. Force-push
Only after the step-6 notice went out (or was confirmed unnecessary). filter-repo removed origin:
git remote add origin <url>
git push origin --force --all && git push origin --force --tagsPer other remote from step 1, each on its own yes: git remote add <remote> <url> && git push <remote> --force <each branch it holds> (plus --tags if it held tags). One the user declines keeps the old history and the secret: say so.
A hook or permission rule that blocks the push → show the user these exact commands to run themselves (in Claude Code, prefixed with !), then continue at step 10 once they confirm.
--all covers non-default branches only because filter-repo turned the fresh clone's origin/* into local branches. Run git branch before pushing and confirm every branch you mean to keep is listed; a missing one is not pushed, and keeps its old history on the remote. Branches fetched from another remote at step 7 are local too: then push origin its own branches by name instead of --all.
10. Post-push
Every local copy of this repo, the user's own included, still holds the old commits (and the secret). A plain git pull would merge the old history back in and the next push would re-leak it. Per copy, either:
- Re-clone (simplest): copy uncommitted work, stashes (
git stash show -p --include-untracked stash@{n}) and unpushed branches and tags (git bundle create) out of the old clone first, and keep it until they are carried over. - Reset in place: follow POST-PUSH.md. It parks the WIP first, saves every stash entry (untracked parts included) and every local-only branch and tag outside the repo, resets the pushed branches, re-creates the stashes on the new history and checks them against the saved copies, re-points a tag whose commit was rebased, and deletes a branch or tag only on a yes. Its
reflog expireandgcrun before its final steps: a warning per branch or tag kept as is (old history, secret included, never push it) and a gated offer to delete the save folder. Never a baregit reflog expire --all: it wipes the stash list.
Tell collaborators the same (the step-6 notice).
gh pr list -R <o>/<r> --state open → name PRs that must be closed and recreated. Tell the user: old commits stay reachable on GitHub (via refs/pull/*, direct SHA, cached views) until GitHub Support garbage-collects them. Rotation protects the secret; the push is hygiene.
11. Flip visibility
State gh's consequences first: stars/watchers lost, forks detached, push rulesets disabled, Actions history becomes public. Gate, separate from the push.
gh repo edit <o>/<r> --visibility public --accept-visibility-change-consequences
gh repo view <o>/<r> --json isPrivate12. Cleanup
rm -rf <tmp>/scrub-work <tmp>/scan-clone <tmp>/rewrite-cloneDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

