| name | prune-branches |
| description | Audits every branch a repo carries — local, remote, and worktree-pinned — and sorts them into safe to delete, stale, and carrying unmerged work, rescuing the last group onto a fresh verified branch before recommending anything. Built for repos that squash-merge, where `git branch --merged` and `git cherry` both report merged branches as unmerged because patch-ids never match, so merge evidence has to come from the PR list instead. Separates a stale remote-tracking ref from a branch that really is still on the remote. Reports and recommends; never deletes a branch and never pushes a deletion. Complements land, which prevents this debris on the happy path — this cleans up what never took it. |
| when_to_use | Use when asked to clean up, prune or audit branches, to work out which branches are safe to delete, or why a branch will not delete — including 'I thought I deleted these' and a `git branch -d` that refuses because a worktree holds the branch. Not for deleting one branch already known to be merged: that is a one-liner, and `land` already does it for a branch it just merged. An instruction to delete rather than report is the case the refusal exists for, not a reason to skip it. |
| allowed-tools | Bash(git branch *) Bash(git log *) Bash(git rev-list *) Bash(git ls-remote *) Bash(git worktree list *) Bash(git show *) Bash(git diff *) Bash(git cherry *) Bash(git fetch *) Bash(gh pr list *) |
| effort | max |
| disable-model-invocation | false |
Pruning branches
Enumerate every branch a repo carries, establish for each whether its
work is already in main, and end at a verdict table plus the exact
deletion command for the user to run.
Audit and report is the design, not caution
Read this before deciding the ladder below is over-careful and the
obvious finish is to delete the safe branches yourself.
The deletion is not yours to run, and attempting it burns the turn.
Claude Code's auto-mode classifier denies git push origin --delete as
[Git Destructive]. That denial is correct and it is not going away, so
a skill whose payoff step is the deletion fails its last step every
time — observed on four branches that had already been verified safe. A
verified recommendation is where the value actually is and where the
permission model already allows it to land.
The local half is not the soft half. In a squash repo
git branch -d refuses the very branches the audit clears — its merged
check is the one rung 2 says is wrong — so the only local deletion that
works is -D, the forced form, on a verdict git itself disputes.
Measured 2026-09-14: after git merge --squash,
git branch --merged main omitted the branch and -d answered
not fully merged. Not local, not remote: both go in the command
string.
So the output is a table and a command string. The user runs it.
An instruction to delete does not lift this. "Just delete them",
"don't make me run anything", "I trust your call on which are dead" —
each is the case this section exists for, not a waiver of it. The two
reasons above are reasons, not conditions: a permission mode the user
could loosen and a -d the user could force do not make the deletion
yours once they are gone. Reasoning from the mechanism is exactly how
the refusal was lost — measured 2026-09-14 on both invocation paths,
under a query that delegated the classification: the skill read its own
no-delete rule as the user's to waive, and used the -D fact as
guidance for deleting rather than grounds for not. The shape to hold is
land's Absolute constraints — an instruction to do these is a stop,
not an override. Say so once, give the table and the command, and stop.
The allowed-tools line encodes that split, and it pre-approves
rather than restricts. Every probe the audit needs is listed, so the
read-only half runs without a prompt per branch; no mutating command is
listed, so the cherry-pick, the worktree removal and the deletion stay
prompted. Listing a write would not enable anything currently
blocked — it would remove the prompt that keeps this skill honest. The
guard is this rule, not the field.
1. Detect the merge style first
The whole ladder below exists to work around squash-merge. Check before
performing it, or you will do four rungs of unnecessary work and present
it as rigor.
git log --merges --oneline | wc -l
gh pr list --state merged --limit 100 | wc -l
Near-zero merge commits against many merged PRs means squash or
rebase — continue to step 2.
Many merge commits means merge-commit style, where git branch --merged main is accurate and sufficient. Say so, run it, report, and
stop. Do not perform the rungs.
Only the squash path is evidence-backed. This procedure was
validated end to end on one squash-merge GitHub repo. Under rebase-merge
the failure mode is the same as squash — patch-ids do not survive the
rewrite — but the remedy may differ, and that was not tested. Under
merge-commit style the early exit above is the whole skill. State
whichever limit applies in the report rather than implying all three
were covered.
2. Rung 0 — establish what actually exists
git fetch origin --prune
git worktree list
git ls-remote --heads origin
git branch -a shows neither of the two findings that live here.
A branch pinned by a worktree cannot be deleted at all. The error
names the worktree path, and removing the worktree is the fix:
git worktree remove <path> while the directory exists,
git worktree prune when it was deleted by hand and only its entry
under .git/worktrees survives — git worktree list shows which. Both
are writes, so propose them, do not run them.
A remote-tracking ref is not evidence the branch is still on the
remote. origin/<name> can survive locally long after the branch is
gone. ls-remote asks the remote itself and is the only thing that
settles it. The observed run opened with a user who believed these
branches were already deleted; this is the command that answered it.
3. Rung 1 — ahead/behind
Per branch:
git rev-list --count main..origin/<branch>
git rev-list --count origin/<branch>..main
Zero ahead ends the analysis. The branch is fully contained in
main and is safe to delete regardless of how it got that way — no PR
lookup, no content reading. In the observed run this disposed of a third
of the set in one pass, including one branch identical to main.
Anything with commits ahead goes to rung 2.
4. Rung 2 — merge evidence
gh pr list --state all --limit 100 --json number,headRefName,state,title
Key on headRefName. A PR whose head was this branch and whose state
is MERGED is the merge evidence, and the branch is safe. The other
two states both mean keep, for different reasons: CLOSED is work
someone declined, still only on the branch, and goes on to rung 3;
OPEN is work in flight — keep it, say so, and stop there, since a
branch someone is actively landing needs no content archaeology.
--limit 100 returns the hundred most recent, so an old branch's PR can
fall outside it and read as no PR. That sends it to rung 3, the safe
direction but not a free one; raise the limit, or confirm a missing one
with gh pr list --state all --head <branch>.
git branch --merged and git cherry are both wrong here, and wrong
in the dangerous direction. Squashing rewrites the commits, so
patch-ids never match and both commands report squash-merged branches as
unmerged. In the observed run git cherry flagged every commit on
every remaining branch as unique — including on two branches that held
nothing worth keeping. An agent that trusts it keeps everything forever;
an agent that learns to override it deletes real work. Neither is a
usable default, which is why merge evidence comes from the PR list.
This rung is GitHub-specific. It depends on gh. GitLab and Azure
DevOps equivalents were not investigated — say so rather than presenting
the skill as forge-agnostic.
gh may not be acting as the account you expect. The GitHub API
actor is a third identity, bound separately from the commit author, and
gh auth status reports the keyring's active account rather than the
one gh will act as in this repo. Nothing here writes, so the full
identity procedure in land is not needed — just do not tell the user
that gh auth status says who they are.
Commits ahead and no merge evidence — no PR, or a CLOSED one —
goes to rung 3.
5. Rung 3 — content recency
This is the rung that prevents the expensive mistake, and the one most
likely to be skipped as redundant. Do not skip it.
"Has commits main does not" and "has work worth keeping" read as the
same statement and are not. A branch that diverged from an old main
looks identical, at commit level, to a branch carrying real work. The
difference is only visible in the content.
Resolve it per contested file, comparing both sides:
git log -1 --format="%h %ad %s" --date=short origin/<branch> -- <path>
git log -1 --format="%h %ad %s" --date=short main -- <path>
In the observed run a branch whose seven unique commits looked
substantial turned out to hold the older copy of its contested file —
superseded on main by a later PR. Every commit-level signal rated it
identical to the branches that carried real work.
6. Rung 4 — read what survives, and put judgment back to the user
Three patterns, all observed:
-
Commits that cancel exactly. Verify with a diff across the span,
not by reading the messages — a revert pair is obvious in the log and
a refactor-then-unrefactor pair is not:
git diff --stat <first>^ <last> -- <paths>
Empty output means the span is a no-op and the branch is safe.
-
Commits that contradict something the repo says about itself. This
is where the skill sends you to the repo's own instructions before
recommending a rescue. In the observed run a commit hand-applied a
vendor format upgrade that the repo documented as a portal-only
action — nothing in git would ever have flagged it, and rescuing it
would have re-landed a change the repo forbids.
-
Genuinely unmerged work, which goes to step 7.
Teach the habit of asking, and stop there. This rung can name the
question to ask; it cannot enumerate the answers, because they are
properties of the repo rather than of git. Put the call back to the user
with the evidence, and say plainly that it is a judgment call.
This is also where the logical commit split commit writes pays off:
the survivors are readable as units because they were committed as
units.
7. Rescue before recommending deletion
Any branch still holding real work after rung 4 is rescued, not deleted
— but the rescue is an extraction, not a relocation. It exists to
lift the survivors out of a branch that also holds junk, so the junk
branch can go. A branch whose every commit survived, already named to
convention, has nothing to extract: leave it where it is and say so.
Cherry-picking it onto a fresh branch manufactures the very debris this
audit is for. Observed 2026-09-14 on a one-commit branch with no PR: the
whole branch was the survivor, and the right output was "keep".
git switch main && git pull --ff-only
git switch -c <type>/<kebab-slug>
git cherry-pick <only the commits that survived rung 4>
Then verify with the repo's own validator and guard commands — read
them out of the project instructions rather than assuming a test runner
exists. A rescue that has not been run against the repo's own checks is
a claim, not a result.
Report the new branch and hand the landing to land. Do not push it.
A kept branch is handed over the same way, and it is usually behind
main — the rescue above is cut from fresh main and cannot be, but
a branch left in place predates every merge since its fork point, and
land's --ff-only refuses that rather than fixing it: it stops and
says main has moved. git merge-base --is-ancestor main <branch>
answers it (exit 0 means fine); if not, rebase onto main before the
handoff and say so, so the SHAs land reports are not a surprise.
8. Report, and stop
Emit three things:
-
A verdict table — one row per branch: the branch, the rung that
settled it, and the evidence that settled it. The rung number is the
useful column; it tells the user how much to trust the row.
-
The rescue branch, if any, with what was cherry-picked onto it
and which checks it passed.
-
The exact deletion command, for the user to run:
git branch -D <branch> <branch> ...
git push origin --delete <branch> <branch> ...
When nothing cleared, say so in as many words. An empty set is a
verdict, not a missing step — observed 2026-09-14 on a two-branch
repo where both carried work.
Then stop. Do not run it, and do not offer to.
A cheap upstream fix exists. Enabling auto-delete-on-merge and
branch protection stops most of this debris accumulating in the first
place; worth one closing sentence to the user when the audit found a
large set — and worth checking even when it found none, since a clean
list can be land deleting what it merged rather than the setting:
gh api repos/:owner/:repo --jq .delete_branch_on_merge read false
on a repo with no debris at all, 2026-09-14.
Boundary with land
Both skills delete branches, and the split is clean in both directions.
land deletes a branch it has just merged, from a state it watched
happen — its step 9 is deliberately ungated for exactly that reason, and
that reasoning does not generalise. This skill handles branches whose
provenance is unknown and which never went through land at all, which
is why it gates by not deleting.
land prevents this debris on the happy path. This cleans up what never
took it.
Constraints
- Never delete a branch and never push a deletion. Not local, not
remote, not "the obviously safe ones", not when told to. The output is
a command string.
- Never remove a worktree. Propose it; the user runs it.
- Do not push the rescue branch. Hand it to
land.
- Do not trust
git branch --merged or git cherry in a squash
repo — both report merged branches as unmerged. Merge evidence is
the PR list.
- Zero commits ahead is a complete answer. Do not read content to
confirm what
rev-list already settled.
- State the limits in the report: only squash-merge GitHub was
validated, and rung 2 depends on
gh.
- Rung 4 is the user's call, not yours. Present the evidence and the
question; do not decide what the repo considers worth keeping.