# Branch Hygiene

> 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.

- Skill: `shrwnsan/branch-hygiene` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add shrwnsan/branch-hygiene`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shrwnsan/branch-hygiene/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: shrwnsan (https://skillmd.com/u/shrwnsan)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/shrwnsan/branch-hygiene

---


# 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

1. **Classify** — run the read-only classifier:
   ```bash
   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>`

2. **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.

3. **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 |

4. **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:
1. Extract its commit subjects; grep mainline log for the same subjects with
   `(#N)` suffixes — squash-merges preserve the subject.
2. Check file presence: key files the branch adds, present on mainline?
3. 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.

