Branch Cleanup
Delete merged branches (local and optionally remote) with explicit user confirmation, and flag stale unmerged branches for manual review.
Auto-Invoke Triggers
This skill activates when:
- Keywords: "cleanup branches", "delete merged branches", "prune old branches", "remove stale branches", "branch cleanup", "remove dead branches"
- Command:
/cleanup-branches
Arguments
--base <branch>— Base branch for merge check (default: main)--threshold <months>— Inactivity threshold for stale detection (default: 3)--remote— Include remote branch deletion--dry-run— Show what would be deleted without acting
Safety Model
- Merged branches: Deletable after explicit user confirmation
- Unmerged branches: Never auto-deleted — reported with manual commands only
- Dry-run: Available via
--dry-runflag to preview actions - Confirmation: Before each destructive step, list branches and ask the user
Workflow
Execute each step below using the Bash tool.
Step 1: Validate Git Repository
git rev-parse --is-inside-work-tree 2>/dev/null || echo "NOT_A_GIT_REPO"
If not a git repo, stop and inform the user.
Step 2: Parse Arguments
Parse $ARGUMENTS for:
--base BRANCH→ set BASE_BRANCH=BRANCH (default: main)--threshold N→ set THRESHOLD_MONTHS=N (default: 3)--remote→ set INCLUDE_REMOTE=true (default: false)--dry-run→ set DRY_RUN=true (default: false)
Verify the base branch exists:
git rev-parse --verify "$BASE_BRANCH" 2>/dev/null || echo "BASE_BRANCH_NOT_FOUND"
If the base branch doesn't exist, try master as fallback. If neither exists, stop and inform the user.
Step 3: Fetch Latest Remote State
if ! git fetch --prune 2>/dev/null; then
echo "Warning: Could not reach remote. Remote branch data may be stale."
fi
Step 4: Display Branch Status Summary
current_branch=$(git branch --show-current)
total_local=$(git branch | wc -l | tr -d ' ')
total_remote=$(git branch -r | grep -v HEAD | wc -l | tr -d ' ')
remote=$(git config --get "branch.$BASE_BRANCH.remote" 2>/dev/null || echo "origin")
merged_local=$(git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | wc -l | tr -d ' ')
merged_remote=$(git branch -r --merged "$remote/$BASE_BRANCH" | grep -v "$remote/$BASE_BRANCH" | grep -v "$remote/HEAD" | wc -l | tr -d ' ')
echo "=== BRANCH STATUS ==="
echo "Current branch: $current_branch"
echo "Base branch: $BASE_BRANCH"
echo "Local branches: $total_local ($merged_local merged into $BASE_BRANCH)"
echo "Remote branches: $total_remote ($merged_remote merged into $BASE_BRANCH)"
Present this summary to the user.
Step 5: Local Merged Branch Cleanup
List local branches merged into base (excluding base and current branch):
git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | while IFS= read -r branch; do
branch="${branch## }"
last_commit=$(git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1)
echo " $branch (last commit: ${last_commit:-unknown})"
done
Count:
merged_count=$(git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | wc -l | tr -d ' ')
if [ "$merged_count" -eq 0 ]; then
echo " (none)"
fi
echo "Found $merged_count local merged branch(es)"
If merged branches exist and not --dry-run:
Ask the user for confirmation using natural conversation: "These N branches are merged into BASE_BRANCH. Delete them?"
If confirmed, delete each branch:
git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | while IFS= read -r branch; do
branch="${branch## }"
git branch -d "$branch"
done
If --dry-run: Display what would be deleted but skip the deletion.
Step 6: Remote Merged Branch Cleanup (if --remote)
Only execute if --remote flag was provided.
List remote branches merged into base:
git branch -r --merged "$remote/$BASE_BRANCH" | grep -v "$remote/$BASE_BRANCH" | grep -v "$remote/HEAD" | while IFS= read -r branch; do
branch="${branch## }"
short_name="${branch#$remote/}"
last_commit=$(git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1)
echo " $short_name (last commit: ${last_commit:-unknown})"
done
Count:
remote_merged=$(git branch -r --merged "$remote/$BASE_BRANCH" | grep -v "$remote/$BASE_BRANCH" | grep -v "$remote/HEAD" | wc -l | tr -d ' ')
if [ "$remote_merged" -eq 0 ]; then
echo " (none)"
fi
echo "Found $remote_merged remote merged branch(es)"
If remote merged branches exist and not --dry-run:
Ask the user for confirmation: "These N remote branches are merged. Delete them from $remote?"
If confirmed, delete each remote branch:
git branch -r --merged "$remote/$BASE_BRANCH" | grep -v "$remote/$BASE_BRANCH" | grep -v "$remote/HEAD" | while IFS= read -r branch; do
branch="${branch## }"
short_name="${branch#$remote/}"
git push "$remote" --delete "$short_name"
done
If --dry-run: Display what would be deleted but skip the deletion.
Step 7: Stale Unmerged Branch Report
List inactive unmerged branches (past threshold) with ahead/behind counts. Never delete these — only display them.
Calculate threshold:
if [[ "$OSTYPE" == "darwin"* ]]; then
threshold=$(date -v-${THRESHOLD_MONTHS}m +%s)
else
threshold=$(date -d "${THRESHOLD_MONTHS} months ago" +%s)
fi
Scan for stale unmerged branches:
echo "=== STALE UNMERGED BRANCHES (manual review required) ==="
git for-each-ref --sort=committerdate --format='%(refname:short) %(committerdate:unix) %(committerdate:relative)' refs/heads/ | while IFS= read -r line; do
branch=$(echo "$line" | awk '{print $1}')
timestamp=$(echo "$line" | awk '{print $2}')
relative=$(echo "$line" | cut -d' ' -f3-)
[ "$branch" = "$BASE_BRANCH" ] && continue
if [[ "$timestamp" =~ ^[0-9]+$ ]] && [ "$timestamp" -lt "$threshold" ]; then
merged=$(git branch --merged "$BASE_BRANCH" | grep -w "$branch" | wc -l | tr -d ' ')
if [ "$merged" -eq 0 ]; then
counts=$(git rev-list --left-right --count "$BASE_BRANCH"..."$branch" 2>/dev/null)
behind=$(echo "$counts" | awk '{print $1}')
ahead=$(echo "$counts" | awk '{print $2}')
echo " $branch ($relative) [ahead $ahead, behind $behind]"
fi
fi
done
After listing, suggest manual deletion commands (but never execute them):
To delete these branches manually:
Local: git branch -D <branch>
Remote: git push origin --delete <branch>
Step 8: Summary Report
Present a summary of all actions taken:
=== CLEANUP SUMMARY ===
Local merged branches deleted: N
Remote merged branches deleted: N (or "skipped — use --remote")
Stale unmerged branches flagged: N (manual review)
Important Caveats
- Squash merges: Branches merged via squash-and-merge on GitHub will NOT appear as "merged" in
git branch --merged. They show as unmerged even though their changes are in the base branch. Check stale unmerged branches carefully. - Current branch: The current branch is never deleted, even if merged.
- Protected branches:
main,master, and the base branch are always excluded from deletion. - Remote permissions: Deleting remote branches requires push access to the remote.
Progressive Disclosure
For more details, see:
- WORKFLOW.md — Detailed 5-phase methodology
- EXAMPLES.md — Usage scenarios with sample output
- TROUBLESHOOTING.md — Common issues and solutions
Version
1.0.0