Branch Audit — safe stale-branch investigation and pruning
name: branch-audit description: Audit local git branches for safe deletion with content-level verification (PR state + git cherry), flag-don't-delete discipline, and a mandatory user approval gate. Auto-invoke when user says "audit branches", "prune branches", "clean up branches", "stale branches", or "which branches can I delete".
Why this exists
This repo accumulates local branches fast: interactive sessions, the Pilot
daemon (pilot/GH-*), recovery work, backups. A 2026-07-05 audit found 29
local branches; 21 were safely deleted — but only after content-level
verification caught, among others, 15 unpushed commits on feat/aws-bench
that existed nowhere else. Ancestry checks alone would have missed that.
Prime directive: a branch is never deleted without explicit per-branch user approval, and never on ancestry evidence alone.
Method
Step 1 — Enumerate COMPLETELY
git branch --format='%(refname:short)' # never pipe through head — a truncated
# list caused a missed-branch round
git worktree list # branches checked out in worktrees
# cannot be deleted and may be live sessions
Exclude from the audit: the current main checkout, any claude/* branch
attached to a worktree (possibly a live session), and pilot-worktree-GH-*
daemon worktrees (per CLAUDE.md, leave alone).
Step 2 — Per-branch evidence (mechanical)
For each branch collect:
git log -1 --format='%h %cd %s' --date=short "$b" # tip
gh pr list --head "$b" --state all --json number,state # PR record
git cherry main "$b" | grep -c '^+' # non-equivalent commits
git cherry is the ground truth, not --merged. This repo squash-merges:
merged branches are never ancestors of main, and git branch --merged is
useless. git cherry compares patch IDs — 0 non-equivalent commits means
every change is content-identical to something on main.
Step 3 — Classify
| Class | Criteria | Action |
|---|---|---|
| SAFE | merged PR and cherry = 0 | propose deletion |
| SAFE (ancestor) | no PR, cherry = 0, merge-base --is-ancestor = yes |
propose deletion |
| HOLDS-WORK | cherry > 0 | deep-check (Step 4) — never propose blind |
| CHECKED-OUT | in a worktree | check worktree dirty state; live sessions untouchable |
Step 4 — Deep checks for HOLDS-WORK branches
- Local vs remote divergence — the highest-stakes check:
If local is ahead: push to origin as backup before anything else.git fetch origin "$b" git rev-list --count origin/"$b".."$b" # local-only commits = sole copy - Closed-PR supersession — a closed (not merged) PR may carry the answer:
Precedent:gh pr view <n> --json comments --jq '.comments[-2:][].body'recover/TASK-300-ghost-sha-guardshowed 2 "unique" commits, but PR #3189's closing comment named the superseding PR (#3193) and the guard was verified live ininternal/executor/git_freshness.go— same logic, different patch IDs. Verify the supersession claim in main's code, not just the comment. - Lost-docs salvage — backup branches can hold
.agent/files missing from main (graph nodes referencing nonexistent files are the tell):
Salvage via a docs PR before considering deletion.git diff --name-only $(git merge-base main "$b") "$b" | while read f; do [ -e "$f" ] || echo "MISSING: $f" done
Step 5 — Approval gate (mandatory)
Present the full classification table with evidence. Deletion requires
explicit user approval, per branch or per named group. Branches the user has
flagged as sensitive (historically: feat/aws-bench, feat/openrouter-engine)
stay untouched regardless of evidence.
Step 6 — Execute in safety order
- Backup pushes first (
git push origin <branch>for any sole-copy work) - Salvage PRs second
- Deletions last:
git branch -D <approved...> git remote prune origin
Pitfalls learned in the field
gh pr merge --delete-branchfails with "'main' is already used by worktree" in multi-worktree layouts — merge succeeds remotely; verify withgh pr view --json state, clean the local branch manually.- A branch checked out in a clean worktree at a merged tip loses nothing when
the worktree is removed — but confirm
status --porcelainis empty first. - Deleted branches remain reflog-recoverable for ~90 days; mention this in the approval ask.