CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

git-cleanup

Delete local branches that have been merged or whose PRs are closed/stale

Quality

76%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./.agents/skills/git-cleanup/SKILL.md
SKILL.md
Quality
Evals
Security

git-cleanup

Use this skill when the user asks to run the migrated command git-cleanup or invokes $git-cleanup.

Command Template

Git Cleanup: Remove Stale Local Branches

Analyze local branches, classify them by status, and offer to delete merged/stale ones.

Instructions

1. Guard: Not on a Detached HEAD

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.

2. Detect Default Branch

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.

3. List Local Branches with Metadata

git for-each-ref --format='%(refname:short)|%(committerdate:short)|%(committerdate:relative)' refs/heads/ | sort -t'|' -k2

Exclude <default-branch> and the current branch from candidates.

4. List Detached Worktrees

Run:

git worktree list --porcelain

Parse 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> line
  • whether a locked line is present

Classify each entry by its path basename:

  • review-pr worktree: basename matches ^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.
  • other detached worktree: anything else.

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.

5. Check Merge and PR Status

For each candidate branch:

  1. Ancestor check: git merge-base --is-ancestor <branch> <default-branch> — if yes, it's merged (rebase-merged or fast-forwarded).

  2. 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\")"'

6. Classify Branches

Place each branch into one of these categories:

CategoryCriteria
ActiveOpen PR, or current branch
MergedPR state is MERGED, or branch is ancestor of default branch
ClosedPR state is CLOSED (not merged)
StaleNo PR found and not merged
ProtectedDefault branch or current branch — never delete

Classify each detached worktree from step 4 into one of these categories. Two checks, run per worktree:

  • clean: git -C <worktree-path> status --porcelain produces no output
  • local commits: the ref refs/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.
CategoryCriteria
Review worktree — safe to removereview-pr worktree; PR state MERGED or CLOSED; clean; no local commits
Review worktree — activereview-pr worktree; PR state OPEN or UNKNOWN; clean; no local commits (kept, like open-PR branches)
Review worktree — has local changesdirty, or has local commits, or local-commit status unknown
Other detachednot a review-pr worktree, or locked

Locked worktrees are never removed (git refuses without --force); list them under Other detached with a note.

7. Present Summary

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.

8. Confirm Deletion

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

9. Remove Worktrees

Before deleting branches, check for associated worktrees:

git worktree list --porcelain

For 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 prune

10. Delete Confirmed Branches (skip any whose worktree removal was declined)

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

11. Report Results

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)
Repository

phatblat/dotfiles

Path

.agents/skills/git-cleanup/SKILL.md

Last updated
First committed

Is this your skill?

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.