Delete local branches that have been merged or whose PRs are closed/stale
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/git-cleanup/SKILL.mdUse this skill when the user asks to run the migrated command git-cleanup or invokes $git-cleanup.
Analyze local branches, classify them by status, and offer to delete merged/stale ones.
Run git branch --show-current. If empty, stop and tell the user:
"You're in detached HEAD state. Check out a branch first."
Record the current branch name — it must never be deleted.
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || git config init.defaultBranch || echo "main"Record as <default-branch>. It must never be deleted.
git for-each-ref --format='%(refname:short)|%(committerdate:short)|%(committerdate:relative)' refs/heads/ | sort -t'|' -k2Exclude <default-branch> and the current branch from candidates.
Run:
git worktree list --porcelainParse the output into blank-line-separated entries. Skip the first entry (the main worktree — git refuses to remove it anyway). For every entry containing a detached line (in place of a branch refs/heads/... line), record:
<worktree-path> from the worktree <path> line<worktree-head> from the HEAD <sha> linelocked line is presentClassify each entry by its path basename:
^review-pr-(?P<repo>.+)-(?P<number>\d+)-[A-Za-z0-9_]{8}$. The repo component may itself contain hyphens and digits (e.g. review-pr-cloud-infra-14965-h0md9p_l → repo cloud-infra, PR 14965), so take <number> as the last numeric group before the 8-character random suffix.For each review-pr worktree, query the PR state in the current repo:
gh pr view <number> --json state,mergedAt --jq '"\(.state)|\(.mergedAt // "n/a")"'state is one of OPEN, CLOSED, MERGED. If the command fails (offline, PR deleted), record the state as UNKNOWN.
For each candidate branch:
Ancestor check: git merge-base --is-ancestor <branch> <default-branch> — if yes, it's merged (rebase-merged or fast-forwarded).
PR status: Query GitHub for the branch's PR state:
gh pr list --head "<branch>" --state all --json number,state,mergedAt --jq '.[0] | "\(.number)|\(.state)|\(.mergedAt // \"n/a\")"'Place each branch into one of these categories:
| Category | Criteria |
|---|---|
| Active | Open PR, or current branch |
| Merged | PR state is MERGED, or branch is ancestor of default branch |
| Closed | PR state is CLOSED (not merged) |
| Stale | No PR found and not merged |
| Protected | Default branch or current branch — never delete |
Classify each detached worktree from step 4 into one of these categories. Two checks, run per worktree:
git -C <worktree-path> status --porcelain produces no outputrefs/remotes/origin/pr/<number> exists (git rev-parse --verify refs/remotes/origin/pr/<number>) AND resolves to a different SHA than <worktree-head>. If the ref does not exist, treat local-commit status as unknown.| Category | Criteria |
|---|---|
| Review worktree — safe to remove | review-pr worktree; PR state MERGED or CLOSED; clean; no local commits |
| Review worktree — active | review-pr worktree; PR state OPEN or UNKNOWN; clean; no local commits (kept, like open-PR branches) |
| Review worktree — has local changes | dirty, or has local commits, or local-commit status unknown |
| Other detached | not a review-pr worktree, or locked |
Locked worktrees are never removed (git refuses without --force); list them under Other detached with a note.
Display a table grouped by category:
## Branch Cleanup Summary
### Active (will keep)
| Branch | Age | PR |
|--------|-----|----|
| ... | ... | .. |
### Merged (safe to delete)
| Branch | Age | PR |
|--------|-----|----|
| ... | ... | .. |
### Closed (safe to delete)
| Branch | Age | PR |
|--------|-----|----|
| ... | ... | .. |
### Stale — no PR (safe to delete)
| Branch | Age | PR |
|--------|-----|----|
| ... | ... | .. |If there are no branches to delete, report "All branches are active or protected. Nothing to clean up." and stop.
Add detached-worktree sections to the summary when any exist:
### Review Worktrees — PR merged/closed (safe to remove)
| Worktree | PR | State |
|----------|----|-------|
| ... | #N | MERGED |
### Review Worktrees — has local changes (explicit approval required)
| Worktree | PR | Reason |
|----------|----|--------|
| ... | #N | dirty / local commits / unknown |
### Other Detached Worktrees (explicit pick only)
| Worktree | HEAD |
|----------|------|
| ... | abc1234 |Review worktrees in the active category go in the existing "Active (will keep)" table with their PR number. If there is nothing to delete in any category (branches or worktrees), report "All branches and worktrees are active or protected. Nothing to clean up." and stop.
Ask the user:
"Delete N branches (M merged, X closed, Y stale) and W review worktrees (merged/closed PRs)? [yes/no/pick]"
- yes — delete all merged + closed + stale branches and all "safe to remove" review worktrees
- no — abort, keep everything
- pick — let user specify which categories or individual branches/worktrees to delete; "has local changes" and "other detached" worktrees are removed ONLY when explicitly picked by path
Before deleting branches, check for associated worktrees:
git worktree list --porcelainFor each confirmed branch, check if a worktree references it (the branch refs/heads/<branch> line in porcelain output). If so, remove the worktree first:
git worktree remove <worktree-path>If removal fails (dirty worktree), warn the user and skip that branch — do not force-remove without explicit approval.
Then, for each confirmed detached worktree:
git worktree remove <worktree-path>Do not pass --force. If removal fails (dirty, locked, or main worktree), warn the user and skip — force-remove only items the user explicitly approved by path after the warning.
After all removals, clear stale administrative entries whose directories have already vanished (macOS cleans $TMPDIR, which orphans review-pr worktree entries):
git worktree prunegit branch -D <branch1> <branch2> ...Delete in a single command for efficiency. Do NOT use -d (lowercase) since squash-merged branches won't be detected as merged by git.
List deleted branches and remaining local branches:
Deleted N branches:
- branch1
- branch2
Remaining local branches:
- main
- feature/current-work
- ...When worktrees were removed, add:
Removed N worktrees:
- /path/to/review-pr-<repo>-<number>-<rand> (PR #N, MERGED)phatblat/dotfiles
4d65cab
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.