| name | clean-branches |
| description | Delete local branches whose PRs have been merged upstream. Checks GitHub PR status for each branch. Triggers on: clean branches, delete merged branches, prune branches, branch cleanup. |
| allowed-tools | Bash, Read, Grep, Glob |
Clean Branches - Delete Merged Local Branches
Clean up local branches by checking their GitHub PR status and deleting those
that have been merged.
Steps
- Detect the default branch -- Determine the repo's default branch name
(could be
main, master, develop, etc.):
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
- Fetch and list branches -- Fetch the latest remote state, then get all
local branches except the default branch:
git fetch origin "$DEFAULT_BRANCH"
git branch --format='%(refname:short)' | grep -v "^${DEFAULT_BRANCH}$"
Show the list to the user. If no branches exist besides the default branch,
report that and stop.
- Check merge status for each branch -- Use multiple methods to detect
merges (including squash merges):
Method 1: Direct ancestry -- Catches regular merges and fast-forwards:
git branch --merged "origin/$DEFAULT_BRANCH" --format='%(refname:short)'
Method 2: Squash merge detection via git cherry -- For branches not caught
by method 1, check if all commits have equivalent patches in the default branch.
A - prefix means the patch already exists upstream:
git cherry -v "origin/$DEFAULT_BRANCH" <branch>
If ALL lines start with - (or output is empty), the branch has been
squash-merged.
Method 3: GitHub PR status (if gh is authenticated) -- For remaining
branches:
gh pr list --head <branch> --state merged --json number,title,url
gh pr list --head <branch> --state closed --json number,title,url,mergedAt
A PR is "closed without merge" when it appears in the closed list but has an
empty/null mergedAt field. If gh is not authenticated, skip this method.
Important: Always compare against origin/$DEFAULT_BRANCH (not the local
default branch) to detect merges even when the local branch is stale.
-
Categorize branches into these groups:
- Merged: Detected as merged by any method above -> safe to delete
- Closed (not merged): PR was closed without merging -> flag for user
decision
- No PR / Open PR: No PR found (and not detected as merged), or PR is
still open -> skip
-
Display categorized list -- Show the user a summary table with:
- Branch name
- Category (Merged / Closed / No PR / Open)
- PR number and title (if available)
- Detection method (ancestry / squash / PR status)
-
Confirm with user -- Ask the user which branches to delete:
- Pre-select "Merged" branches for deletion
- Ask about "Closed (not merged)" branches individually
- Never auto-delete "No PR" or "Open PR" branches
-
Delete confirmed branches -- If currently on a branch being deleted,
switch to the default branch first and fast-forward it:
git checkout "$DEFAULT_BRANCH" && git merge --ff-only "origin/$DEFAULT_BRANCH"
git branch -D <branch1> <branch2> ...
After deleting, prune stale remote tracking references:
git remote prune origin
- Report results -- Summarize:
- How many branches were deleted
- Which branches were kept and why