Clean Branches (aka CB / prune)
Audit branches in the current repo — both your local checkout and the remote. Delete dead ones, rebase stale ones, open MRs for orphaned work, and sweep up local-only stragglers — all without disrupting active sessions.
When this fires
- User says “clean branches”, “cb”, “prune”, “prune branches”, “tidy up branches”
- User says “clear dead branches”, “clean up the repo”
- User says “what branches can we delete?”
Scope: local AND remote
Prune in both places — they accumulate junk independently:
- Remote branches — dead/stale/orphaned remote refs (the bulk of this skill).
- Local branches — branches that linger in your checkout after their PR merged, or whose upstream remote was deleted (
[gone]). The remote pass alone won’t catch a local branch whose remote is already gone, so there’s a dedicated local pass (step 8).
Definitions
| Category | Criteria | Action |
|---|---|---|
| Dead | Purely behind main (no unique commits ahead), no open MR, no linked issue, not created in the last 7 days | Delete |
| Stale | Has unique commits ahead of main but is behind main, no recent activity (>30 days), not actively being worked on | Rebase on main, open MR if none exists |
| Active | Has an open MR, linked issue, recent commits (<30 days), or a claim comment | Skip — don’t touch |
| New | Created in the last 7 days | Skip — too fresh to judge |
| Local merged | Local branch fully merged into main, or whose PR merged (upstream [gone]) |
Delete locally (git branch -d) — step 8 |
| Local-only unpushed | Local branch with unique commits, never pushed, no MR | Flag — ask before touching (step 8) |
The first four rows apply to remote branches (steps 1–7); the last two are the local pass (step 8). A branch can need both — e.g. delete the remote ref and the leftover local tracking branch.
Procedure
1. Detect the forge
git remote get-url originDetermine GitHub (gh) vs GitLab (glab).
2. Fetch and list remote branches
git fetch --prune origin
git branch -r --merged origin/main | grep -v 'origin/main\|origin/HEAD'
git branch -r --no-merged origin/main3. Classify each branch
For each remote branch (excluding main, HEAD, protected branches):
a. Check if it’s purely behind main (merged/dead)
# Commits on branch not on main (ahead count)
git rev-list --count origin/main..origin/<branch>
# If 0 → branch is purely behind main (already merged or never diverged)b. Check recency
git log -1 --format='%ci' origin/<branch>Skip if created/last-committed within the last 7 days (too new).
c. Check for open MR/PR
# GitLab
glab mr list --source-branch=<branch> 2>&1 | cat
# GitHub
gh pr list --head=<branch> --json number,title,state | cat # LIST_PRSIf an open MR exists → Active, skip.
d. Check for linked issues
Look for branch naming patterns that reference issues: - fix/123-*, feat/123-*, issue-123-* → check if issue #123 is open - If the linked issue is open → Active, skip
e. Check for active work claims
# Look for recent "working on this" / claim comments on any linked MR/issueIf a claim comment exists within the last 24 hours → Active, skip.
4. Present the plan (dry run)
Before taking any action, present a table to the user:
## Branch Cleanup Plan
| Branch | Last commit | Status | Action |
|--------|-------------|--------|--------|
| `old-feature` | 2025-03-15 | Dead (merged) | 🗑️ Delete |
| `wip-refactor` | 2025-11-20 | Stale (45 days, no MR) | 🔄 Rebase + open MR |
| `fix/42-typo` | 2026-06-15 | Active (open MR !80) | ⏭️ Skip |
| `experiment` | 2026-06-12 | New (<7 days) | ⏭️ Skip |
Proceed? (or pick specific branches to act on)
Wait for user confirmation before proceeding. If user says “just go” or “do it”, proceed with all proposed actions.
5. Delete dead branches
git push origin --delete <branch> # DELETE_REFAlso clean up local tracking branches:
git branch -d <local-tracking-branch> # if it exists locally6. Rebase stale branches
For each stale branch:
git checkout -B <branch> origin/<branch> # -B (not -b) force-resets if the branch already exists locally
git rebase origin/mainIf rebase has conflicts: - Attempt to resolve automatically (see the resolve-conflicts skill — consolidate both sides, don’t blind-pick) - If conflicts are non-trivial, skip this branch and report it - Don’t force-push a broken rebase
If rebase succeeds:
git push --force-with-lease origin <branch> # PUSH7. Open MRs for orphaned stale branches
For stale branches that have no open MR after rebasing:
# GitLab — assign to the current glab user (override ASSIGNEE to assign someone else)
ASSIGNEE="$(glab api user 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['username'])")"
glab mr create --source-branch=<branch> --target-branch=main \
--title "<inferred title from branch name>" \
--description "Orphaned branch rebased onto main. Review or close if no longer needed." \
${ASSIGNEE:+--assignee "$ASSIGNEE"}
# GitHub
gh pr create --head=<branch> --base=main \
--title "<inferred title>" \
--body "Orphaned branch rebased onto main. Review or close if no longer needed." # CREATE_PR8. Prune local branches
The remote pass doesn’t touch branches that only exist in your checkout. Sweep those too. First refresh remote-tracking state so “merged” and “gone” are accurate:
git fetch --prune origin # marks deleted upstreams as [gone]
git branch --show-current # never delete the branch you're onClassify each local branch (excluding main/master/protected and the current branch):
a. Merged into main → delete
git branch --merged origin/main | grep -vE '^\s*\*|^\s*main\s*$|^\s*master\s*$'
# Line-anchored so only the literal `main`/`master` lines (and the current `*`
# branch) are excluded — a branch like `maintain-docs` or `feature-main-menu`
# is NOT silently filtered out.
# Compare against origin/main (just fetched), NOT local `main` — your local main
# may be behind, which would hide branches that are actually merged.
git branch -d <branch> # -d refuses if NOT actually merged — a safety net-d (never -D) is deliberate: if git refuses, the branch has unmerged commits — treat it as stale, not dead (see b).
b. Upstream gone but the PR merged → delete
A branch whose remote was deleted shows [gone]:
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads \
| grep '\[gone\]'For each, confirm the PR/MR actually merged before deleting (never assume):
# GitHub
gh pr list --head <branch> --state merged --json number,mergedAt | cat # LIST_PRS
# GitLab
glab mr list --source-branch=<branch> --state merged 2>&1 | cat- PR merged →
git branch -D <branch>is acceptable here (the work landed via squash/rebase merge, so-dmay not see it as merged). Confirm the merge first. - No merged PR and unique commits exist → stale local work: don’t delete; offer to push it and open an MR (step 7 mechanics).
c. Never pushed, has unique commits → keep, but flag
git for-each-ref --format='%(refname:short) %(upstream)' refs/heads \
| awk '$2=="" {print $1}' # local branches with no upstream at allReport these as “local-only, unpushed” and ask before doing anything — they may be in-progress work that hasn’t been pushed yet. Don’t delete without confirmation.
Apply the same dry-run discipline to local deletions as step 4 does for remote branches — no silent local deletions. If you’re doing a full local+remote sweep, fold these local rows into the step-4 plan and present them together; if you’re running the local pass on its own, present a standalone local plan here and wait for confirmation before deleting anything.
9. Report
Print a summary covering both local and remote:
## Branch Cleanup Complete — <timestamp>
### Deleted — remote (dead)
- `old-feature` (last commit 2025-03-15, merged into main)
### Deleted — local (merged / upstream gone)
- `add-wrap-up-skill` (PR #26 merged; local straggler)
- `ums-session-learnings` (merged into main)
### Rebased + MR opened (stale)
- `wip-refactor` → [!85](url) (rebased, 3 commits ahead)
### Skipped (active/new)
- `fix/42-typo` — open MR !80
- `experiment` — created 2 days ago
### Flagged — local-only, unpushed (left alone)
- `scratch-idea` — 4 unpushed commits, no MR; your call
### Failed (conflicts)
- `ancient-branch` — rebase conflicts, needs manual resolution
Safety rules
- Never delete
main,master,develop, or any protected branch. - Never force-push to a branch with an open MR without rebasing cleanly.
- Always present the plan first — no silent deletions.
- Check for active work before touching any branch.
- Preserve local branches the user is currently on (
git branch --show-current). - Don’t delete branches newer than 7 days — they might be in-progress work that just hasn’t gotten an MR yet.
- Prefer
git branch -dover-Dfor local deletions —-drefuses unless the branch is merged, which catches “I thought this landed but it didn’t.” Only use-Dafter confirming the PR merged (squash/rebase merges can leave a local branch that-dwon’t recognize as merged). - Never delete a local-only unpushed branch without confirmation — if it has unique commits and no remote, that work exists nowhere else.
Relationship to other skills
sync-pr-branch— used internally when rebasing stale branchesclaim-pr— checked to avoid touching claimed branchesardi— user may want to ARDI the newly opened MRs afterwardclean-worktrees/cw— the worktree counterpart. This skill sweeps branches; that one sweeps the worktrees a branch is checked out into. Run both so neither a dead worktree nor an orphaned branch lingers.
Anti-patterns
- ❌ Deleting branches without checking for open MRs/issues first
- ❌ Force-pushing a broken rebase
- ❌ Touching branches that someone is actively working on
- ❌ Deleting branches without user confirmation
- ❌ Rebasing branches that have open MRs (use merge instead, or skip)