Git Branch Cleanup
Systematically identify and remove branches that are no longer needed. Stale branches accumulate fast — especially when PRs auto-delete the remote branch on merge but the local tracking ref and local branch stick around. This skill walks through the cleanup safely so nothing important gets lost.
Workflow
Step 1: Refresh remote state
Start by fetching and pruning so the local view of remote branches is
accurate. Without this, you'd see stale origin/* refs for branches
GitHub already deleted on merge.
git fetch origin --prune
Step 2: Detect the default branch
Don't assume main — check:
git symbolic-ref -q --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@'
The -q flag suppresses the fatal error when origin/HEAD isn't set, and
--short strips the refs/remotes/ prefix. If that fails, fall back to
checking whether main or master exists locally. Use the result as
$DEFAULT for the rest of the workflow.
Step 3: Categorize every branch
Run these four commands and cross-reference the results:
| Command | Purpose |
|---|---|
git branch -vv |
Local branches with tracking status and ahead/behind |
git branch --merged origin/$DEFAULT |
Local branches fully merged into the default branch |
git branch -r --merged origin/$DEFAULT |
Remote branches fully merged |
git branch -r --no-merged origin/$DEFAULT |
Remote branches with unmerged work (context only) |
From these results, build four categories:
- Merged local branches — appear in
--mergedoutput, excluding the default branch itself. These are safe to delete. - Stale remote branches — appear in
-r --mergedoutput, excludingorigin/HEADandorigin/$DEFAULT. These are safe to delete from the remote. - Orphaned local branches —
git branch -vvshows their tracking remote asgonebut they are NOT in the--mergedlist. These contain unmerged work and need the user's attention. - Active unmerged branches — everything else. Leave these alone.
Step 4: Present the summary
Show the user a clear, categorized summary before doing anything destructive:
**Safe to delete — N local branches merged into $DEFAULT:**
- branch-name-1
- branch-name-2
**Safe to delete — M remote branches merged into $DEFAULT:**
- origin/branch-name-1 → delete as `branch-name-1`
- origin/branch-name-2 → delete as `branch-name-2`
(or: "All merged remote branches were already pruned by GitHub.")
**Needs attention — K orphaned branches (remote gone, not merged):**
- branch-name (last commit: <short msg>)
**Keeping — J active unmerged branches:**
- branch-name → origin/branch-name
If any category is empty, say so briefly (e.g., "No orphaned branches.") rather than omitting the section — the user should see that every category was checked.
Wait for the user to confirm before proceeding to deletion.
Step 5: Delete confirmed branches
Once the user confirms:
Local merged branches:
git branch -d branch1 branch2 branch3
Use -d (lowercase) which refuses to delete unmerged branches as a safety
net. Never use -D unless the user explicitly asks to force-delete an
unmerged branch.
Remote merged branches:
git push origin --delete branch1 branch2 branch3
Use bare branch names — strip the origin/ prefix. These may fail with
"remote ref does not exist" if GitHub already deleted them on PR merge.
That's expected — just note it and move on.
Final prune to clean up any remaining stale tracking refs:
git fetch origin --prune
Step 6: Verify and report
Show the final branch state:
git branch -vv
Summarize what was done: how many local branches deleted, how many remote branches deleted, how many pruned, and what remains.
Protected Branches
Never delete these, even if they appear in a merged list:
- The default branch (
main,master, or whatever$DEFAULTresolved to) develop,staging,production(if they exist)- Any branch the user explicitly asks to keep
Edge Cases
- No branches to clean up — if everything is already tidy, say so.
- Permission errors on remote delete — report the error clearly and suggest the user delete the branch via the GitHub UI or check permissions.
- Large number of branches — if there are more than ~20 branches in any category, list them all but consider grouping by prefix (e.g., "12 feature/* branches, 5 chore/* branches").