Branch & Worktree Hygiene
Classifies every local/remote branch and worktree against the mainline, then
cleans up what is resolved. Never guesses — every deletion is backed by a
verified verdict.
The core problem
git branch --merged is useless in squash-merge workflows: it reports
nothing mergeable even when branches are fully resolved (content landed via
squashed PRs). The only reliable test is content-level: PR-number log matching,
file presence, and feature greps.
Workflow
Classify — run the read-only classifier:
sh <skill-dir>/scripts/classify-branches.sh [mainline-ref]
Default mainline: main (falls back to master). Emits one line per item:
<verdict> <kind> <ref-or-path> <evidence>
Verify — the classifier's output is evidence, not authority. Before
acting on any verdict, spot-check it: open the cited PR commit, diff the
named files, confirm the feature grep. Classifiers hallucinate less than
models but more than never.
Act per tier:
| Verdict |
Action |
Confirmation |
merged-ancestry / merged-content (branch) |
git branch -D |
none (reflog recovers) |
remote-gone (local branch, upstream deleted) |
git branch -D + remove any worktree |
none |
stale-base (branch predates mainline; merging would regress) |
surface to user |
user decides |
stale-base is currently emitted by review judgment, not the classifier — treat divergence without squash evidence as unmerged and assess manually.
| superseded (content landed, mainline evolved past it) | surface to user | user decides |
| unmerged (genuinely unique commits) | surface to user, never delete | user decides |
| worktree-prunable (directory gone, registry entry remains) | git worktree prune | none (metadata only) |
| worktree-clean-merged (clean tree, branch resolved) | git worktree remove | always confirm (deletes a directory) |
| worktree-dirty / worktree-unmerged | never touch | — |
| orphan-dir (looks like a worktree, not registered) | surface, move with trash on confirm | user decides |
Remote branches — deleting origin/* refs is outward-facing: list them
with verdicts, delete only after explicit user approval, in one batch push.
Squash-merge detection (how the classifier thinks)
For each branch not mergeable by ancestry:
- Extract its commit subjects; grep mainline log for the same subjects with
(#N) suffixes — squash-merges preserve the subject.
- Check file presence: key files the branch adds, present on mainline?
- Feature greps: distinctive strings from the branch's diff, present in
mainline's version of those files?
All three hit →
merged-content. Content present but mainline's copy is
larger/newer → superseded. Diff would remove lines mainline has → stale-base.
Worktree specifics
git worktree list --porcelain is the source of truth; each entry's branch
or detached HEAD gets the same verdict engine as free branches.
- Detached-HEAD worktrees: verdict keyed on the commit —
merged-ancestry
if contained in mainline and tree is clean.
- Tool-created worktrees (e.g.
.claude/worktrees/) are included; their
auto-cleanup is best-effort, this is the backstop.
Known limitations
- Origin only: the remote scan covers
origin/* exclusively. Repos with
additional remotes (upstream/, fork/) only get their origin refs
classified — run with awareness, or extend the remote loop.
- Squash-merge detection is heuristic: subject matching can false-positive
on generic subjects ("fix bugs"); the verify step exists for exactly this
reason. Never trust a verdict without checking its evidence.
Hard rules
- The classifier script is read-only — it never deletes, pushes, or prunes.
- Dirty worktrees and
unmerged refs are never modified, only reported.
git worktree remove and remote ref deletion always require user confirmation.
- Use
trash, never rm -rf, for orphaned directories.
1---2name: branch-hygiene3description: Classify and clean up merged, stale, and orphaned git branches and worktrees. Detects squash-merges that git branch --merged cannot see, stale worktrees, and remote-gone branches. Use when user asks to clean up branches, prune worktrees, tidy the repo, or perform git housekeeping.4---56# Branch & Worktree Hygiene78Classifies every local/remote branch and worktree against the mainline, then9cleans up what is resolved. Never guesses — every deletion is backed by a10verified verdict.1112## The core problem1314`git branch --merged` is **useless in squash-merge workflows**: it reports15nothing mergeable even when branches are fully resolved (content landed via16squashed PRs). The only reliable test is content-level: PR-number log matching,17file presence, and feature greps.1819## Workflow20211. **Classify** — run the read-only classifier:22 ```bash23 sh <skill-dir>/scripts/classify-branches.sh [mainline-ref]24 ```25 Default mainline: `main` (falls back to `master`). Emits one line per item:26 `<verdict> <kind> <ref-or-path> <evidence>`27282. **Verify** — the classifier's output is *evidence, not authority*. Before29 acting on any verdict, spot-check it: open the cited PR commit, diff the30 named files, confirm the feature grep. Classifiers hallucinate less than31 models but more than never.32333. **Act per tier**:3435 | Verdict | Action | Confirmation |36 |---|---|---|37 | `merged-ancestry` / `merged-content` (branch) | `git branch -D` | none (reflog recovers) |38 | `remote-gone` (local branch, upstream deleted) | `git branch -D` + remove any worktree | none |39 | `stale-base` (branch predates mainline; merging would regress) | surface to user | user decides |40 > `stale-base` is currently emitted by review judgment, not the classifier — treat divergence without squash evidence as `unmerged` and assess manually.41 | `superseded` (content landed, mainline evolved past it) | surface to user | user decides |42 | `unmerged` (genuinely unique commits) | surface to user, never delete | user decides |43 | `worktree-prunable` (directory gone, registry entry remains) | `git worktree prune` | none (metadata only) |44 | `worktree-clean-merged` (clean tree, branch resolved) | `git worktree remove` | **always confirm** (deletes a directory) |45 | `worktree-dirty` / `worktree-unmerged` | never touch | — |46 | `orphan-dir` (looks like a worktree, not registered) | surface, move with `trash` on confirm | user decides |47484. **Remote branches** — deleting `origin/*` refs is outward-facing: list them49 with verdicts, delete only after explicit user approval, in one batch push.5051## Squash-merge detection (how the classifier thinks)5253For each branch not mergeable by ancestry:541. Extract its commit subjects; grep mainline log for the same subjects with55 `(#N)` suffixes — squash-merges preserve the subject.562. Check file presence: key files the branch adds, present on mainline?573. Feature greps: distinctive strings from the branch's diff, present in58 mainline's version of those files?59All three hit → `merged-content`. Content present but mainline's copy is60larger/newer → `superseded`. Diff would *remove* lines mainline has → `stale-base`.6162## Worktree specifics6364- `git worktree list --porcelain` is the source of truth; each entry's `branch`65 or detached `HEAD` gets the same verdict engine as free branches.66- Detached-HEAD worktrees: verdict keyed on the commit — `merged-ancestry`67 if contained in mainline and tree is clean.68- Tool-created worktrees (e.g. `.claude/worktrees/`) are included; their69 auto-cleanup is best-effort, this is the backstop.7071## Known limitations7273- **Origin only**: the remote scan covers `origin/*` exclusively. Repos with74 additional remotes (`upstream/`, `fork/`) only get their origin refs75 classified — run with awareness, or extend the remote loop.76- **Squash-merge detection is heuristic**: subject matching can false-positive77 on generic subjects ("fix bugs"); the verify step exists for exactly this78 reason. Never trust a verdict without checking its evidence.7980## Hard rules8182- The classifier script is **read-only** — it never deletes, pushes, or prunes.83- Dirty worktrees and `unmerged` refs are never modified, only reported.84- `git worktree remove` and remote ref deletion always require user confirmation.85- Use `trash`, never `rm -rf`, for orphaned directories.