
Cleaning Up Branches
- 17 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Helps with ai & agent building tasks.
About
cleaning-up-branches is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- cleaning-up-branches
- AI & Agent Building
- AI-coding skill
Cleaning Up Branches by the numbers
- 17 all-time installs (skills.sh)
- Ranked #10,886 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joaquimscosta/arkhe-claude-plugins --skill cleaning-up-branchesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 5, 2026 |
| Repository | joaquimscosta/arkhe-claude-plugins ↗ |
What it does
Helps with ai & agent building tasks.
Files
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:
1. Keywords: "cleanup branches", "delete merged branches", "prune old branches", "remove stale branches", "branch cleanup", "remove dead branches" 2. 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."
fiStep 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})"
doneCount:
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"
doneIf `--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"
doneIf `--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})"
doneCount:
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"
doneIf `--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)
fiScan 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
doneAfter 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
Branch Cleanup Examples
Usage scenarios for the cleaning-up-branches skill.
Example 1: Default Usage (Local Merged Only)
Input:
/cleanup-branchesOutput:
=== BRANCH STATUS ===
Current branch: main
Base branch: main
Local branches: 8 (3 merged into main)
Remote branches: 12 (5 merged into main)
=== LOCAL MERGED BRANCHES ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
chore/003-deps (last commit: 2025-07-20)
Found 3 local merged branch(es)Claude asks: _"These 3 branches are merged into main. Delete them?"_
User confirms → branches deleted:
→ Deleted: feat/001-user-auth
→ Deleted: fix/002-login-bug
→ Deleted: chore/003-deps
=== STALE UNMERGED BRANCHES (manual review required) ===
experiment/old-feature (8 months ago) [ahead 3, behind 42]
To delete these branches manually:
Local: git branch -D <branch>
Remote: git push origin --delete <branch>
=== CLEANUP SUMMARY ===
Local merged branches deleted: 3
Remote merged branches deleted: skipped — use --remote
Stale unmerged branches flagged: 1 (manual review)Example 2: Including Remote Branches
Input:
/cleanup-branches --remoteOutput (after local cleanup):
=== REMOTE MERGED BRANCHES ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
chore/003-deps (last commit: 2025-07-20)
docs/004-readme (last commit: 2025-06-10)
refactor/005-api (last commit: 2025-08-22)
Found 5 remote merged branch(es)Claude asks: _"These 5 remote branches are merged. Delete them from origin?"_
User confirms:
→ Deleted remote: feat/001-user-auth
→ Deleted remote: fix/002-login-bug
→ Deleted remote: chore/003-deps
→ Deleted remote: docs/004-readme
→ Deleted remote: refactor/005-api
=== CLEANUP SUMMARY ===
Local merged branches deleted: 3
Remote merged branches deleted: 5
Stale unmerged branches flagged: 1 (manual review)Example 3: Custom Base Branch and Threshold
Input:
/cleanup-branches --base develop --threshold 1Output:
=== BRANCH STATUS ===
Current branch: feat/006-new-feature
Base branch: develop
Local branches: 10 (4 merged into develop)
Remote branches: 15 (6 merged into develop)
=== LOCAL MERGED BRANCHES ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
chore/003-deps (last commit: 2025-07-20)
hotfix/007-crash (last commit: 2025-09-10)
Found 4 local merged branch(es)With --threshold 1, more branches appear as stale (1 month inactivity vs default 3).
Example 4: Dry-Run Mode
Input:
/cleanup-branches --remote --dry-runOutput:
=== BRANCH STATUS ===
Current branch: main
Base branch: main
Local branches: 8 (3 merged into main)
Remote branches: 12 (5 merged into main)
[DRY RUN] Would delete 3 local merged branches:
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
chore/003-deps (last commit: 2025-07-20)
[DRY RUN] Would delete 5 remote merged branches:
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
chore/003-deps (last commit: 2025-07-20)
docs/004-readme (last commit: 2025-06-10)
refactor/005-api (last commit: 2025-08-22)
=== STALE UNMERGED BRANCHES (manual review required) ===
experiment/old-feature (8 months ago) [ahead 3, behind 42]
=== CLEANUP SUMMARY ===
[DRY RUN] No branches were deleted.
Local merged branches that would be deleted: 3
Remote merged branches that would be deleted: 5
Stale unmerged branches flagged: 1 (manual review)Example 5: Natural Language Trigger
User says: "Can you clean up the old merged branches in this repo?"
The skill auto-invokes because of the trigger phrase "clean up ... merged branches". Claude runs the cleanup workflow with default settings (local only, 3-month threshold, main as base).
User says: "Delete all the stale branches including remote ones"
The skill auto-invokes with --remote inferred from "including remote ones".
Example 6: No Cleanup Needed
Input:
/cleanup-branchesOutput:
=== BRANCH STATUS ===
Current branch: main
Base branch: main
Local branches: 2 (0 merged into main)
Remote branches: 3 (0 merged into main)
No local merged branches to delete.
No stale unmerged branches found.
=== CLEANUP SUMMARY ===
Local merged branches deleted: 0
Remote merged branches deleted: skipped — use --remote
Stale unmerged branches flagged: 0
All branches are active and up to date.Example 7: Squash-Merged Branch Cleanup
Input:
/cleanup-branchesOutput:
=== BRANCH STATUS ===
Current branch: main
Base branch: main
Local branches: 21 (1 merged into main)
Remote branches: 15 (3 merged into main)
=== LOCAL MERGED BRANCHES ===
feat/003-roadmap-critic (last commit: 2025-12-10)
Found 1 local merged branch(es)Claude asks: _"This branch is merged into main. Delete it?"_ User confirms.
→ Deleted: feat/003-roadmap-critic
=== SQUASH-MERGED BRANCHES ===
feat/007-rfc-skills-migration (2 weeks ago)
chore/005-reference-doc-sync (3 weeks ago)
feat/003-icon-forge (4 weeks ago)
assets/add-plugin-banners (4 weeks ago)
fix/merge-duplicate-checkpoints (4 weeks ago)
Found 5 squash-merged branch(es) (verified via git cherry)Claude asks: _"These 5 branches were squash-merged into main (verified via git cherry). Delete them?"_ User confirms.
→ Deleted: feat/007-rfc-skills-migration
→ Deleted: chore/005-reference-doc-sync
→ Deleted: feat/003-icon-forge
→ Deleted: assets/add-plugin-banners
→ Deleted: fix/merge-duplicate-checkpoints
=== STALE UNMERGED BRANCHES (manual review required) ===
backup-before-rewrite-20251127 (4 months ago) [ahead 56, behind 120]
To delete these branches manually:
Local: git branch -D <branch>
Remote: git push origin --delete <branch>
=== CLEANUP SUMMARY ===
Local merged branches deleted: 1
Squash-merged branches deleted: 5
Remote merged branches deleted: skipped — use --remote
Stale unmerged branches flagged: 1 (manual review)Note: Squash-merged branches use git branch -D (force delete) since git doesn't recognize them as merged. The git cherry verification ensures they're safe to delete.
Version
1.1.0
Branch Cleanup Troubleshooting
Common issues and solutions for the cleaning-up-branches skill.
Cannot Delete Current Branch
Error:
error: Cannot delete branch 'feat/001-user-auth' checked out at '/path/to/repo'Cause: You are currently on the branch you're trying to delete.
Solution:
# Switch to the base branch first
git checkout main
# Then run cleanup again
/cleanup-branchesThe skill automatically excludes the current branch from deletion candidates, but if you manually attempt to delete it, git will refuse.
Remote Branch Deletion Permission Denied
Error:
remote: Permission to org/repo.git denied
fatal: unable to delete 'feat/001-user-auth': remote ref does not existCause: You don't have push access to the remote, or the branch was already deleted on the remote.
Solution: 1. Verify you have push access:
gh repo view --json viewerPermission2. If the branch was already deleted remotely, run:
git fetch --prune3. If you lack permissions, ask a repository admin to delete the remote branches.
Squash-Merged Branch Not Detected Automatically
Symptom: A branch shows up as "stale unmerged" even though its changes were merged via squash-and-merge on GitHub. It does NOT appear in the "SQUASH-MERGED BRANCHES" section.
Note: As of v1.1.0, most squash-merged branches are detected automatically using git cherry (patch-id comparison). However, edge cases exist.
Cause of false negatives:
- The squash commit on base was amended after merge, changing the patch-id
- Only some commits were cherry-picked (partial merge)
- The branch was rebased and modified before squash-merge
Solution: Verify manually and force-delete if confirmed:
# Check git cherry output — all '-' lines means squash-merged
git cherry main <branch-name>
# Cross-reference with GitHub
gh pr list --state merged --head <branch-name>
# If confirmed squash-merged, safe to delete
git branch -D <branch-name> # Force-delete local
git push origin --delete <branch-name> # Delete remoteProtected Branch Errors
Error:
error: refusing to delete the current branch 'main'Cause: Attempting to delete a protected branch (main, master, or the specified base branch).
Solution: The skill automatically excludes main, master, and the --base branch from deletion. If you see this error, it indicates an edge case — report it as a bug.
Base Branch Not Found
Error:
fatal: Not a valid object name: 'main'Cause: The specified base branch doesn't exist in the repository.
Solution:
# Check available branches
git branch -a
# Use the correct base branch
/cleanup-branches --base develop
/cleanup-branches --base masterNetwork Errors During Remote Operations
Error:
fatal: unable to access 'https://github.com/org/repo.git/': Could not resolve hostCause: No network access or the remote URL is incorrect.
Solution: 1. Check network connectivity 2. Verify the remote URL:
git remote -v3. Run without --remote to clean up local branches only:
/cleanup-branchesxargs: Argument List Too Long
Error:
xargs: argument list too longCause: Extremely large number of branches to delete.
Solution: Delete branches in batches:
# Delete first 50 merged branches
git branch --merged main | grep -v main | head -50 | xargs git branch -d
# Repeat as neededBranch Deleted Locally But Still Shows on Remote
Symptom: After deleting local merged branches, you still see them on the remote.
Cause: Local deletion does not affect remote branches. The --remote flag is required for remote cleanup.
Solution:
/cleanup-branches --remoteVersion
1.1.0
Branch Cleanup Workflow
Step-by-step methodology for deleting merged branches and flagging stale unmerged branches.
Overview
The branch cleanup skill follows a 5-phase workflow: 1. Environment Validation — Verify git repository, parse arguments, fetch remote state 2. Local Merged Branch Cleanup — Delete local branches merged into base (with confirmation) 3. Remote Merged Branch Cleanup — Delete remote merged branches (with confirmation, if --remote) 4. Stale Unmerged Branch Report — Flag inactive unmerged branches (read-only) 5. Summary & Audit Trail — Report all actions taken
Phase 1: Environment Validation
Git Repository Check
git rev-parse --is-inside-work-tree 2>/dev/nullIf this fails, the user is not in a git repository. Stop and inform them.
Argument Parsing
Parse the $ARGUMENTS string for four optional flags:
| Flag | Variable | Default | Description |
|---|---|---|---|
--base BRANCH | BASE_BRANCH | main | Base branch for merge check |
--threshold N | THRESHOLD_MONTHS | 3 | Months of inactivity |
--remote | INCLUDE_REMOTE | false | Include remote branch deletion |
--dry-run | DRY_RUN | false | Preview without deleting |
Base Branch Validation
git rev-parse --verify "$BASE_BRANCH" 2>/dev/null
# Fallback: try master if main doesn't exist
git rev-parse --verify "master" 2>/dev/nullIf neither main nor master exists and no --base was specified, stop and ask the user which branch to use.
Fetch Remote State
git fetch --prune 2>/dev/nullThis prunes stale remote-tracking refs and ensures merge status checks are accurate.
Status Summary
current_branch=$(git branch --show-current)
total_local=$(git branch | wc -l | tr -d ' ')
merged_local=$(git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -v "$BASE_BRANCH" | wc -l | tr -d ' ')Present the overview before proceeding with destructive operations.
Output: Validated environment with parsed arguments and branch summary.
Phase 2: Local Merged Branch Cleanup
Purpose
Delete local branches that have been merged into the base branch. These branches are safe to delete because their changes are already in the base.
Discovery
git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -v "$BASE_BRANCH"Enrichment
For each merged branch, show last commit date:
git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1Confirmation
Before deleting, present the full list to the user and ask for explicit confirmation via natural conversation.
Dry-run mode: Display the list with "[DRY RUN] Would delete:" prefix and skip deletion.
Deletion
# Safe delete — only works on merged branches
git branch -d "$branch"Using -d (lowercase) ensures git refuses to delete unmerged branches.
Output Format
=== LOCAL MERGED BRANCHES ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
Found 2 local merged branch(es)
Delete these 2 branches? [Confirm/Skip]
→ Deleted: feat/001-user-auth
→ Deleted: fix/002-login-bugOutput: List of deleted branches or skip confirmation.
Phase 2.5: Squash-Merged Branch Cleanup
Purpose
Detect and delete branches whose changes are already in the base branch via squash-and-merge or rebase-merge. These branches appear as "unmerged" to git branch --merged because squash merges create a new commit rather than preserving branch commits as ancestors.
Detection: git cherry
git cherry compares patch-ids (content-based hashes of diffs) to determine if a branch's commits have equivalents in the base branch:
-prefix: Equivalent patch exists in base (change is already in base)+prefix: No equivalent patch (change is NOT in base)
If ALL commits show -, the branch is squash-merged and safe to delete.
Process
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
# Skip base, current, and already-merged branches
# Compute merge-base, count unique commits
# Run git cherry — if unpicked=0, branch is squash-merged
unpicked=$(git cherry "$BASE_BRANCH" "$branch" | grep '^+' | wc -l | tr -d ' ')
doneSafety Model
Squash-merged branches require special handling:
- Cannot use `git branch -d`: Git doesn't recognize them as merged, so
-d(safe delete) will refuse - Must use `git branch -D`: Force delete is required
- User confirmation is mandatory: Always present the list and ask before deleting
- Dry-run support:
--dry-runshows "[DRY RUN] Would delete:" prefix
Confirmation
Present the full list with verification context:
=== SQUASH-MERGED BRANCHES ===
feat/007-rfc-skills-migration (2 weeks ago)
chore/005-reference-doc-sync (3 weeks ago)
Found 2 squash-merged branch(es) (verified via git cherry)
Delete these 2 squash-merged branches? [Confirm/Skip]Deletion
git branch -D "$branch" # Force delete requiredEdge Cases
- Partial squash: Mix of
+and-ingit cherryoutput — NOT flagged as squash-merged - Amended commits: If squash commit was amended after merge, patch-ids may not match — remains undetected
- Rebase merges: Also detected (same mechanism) — correct behavior
Output: List of deleted squash-merged branches or skip confirmation.
Phase 3: Remote Merged Branch Cleanup
Trigger
Only executed when --remote flag is provided.
Discovery
git branch -r --merged "origin/$BASE_BRANCH" | grep -v "origin/$BASE_BRANCH" | grep -v "origin/HEAD"Confirmation
Present the full list and ask for explicit confirmation. Remote deletions are irreversible (the branch can be recreated from reflog within a limited window, but is effectively gone).
Deletion
# Strip origin/ prefix and delete from remote
git push origin --delete "$branch_name"Output Format
=== REMOTE MERGED BRANCHES ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-01)
Found 2 remote merged branch(es)
Delete these 2 remote branches from origin? [Confirm/Skip]
→ Deleted remote: feat/001-user-auth
→ Deleted remote: fix/002-login-bugOutput: List of deleted remote branches or skip confirmation.
Phase 4: Stale Unmerged Branch Report
Purpose
Identify branches NOT merged into base with no commits within the threshold period. These are flagged for manual review only — never auto-deleted.
Threshold Calculation
Cross-platform epoch timestamp:
# macOS (BSD date)
if [[ "$OSTYPE" == "darwin"* ]]; then
threshold=$(date -v-${THRESHOLD_MONTHS}m +%s)
# Linux (GNU date)
else
threshold=$(date -d "${THRESHOLD_MONTHS} months ago" +%s)
fiScanning
git for-each-ref --sort=committerdate \
--format='%(refname:short) %(committerdate:unix) %(committerdate:relative)' \
refs/heads/Filtering
For each branch: 1. Skip the base branch and current branch 2. Check timestamp against threshold (must be older) 3. Verify branch is NOT merged into base 4. Calculate ahead/behind divergence counts
Output Format
=== STALE UNMERGED BRANCHES (manual review required) ===
experiment/old-feature (8 months ago) [ahead 3, behind 42]
spike/prototype (5 months ago) [ahead 1, behind 28]
To delete these branches manually:
Local: git branch -D <branch>
Remote: git push origin --delete <branch>Output: Read-only report with manual deletion suggestions.
Phase 5: Summary & Audit Trail
Summary Report
=== CLEANUP SUMMARY ===
Local merged branches deleted: 2
Remote merged branches deleted: 2 (or "skipped — use --remote")
Stale unmerged branches flagged: 3 (manual review)Audit Context
Include the parameters used for the cleanup:
- Base branch checked against
- Inactivity threshold applied
- Whether remote was included
- Whether dry-run mode was active
Output: Complete cleanup summary.
Workflow Diagram
+---------------------------+
| 1. Environment Validation |
| - Git repo check |
| - Parse arguments |
| - Validate base branch |
| - Fetch & prune remote |
| - Branch status summary |
+-----------+---------------+
|
v
+---------------------------+
| 2. Local Merged Cleanup |
| - List merged branches |
| - Show last commit dates |
| - ASK USER CONFIRMATION |
| - Delete with git -d |
+-----------+---------------+
|
v
+-----------------------------+
| 2.5 Squash-Merged Cleanup |
| - git cherry per branch |
| - Compare patch-ids |
| - ASK USER CONFIRMATION |
| - Delete with git -D |
+-----------+-----------------+
|
v (if --remote)
+---------------------------+
| 3. Remote Merged Cleanup |
| - List remote merged |
| - Show last commit dates |
| - ASK USER CONFIRMATION |
| - Delete from origin |
+-----------+---------------+
|
v
+---------------------------+
| 4. Stale Unmerged Report |
| - Calculate threshold |
| - Scan for inactive |
| - Show ahead/behind |
| - Suggest manual cmds |
| - NEVER AUTO-DELETE |
+-----------+---------------+
|
v
+---------------------------+
| 5. Summary & Audit Trail |
| - Deleted counts |
| - Flagged counts |
| - Parameters used |
+---------------------------+Error Handling
Cannot Delete Current Branch
Error: Cannot delete branch 'feat/xxx' checked out at '/path'
→ Skip this branch and continue with others
→ Inform user to switch branches first if neededRemote Permission Denied
Error: permission denied to push to 'origin'
→ Stop remote deletion
→ Report which branches were not deleted
→ Suggest checking remote access permissionsNo Merged Branches Found
No local merged branches to delete.
→ Skip Phase 2
→ Continue to Phase 3 (remote) or Phase 4 (stale)Next Steps
- See
EXAMPLES.mdfor real-world usage scenarios - See
TROUBLESHOOTING.mdfor common issues and solutions
Version
1.1.0