Cleaning up branches
Skill joaquimscosta/arkhe-claude-plugins/plugins/git/skills/cleaning-up-branches
Supercharge Claude Code with 109 specialized components — 22 agents, 32 commands, 55 skills across 13 modular plugins. Deep reasoning, autonomous dev loops, DDD architecture, design system enforcement, git automation, and more.
npx -y skills add joaquimscosta/arkhe-claude-plugins --skill cleaning-up-branchesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Deletes merged git branches (local and remote) and flags stale unmerged branches for manual review. Use when user mentions "cleanup branches", "delete merged branches", "prune old branches", "remove stale branches", "branch cleanup", or runs /cleanup-branches command.
SKILL.md
9.8 KB, as published. Nobody here has run it
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: Squash-Merged Branch Cleanup
Detect branches whose changes are already in base via squash-and-merge or rebase-merge. Uses git cherry to compare patch-ids.
echo "=== SQUASH-MERGED BRANCHES ==="
squash_branches=""
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
[ "$branch" = "$BASE_BRANCH" ] && continue
current=$(git branch --show-current)
[ "$branch" = "$current" ] && continue
# Skip branches already detected as merged
merged=$(git branch --merged "$BASE_BRANCH" | grep -w "$branch" | wc -l | tr -d ' ')
[ "$merged" -gt 0 ] && continue
# Count commits on branch since merge-base
merge_base=$(git merge-base "$BASE_BRANCH" "$branch" 2>/dev/null)
[ -z "$merge_base" ] && continue
unique_commits=$(git log --oneline "$merge_base".."$branch" --no-merges 2>/dev/null | wc -l | tr -d ' ')
[ "$unique_commits" -eq 0 ] && continue
# git cherry: + means NOT in base, - means equivalent exists in base
unpicked=$(git cherry "$BASE_BRANCH" "$branch" 2>/dev/null | grep '^+' | wc -l | tr -d ' ')
if [ "$unpicked" -eq 0 ]; then
relative=$(git log -1 --format='%cr' "$branch")
echo " $branch ($relative)"
squash_branches="$squash_branches $branch"
fi
done
squash_count=$(echo "$squash_branches" | wc -w | tr -d ' ')
if [ "$squash_count" -eq 0 ]; then
echo " (none)"
fi
echo "Found $squash_count squash-merged branch(es)"
If squash-merged branches exist and not --dry-run:
Ask the user for confirmation: "These N branches were squash-merged into BASE_BRANCH (verified via git cherry). Delete them?"
If confirmed, delete each branch. Note: must use -D (force) since git doesn't recognize squash merges as merged:
for branch in $squash_branches; do
git branch -D "$branch"
done
If --dry-run: Display what would be deleted but skip the deletion.
Step 7: 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 8: 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-)
# Skip base branch and squash-merged branches (already handled in Step 6)
[ "$branch" = "$BASE_BRANCH" ] && continue
echo "$squash_branches" | grep -qw "$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 9: Summary Report
Present a summary of all actions taken:
=== CLEANUP SUMMARY ===
Local merged branches deleted: N
Squash-merged branches deleted: N
Remote merged branches deleted: N (or "skipped — use --remote")
Stale unmerged branches flagged: N (manual review)
Important Caveats
- Squash merges: Detected automatically using
git cherry(patch-id comparison). These require-D(force delete) since git doesn't recognize them as merged. Edge cases: amended commits after squash or partial cherry-picks may not be detected. - 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.1.0