cleaning-up-branches
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
More from joaquimscosta/arkhe-claude-plugins
skill-validator
Validate skills against Anthropic best practices for frontmatter, structure, content, file organization, hooks, MCP, and security (62 rules in 8 categories). Use when creating new skills, updating existing skills, before publishing skills, reviewing skill quality, or when user mentions "validate skill", "check skill", "skill best practices", "skill review", or "lint skill".
30domain-driven-design
Expert guidance for Domain-Driven Design architecture and implementation. Use when designing complex business systems, defining bounded contexts, structuring domain models, choosing between modular monolith vs microservices, implementing aggregates/entities/value objects, or when users mention "DDD", "domain-driven design", "bounded context", "aggregate", "domain model", "ubiquitous language", "event storming", "context mapping", "domain events", "anemic domain model", strategic design, tactical patterns, or domain modeling. Helps make architectural decisions, identify subdomains, design aggregates, and avoid common DDD pitfalls.
26code-explanation
Explains complex code through clear narratives, visual diagrams, and step-by-step breakdowns. Use when user asks to explain code, understand algorithms, analyze design patterns, wants code walkthroughs, or mentions "explain this code", "how does this work", "code breakdown", or "understand this function".
22generating-changelog
Analyzes git commit history and generates professional changelogs with semantic versioning, conventional commit support, and multiple output formats (Keep a Changelog, Conventional, GitHub). Use when editing CHANGELOG.md, CHANGELOG.txt, or HISTORY.md files, preparing release notes, creating releases, bumping versions, updating changelog, documenting changes, writing release notes, tracking changes, version bump, tag release, or when user mentions "changelog", "release notes", "version history", "release", "semantic versioning", or "conventional commits".
21workflow-orchestration
>
19generating-stitch-screens
>
19