Arkhe-claude-plugins cleaning-up-branches
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.
git clone https://github.com/joaquimscosta/arkhe-claude-plugins
T=$(mktemp -d) && git clone --depth=1 https://github.com/joaquimscosta/arkhe-claude-plugins "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/git/skills/cleaning-up-branches" ~/.claude/skills/joaquimscosta-arkhe-claude-plugins-cleaning-up-branches && rm -rf "$T"
plugins/git/skills/cleaning-up-branches/SKILL.mdBranch 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 for merge check (default: main)--base <branch>
— Inactivity threshold for stale detection (default: 3)--threshold <months>
— Include remote branch deletion--remote
— Show what would be deleted without acting--dry-run
Safety Model
- Merged branches: Deletable after explicit user confirmation
- Unmerged branches: Never auto-deleted — reported with manual commands only
- Dry-run: Available via
flag to preview actions--dry-run - 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:
→ set BASE_BRANCH=BRANCH (default: main)--base BRANCH
→ set THRESHOLD_MONTHS=N (default: 3)--threshold N
→ set INCLUDE_REMOTE=true (default: false)--remote
→ set DRY_RUN=true (default: false)--dry-run
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
: Display what would be deleted but skip the deletion.--dry-run
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
: Display what would be deleted but skip the deletion.--dry-run
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
: Display what would be deleted but skip the deletion.--dry-run
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
(patch-id comparison). These requiregit cherry
(force delete) since git doesn't recognize them as merged. Edge cases: amended commits after squash or partial cherry-picks may not be detected.-D - Current branch: The current branch is never deleted, even if merged.
- Protected branches:
,main
, and the base branch are always excluded from deletion.master - 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