From git
Deletes merged Git branches locally and optionally remotely after user confirmation, flags stale unmerged branches for manual review. Supports dry-run, custom base branch, and inactivity threshold.
npx claudepluginhub joaquimscosta/arkhe-claude-plugins --plugin gitThis skill uses the workspace's default tool permissions.
Delete merged branches (local and optionally remote) with explicit user confirmation, and flag stale unmerged branches for manual review.
Lists local and remote git branches for cleanup: merged into base (main/master) or inactive beyond configurable threshold (default 3 months).
Safely identifies and deletes merged and stale Git branches after fetching latest state, filtering by pattern, and user confirmation via interactive prompts. Protects main branches.
Automates git branch cleanup: inventories local/remote branches, identifies merged/gone candidates, protects main/develop/release/current, confirms deletions, prunes remotes, provides recovery SHAs.
Share bugs, ideas, or general feedback.
Delete merged branches (local and optionally remote) with explicit user confirmation, and flag stale unmerged branches for manual review.
This skill activates when:
/cleanup-branches--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--dry-run flag to preview actionsExecute each step below using the Bash tool.
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.
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.
if ! git fetch --prune 2>/dev/null; then
echo "Warning: Could not reach remote. Remote branch data may be stale."
fi
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.
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.
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.
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.
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>
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)
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.main, master, and the base branch are always excluded from deletion.For more details, see:
1.1.0