repository-hygiene-sweep — clean a repository without destroying useful work
Maintain a git repository: prune merged branches, compact storage, hunt large/dangling objects, and (only with explicit consent) rewrite history to remove accidental large files or secrets. The discipline is conservation — every step is reversible-first, and anything ambiguous is skipped, not guessed.
⚠️ Critical Constraints
- Never run a destructive command without explicit, scoped confirmation. Branch deletion, history rewrite,
reflog expire, andgc --prune=noware the destructive set. Name exactly what will be destroyed and wait for a yes. Why: these are the operations that lose real work, and the user often does not realize what a branch or reflog entry was protecting. - Skip when in doubt — never guess. If you cannot prove a branch is fully merged, a file is truly junk, or a repo is not shared, do nothing and report the ambiguity. Why: the cost of a wrong delete (lost work) vastly exceeds the cost of leaving a stale branch around.
- Read-only first; mutate only after. Run the diagnosis pass (counts, sizes, candidate lists) and present it before changing anything. Why: the user must see what they are approving.
- WRONG:
git branch --merged | grep -v '\*' | xargs git branch -d(auto-deletes on a single guess) - CORRECT: list
git branch --merged main, show it, get confirmation, then delete named branches one set at a time.
- WRONG:
- Never delete a branch that is not merged with
-D. Use-d(refuses to delete unmerged). Why:-Dforce-deletes branches that contain unique commits — the classic way to silently lose work.- WRONG:
git branch -D feature/x - CORRECT:
git branch -d feature/xand, if it refuses, investigate why instead of forcing.
- WRONG:
- Never rewrite published/shared history casually. History rewrite (filter-repo, BFG) is acceptable only on a repo the owner confirms is unshared or whose collaborators have agreed to re-clone. Why: rewriting shared history breaks every other clone and can resurrect "deleted" data on the next push.
- Always confirm a clean, backed-up state before any history rewrite. Require a committed/clean working tree and a fresh mirror clone (
git clone --mirror) as the backup. Why: history rewrite is the one operation with no in-repo undo once reflogs are expired. - A leaked secret is not "removed" by rewriting history alone. Always advise rotating/revoking the secret too. Why: the secret may already be cloned, cached, or on a fork — rotation is the only real remediation.
Why This Exists
Repositories accumulate cruft: dozens of merged feature branches, a .git directory bloated by an accidentally-committed binary, loose objects, and .gitignore files that let junk slip in. Cleaning them is routine but genuinely dangerous — the same commands that reclaim space (gc --prune, branch -D, history rewrite) are the ones that permanently destroy work. Agents are biased toward "looks done" and toward force-flags that make errors disappear; that bias is exactly wrong here. This skill exists to make the safe path the default path: diagnose, propose, confirm, act reversibly, and skip anything uncertain.
Quick Start
# 1. Confirm where you are and that nothing is uncommitted (diagnosis is safe, read-only).
git rev-parse --is-inside-work-tree && git status --short
# 2. Run the full read-only diagnosis bundle.
bash {baseDir}/scripts/diagnose.sh # branches, sizes, large blobs, dangling objects
# 3. Review the report, confirm specific actions, then mutate one category at a time.
{baseDir}/scripts/diagnose.sh is Execute — run it to gather the report. It changes nothing.
Workflow
Phase 1 — Diagnose (read-only, always first)
Establish ground truth before touching anything.
git status --short # uncommitted work? (if yes, STOP — back up first)
git fetch --all --prune --dry-run # what would prune; preview only
git branch -vv --merged # branches fully merged into HEAD
git branch -vv --no-merged # branches with unique commits (DO NOT auto-delete)
git for-each-ref --sort=committerdate refs/heads --format='%(committerdate:short) %(refname:short)'
du -sh .git # current repo storage footprint
git count-objects -vH # loose vs packed, size, dangling count
Checkpoint: if the working tree is dirty, or this is a shared repo and the user has not confirmed it, stop and report. Do not proceed to any mutation.
Phase 2 — Prune branches (reversible)
Branch deletion is recoverable via reflog for a while, but treat it as consequential.
# Identify the integration branch explicitly (main/master/develop) — do not assume.
git branch --merged main | grep -vE '^\*| main$| master$| develop$' # safe-to-delete candidates
- Present the candidate list. Delete only confirmed names, using
-d(never-D). - For remote-tracking cleanup of branches already deleted upstream:
git fetch --prune(safe; only removes stale remote refs, not your local branches). - Record each deleted branch's tip SHA in the report so it can be restored:
git branch <name> <sha>.
Checkpoint: verify each deletion succeeded with -d (no force). If git refused, the branch had unique commits — leave it and flag it.
Phase 3 — Compact storage (reversible vs destructive split)
git gc # safe: repacks, keeps reflog-reachable + recent loose objects
git repack -Ad # safe: consolidate packs
# DESTRUCTIVE — only after explicit confirmation:
# git reflog expire --expire=now --all && git gc --prune=now # drops unreachable objects permanently
Run the safe gc by default. Only offer the prune-now path when the user explicitly wants to reclaim space from already-removed history AND understands it makes recovery impossible.
Phase 4 — Find large / dangling objects (read-only)
See references/large-objects.md for the blob-size ranking and dangling-object triage recipes.
Checkpoint: finding a large blob does not mean removing it. Confirm it is genuinely unwanted (not a needed asset) before considering Phase 5.
Phase 5 — Remove large files / secrets from history (DESTRUCTIVE — gated)
This rewrites history. Do not enter this phase without: clean tree, confirmed unshared-or-agreed repo, and a git clone --mirror backup. Procedure, tool choice (git-filter-repo vs BFG), and the force-push/re-clone protocol are in references/history-rewrite.md.
Phase 6 — .gitignore hygiene (low-risk)
git status --ignored --short # what is currently ignored
git ls-files --others --exclude-standard # untracked, not ignored — should any be ignored?
git check-ignore -v <path> # explain why a path is/ignored
- Propose additions for build artifacts, dependency dirs, env/secret files, OS cruft. Do not blanket-ignore source.
- If a file is ALREADY tracked, adding it to
.gitignoredoes nothing — note that it needsgit rm --cached(a tracked-content change, confirm separately).
Output Specification
Produce a report named repo-cleanup-report.md (or inline if small) with:
- Findings — repo size, branch counts (merged/unmerged/stale), large blobs (size + path), dangling object count, .gitignore gaps.
- Actions taken — each mutation, the exact command, and the result.
- Skipped (and why) — every ambiguous item deliberately left alone.
- Recovery — reversible commands (deleted branch SHAs, mirror-backup location).
Quality Rubric
- No destructive command (
branch -D,gc --prune=now, history rewrite,reflog expire) ran without a logged, explicit confirmation for that specific target. - Every branch deletion used
-d, and every deleted branch's tip SHA is recorded for restore. - The report's "Skipped" section is non-empty whenever any candidate was ambiguous — the skill demonstrably chose caution over completeness.
Examples
- Routine pass: Diagnose shows 18 merged branches, 1.2 GB
.git, 40 loose objects. → List merged branches, confirm,-ddelete;git gc; report 1.2 GB → 1.2 GB (size dominated by a large blob in history, surfaced in Phase 4 for a separate decision). - Bloat hunt:
.gitis 4 GB. Phase 4 finds a 3.6 GBdataset.zipcommitted 200 commits back. → Confirm it is unwanted, confirm repo is solo + clean, mirror-backup, thengit filter-repoper references/history-rewrite.md; advise force-push + collaborator re-clone. - Leaked key:
.envwith a live API key was committed. → Rewrite history to drop it AND tell the user the key must be rotated immediately — rewriting alone does not secure it.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
git branch -d refuses ("not fully merged") |
Branch has unique commits | Do NOT use -D. Investigate the commits; keep the branch and flag it. |
.git size unchanged after gc |
Large objects still reachable from history/reflog | Confirm intent; only then Phase 5 history rewrite, not blind --prune=now. |
gc --prune=now lost a stash/commit |
Reflog was expired, removing the only ref | Recover from the mirror backup; this is why the backup is mandatory. |
| Force-push rejected after rewrite | Remote protects the branch / others pushed | Coordinate with collaborators; do not work around branch protections unilaterally. |
Adding to .gitignore had no effect |
File is already tracked | git rm --cached <path> (confirm separately), then commit. |
See Also
| I need to… | Reference |
|---|---|
| Rank large blobs / triage dangling objects | references/large-objects.md |
| Safely rewrite history to remove a file/secret | references/history-rewrite.md |