
Listing Stale Branches
- 17 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Helps with ai & agent building tasks.
About
listing-stale-branches is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- listing-stale-branches
- AI & Agent Building
- AI-coding skill
Listing Stale Branches by the numbers
- 17 all-time installs (skills.sh)
- Ranked #10,861 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 listing-stale-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
Stale Branch Detection
Identify git branches that are candidates for cleanup: merged-but-not-deleted and inactive branches with no recent commits.
Auto-Invoke Triggers
This skill automatically activates when:
1. Keywords: "stale branches", "old branches", "branch cleanup", "prune branches", "dead branches", "unused branches", "inactive branches", "branch hygiene" 2. Actions: "list branches to delete", "find stale branches", "clean up branches"
Arguments
--threshold <months>— Inactivity threshold in months (default: 3)--base <branch>— Base branch for merge check (default: main)--remote— Include remote branch analysis
Workflow
Execute each step below using the Bash tool. This is a read-only skill — never delete branches, only report findings.
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:
--threshold N→ set THRESHOLD_MONTHS=N (default: 3)--base BRANCH→ set BASE_BRANCH=BRANCH (default: main)--remote→ set INCLUDE_REMOTE=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: Calculate Inactivity Threshold
Cross-platform threshold date (epoch seconds):
# macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
threshold=$(date -v-${THRESHOLD_MONTHS}m +%s)
else
# Linux
threshold=$(date -d "${THRESHOLD_MONTHS} months ago" +%s)
fi
echo "Threshold date (epoch): $threshold"Step 4: List Merged Branches
Find local branches already merged into the base branch (safe to delete):
echo "=== MERGED BRANCHES (safe to delete) ==="
merged_count=$(git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | wc -l | tr -d ' ')
git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -vw "$BASE_BRANCH" | while IFS= read -r branch; do
branch="${branch## }"
last_commit_date=$(git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1)
echo " $branch (last commit: ${last_commit_date:-unknown})"
done
if [ "$merged_count" -eq 0 ]; then
echo " (none)"
fi
echo "Total merged: $merged_count"Step 5: Detect Squash-Merged Branches
Detect branches whose changes are already in base via squash-and-merge or rebase-merge. Uses git cherry to compare patch-ids — if all commits have equivalents in base, the branch is squash-merged and safe to delete.
echo "=== SQUASH-MERGED BRANCHES (safe to delete) ==="
squash_count=0
squash_list=""
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
[ "$branch" = "$BASE_BRANCH" ] && 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_count=$((squash_count + 1))
squash_list="$squash_list|$branch"
fi
done
if [ "$squash_count" -eq 0 ]; then
echo " (none)"
fi
echo "Total squash-merged: $squash_count"Step 6: List Inactive Unmerged Branches
Find local branches NOT merged into base with no commits within the threshold period. Include ahead/behind counts:
echo "=== INACTIVE UNMERGED BRANCHES (review before delete) ==="
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, current branch, and squash-merged branches
[ "$branch" = "$BASE_BRANCH" ] && continue
echo "$squash_list" | grep -qw "$branch" && continue
# Check if branch is inactive (older than threshold)
if [[ "$timestamp" =~ ^[0-9]+$ ]] && [ "$timestamp" -lt "$threshold" ]; then
# Check if NOT merged
merged=$(git branch --merged "$BASE_BRANCH" | grep -w "$branch" | wc -l | tr -d ' ')
if [ "$merged" -eq 0 ]; then
# Get ahead/behind counts relative to base
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
doneStep 7: Remote Branch Analysis (if --remote)
Only execute this step if --remote flag was provided.
Detect the remote for the base branch and fetch:
remote=$(git config --get "branch.$BASE_BRANCH.remote" 2>/dev/null || echo "origin")
if ! git fetch --prune 2>/dev/null; then
echo "Warning: Could not reach remote. Skipping remote analysis."
fiIf the fetch warning was shown, skip the rest of Step 7. Otherwise, continue:
List remote merged branches:
echo "=== REMOTE MERGED BRANCHES ==="
remote_merged_count=$(git branch -r --merged "$BASE_BRANCH" | grep -v "HEAD" | grep -vw "$BASE_BRANCH" | wc -l | tr -d ' ')
git branch -r --merged "$BASE_BRANCH" | grep -v "HEAD" | grep -vw "$BASE_BRANCH" | while IFS= read -r branch; do
branch="${branch## }"
last_commit_date=$(git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1)
echo " $branch (last commit: ${last_commit_date:-unknown})"
done
if [ "$remote_merged_count" -eq 0 ]; then
echo " (none)"
fiList remote inactive unmerged branches:
echo "=== REMOTE INACTIVE UNMERGED BRANCHES ==="
git for-each-ref --sort=committerdate --format='%(refname:short) %(committerdate:unix) %(committerdate:relative)' "refs/remotes/$remote/" | grep -v "HEAD" | grep -vw "$BASE_BRANCH" | while IFS= read -r line; do
branch=$(echo "$line" | awk '{print $1}')
timestamp=$(echo "$line" | awk '{print $2}')
relative=$(echo "$line" | cut -d' ' -f3-)
if [[ "$timestamp" =~ ^[0-9]+$ ]] && [ "$timestamp" -lt "$threshold" ]; then
merged=$(git branch -r --merged "$BASE_BRANCH" | grep -w "$branch" | wc -l | tr -d ' ')
if [ "$merged" -eq 0 ]; then
echo " $branch ($relative)"
fi
fi
doneStep 8: Summary
Present a summary report:
current_branch=$(git branch --show-current)
total_local=$(git branch | wc -l | tr -d ' ')
merged_into_base=$(git branch --merged "$BASE_BRANCH" | grep -vw "$BASE_BRANCH" | wc -l | tr -d ' ')
echo "=== SUMMARY ==="
echo "Current branch: $current_branch"
echo "Base branch: $BASE_BRANCH"
echo "Inactivity threshold: $THRESHOLD_MONTHS months"
echo "Total local branches: $total_local"
echo "Merged into $BASE_BRANCH: $merged_into_base"
echo "Squash-merged (detected via git cherry): $squash_count"After the summary, suggest cleanup commands (but never execute them):
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete remote: git push origin --delete <branch>
Delete all merged: git branch --merged main | grep -v main | xargs git branch -dImportant Caveats
- Squash merges: Branches merged via squash-and-merge are detected using
git cherry(patch-id comparison). Edge cases where detection may fail: amended commits after squash, or partial cherry-picks. If a branch appears in "inactive unmerged" but you know it was squash-merged, verify withgit cherry main <branch>. - Read-only: This skill never deletes branches. Deletion commands are shown as suggestions only.
- Remote analysis: The
--remoteflag runsgit fetch --prunewhich contacts the remote. This requires network access.
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
Stale Branch Detection Examples
Real-world scenarios for identifying and cleaning up stale git branches.
Example 1: Default Usage (Local Branches Only)
Context
A developer wants to see which branches can be cleaned up in their project.
Command
/stale-branchesExecution
# Defaults: threshold=3 months, base=main, no remoteOutput
=== MERGED BRANCHES (safe to delete) ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-03)
chore/003-deps-update (last commit: 2025-07-22)
docs/004-api-guide (last commit: 2025-08-30)
Total merged: 4
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
experiment/cache-layer (6 months ago) [ahead 5, behind 89]
spike/graphql-prototype (4 months ago) [ahead 12, behind 67]
=== SUMMARY ===
Current branch: feat/010-dashboard
Base branch: main
Inactivity threshold: 3 months
Total local branches: 14
Merged into main: 4
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete all merged: git branch --merged main | grep -v main | xargs git branch -dKey Takeaways
- 4 merged branches are safe to delete immediately
- 2 unmerged branches need review — the
experiment/cache-layerbranch is 5 commits ahead and may have useful work
---
Example 2: Custom Threshold (1 Month)
Context
A team with frequent releases wants a stricter cleanup policy — anything inactive for 1+ month should be flagged.
Command
/stale-branches --threshold 1Output
=== MERGED BRANCHES (safe to delete) ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-03)
chore/003-deps-update (last commit: 2025-07-22)
docs/004-api-guide (last commit: 2025-08-30)
feat/005-search (last commit: 2025-10-01)
fix/006-sidebar (last commit: 2025-09-28)
Total merged: 6
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
experiment/cache-layer (6 months ago) [ahead 5, behind 89]
spike/graphql-prototype (4 months ago) [ahead 12, behind 67]
feat/007-notifications (2 months ago) [ahead 8, behind 15]
=== SUMMARY ===
Current branch: main
Base branch: main
Inactivity threshold: 1 month
Total local branches: 14
Merged into main: 6
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete all merged: git branch --merged main | grep -v main | xargs git branch -dKey Takeaways
- Lower threshold catches more branches (6 merged vs 4 with default)
feat/007-notificationsappears as inactive now — it's only 2 months old but exceeds the 1-month threshold- The team should decide if that branch is still being worked on
---
Example 3: Including Remote Branches
Context
A team lead wants to clean up both local and remote branches before a major release.
Command
/stale-branches --remoteOutput
=== MERGED BRANCHES (safe to delete) ===
feat/001-user-auth (last commit: 2025-08-15)
fix/002-login-bug (last commit: 2025-09-03)
Total merged: 2
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
experiment/cache-layer (6 months ago) [ahead 5, behind 89]
=== REMOTE MERGED BRANCHES ===
origin/feat/001-user-auth (last commit: 2025-08-15)
origin/fix/002-login-bug (last commit: 2025-09-03)
origin/chore/003-deps-update (last commit: 2025-07-22)
origin/docs/004-api-guide (last commit: 2025-08-30)
origin/feat/005-search (last commit: 2025-10-01)
=== REMOTE INACTIVE UNMERGED BRANCHES ===
origin/experiment/cache-layer (6 months ago)
origin/spike/graphql-prototype (4 months ago)
origin/feat/old-feature (8 months ago)
=== SUMMARY ===
Current branch: main
Base branch: main
Inactivity threshold: 3 months
Total local branches: 8
Merged into main: 2
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete remote: git push origin --delete <branch>
Delete all merged: git branch --merged main | grep -v main | xargs git branch -dKey Takeaways
- Remote has more stale branches than local (common when developers clean locally but forget remote)
origin/feat/old-featureonly exists on remote — someone pushed it but never cleaned up- The
git push origin --delete <branch>command is provided for remote cleanup
---
Example 4: Custom Base Branch
Context
A project uses develop as its integration branch instead of main. The developer wants to find branches merged into develop.
Command
/stale-branches --base developOutput
=== MERGED BRANCHES (safe to delete) ===
feat/sprint-12-auth (last commit: 2025-09-15)
feat/sprint-12-search (last commit: 2025-09-20)
fix/sprint-13-sidebar (last commit: 2025-10-05)
hotfix/login-crash (last commit: 2025-10-02)
Total merged: 4
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
feat/sprint-10-export (5 months ago) [ahead 23, behind 112]
experiment/new-ui (4 months ago) [ahead 45, behind 98]
=== SUMMARY ===
Current branch: feat/sprint-14-dashboard
Base branch: develop
Inactivity threshold: 3 months
Total local branches: 11
Merged into develop: 4
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete all merged: git branch --merged develop | grep -v develop | xargs git branch -dKey Takeaways
- Merge detection correctly uses
developas base feat/sprint-10-exporthas 23 commits ahead — significant unmerged work that should be reviewed- Cleanup commands reference
developinstead ofmain
---
Example 5: Combined Flags
Context
A strict cleanup before release: 1-month threshold, master base branch, including remotes.
Command
/stale-branches --threshold 1 --base master --remoteOutput
=== MERGED BRANCHES (safe to delete) ===
feature/user-profiles (last commit: 2025-09-18)
bugfix/header-overlap (last commit: 2025-10-01)
feature/dark-mode (last commit: 2025-09-25)
Total merged: 3
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
feature/abandoned-chat (3 months ago) [ahead 34, behind 78]
experimental/wasm-build (2 months ago) [ahead 7, behind 45]
=== REMOTE MERGED BRANCHES ===
origin/feature/user-profiles (last commit: 2025-09-18)
origin/bugfix/header-overlap (last commit: 2025-10-01)
origin/feature/dark-mode (last commit: 2025-09-25)
origin/release/v2.3.0 (last commit: 2025-08-30)
=== REMOTE INACTIVE UNMERGED BRANCHES ===
origin/feature/abandoned-chat (3 months ago)
origin/experimental/wasm-build (2 months ago)
=== SUMMARY ===
Current branch: master
Base branch: master
Inactivity threshold: 1 month
Total local branches: 9
Merged into master: 3
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete remote: git push origin --delete <branch>
Delete all merged: git branch --merged master | grep -v master | xargs git branch -dKey Takeaways
- All three flags work together seamlessly
origin/release/v2.3.0appears as remote-only merged branch — old release branchfeature/abandoned-chathas 34 unmerged commits — worth investigating before deleting
---
Example 6: Squash-Merged Branch Detection
Context
A project uses GitHub's "Squash and merge" strategy. After running /stale-branches, several branches that were already merged via squash don't appear as "merged". The skill now detects these automatically.
Command
/stale-branchesOutput
=== MERGED BRANCHES (safe to delete) ===
feat/003-roadmap-critic (last commit: 2025-12-10)
Total merged: 1
=== SQUASH-MERGED BRANCHES (safe to delete) ===
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)
Total squash-merged: 5
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
backup-before-rewrite-20251127 (4 months ago) [ahead 56, behind 120]
=== SUMMARY ===
Current branch: main
Base branch: main
Inactivity threshold: 3 months
Total local branches: 21
Merged into main: 1
Squash-merged (detected via git cherry): 5Key Takeaways
- 5 branches that
git branch --mergedmissed are now detected viagit cherrypatch-id comparison - These are safe to delete — all their code changes are already in
main - The
backup-before-rewrite-20251127branch has genuinely unmerged work (56 commits ahead) and is correctly NOT flagged as squash-merged
---
Auto-Invoke Example
User Message
"Which of my branches are stale? I want to clean up before the release."Skill Behavior
The skill auto-invokes because the user mentioned "stale" and "branches". It runs with default settings (3 months, main, local only) and presents the report. The user can then ask for adjustments like a different threshold or remote analysis.
Next Steps
- See
TROUBLESHOOTING.mdfor common issues and solutions
Version
1.1.0
Stale Branch Detection Troubleshooting
Common issues and solutions when listing stale branches.
Common Issues
Issue 1: "Not a git repository"
Symptom:
Error: fatal: not a git repository (or any of the parent directories): .gitCause:
- Not in a git-initialized directory
- Working in a directory outside any git repository
Solution:
# Verify you're in a git repository
git rev-parse --is-inside-work-tree
# Navigate to your project
cd /path/to/your/projectPrevention:
- Always run
/stale-branchesfrom within a git repository
---
Issue 2: Base branch does not exist
Symptom:
Error: Branch 'main' does not existCause:
- The repository uses
masterinstead ofmain(or another branch name) - The base branch was deleted or renamed
Solution:
# Check which branches exist
git branch -a
# Use the correct base branch
/stale-branches --base master
/stale-branches --base developPrevention:
- The skill automatically falls back to
masterifmaindoesn't exist - For non-standard base branches, always use
--base
---
Issue 3: Squash-merged branch not detected automatically
Symptom:
Branch 'feat/my-feature' appears in INACTIVE UNMERGED section
but was already merged via GitHub squash-and-mergeNote: As of v1.1.0, most squash-merged branches are detected automatically using git cherry (patch-id comparison) and shown in the "SQUASH-MERGED BRANCHES" section. However, edge cases exist where detection may fail.
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) —
git cherryshows a mix of+and- - The branch was rebased and modified before squash-merge, altering the diffs
Solution: If you know a branch was squash-merged but it wasn't detected, verify manually:
# Check git cherry output — all lines should show '-' for squash-merged
git cherry main feat/my-feature
# Alternative: check if PR was merged on GitHub
gh pr list --state merged --head feat/my-feature
# If confirmed squash-merged, safe to delete
git branch -D feat/my-featurePrevention:
- The automatic detection handles the vast majority of squash-merge cases
- For edge cases, cross-reference with
gh pr list --state merged
---
Issue 4: No stale branches found
Symptom:
No merged branches found.
No inactive unmerged branches found.
All branches are active and up to date.Cause:
- All branches are active (commits within the threshold period)
- All merged branches were already cleaned up
- Threshold is too generous (e.g., 12 months)
Solution:
# Try a shorter threshold
/stale-branches --threshold 1
# List all branches to see what exists
git branch -aPrevention:
- Start with the default 3-month threshold
- Use
--threshold 1for stricter cleanup policies
---
Issue 5: Remote fetch fails
Symptom:
Warning: Could not reach remote 'origin'
fatal: Could not read from remote repositoryCause:
- No network connectivity
- Remote URL is invalid or has changed
- SSH key or authentication issue
Solution:
# Check remote configuration
git remote -v
# Test connectivity
git ls-remote origin
# Fix SSH issues
ssh -T git@github.com
# Fix HTTPS authentication
gh auth loginPrevention:
- Ensure network access before using
--remote - The skill gracefully skips remote analysis if fetch fails and reports local results only
---
Issue 6: Too many branches listed
Symptom:
Output lists hundreds of branches, making it hard to reviewCause:
- Large team with many contributors
- Long-running project with accumulated branches
- No branch cleanup policy in place
Solution:
# Use a longer threshold to focus on truly stale branches
/stale-branches --threshold 6
# Focus on local only first (skip --remote)
/stale-branches --threshold 6
# Clean up merged branches first (safest)
git branch --merged main | grep -v main | xargs git branch -dPrevention:
- Establish a branch cleanup policy (e.g., delete after merge)
- Configure GitHub to auto-delete branches after PR merge
- Run
/stale-branchesregularly (e.g., monthly)
---
Issue 7: Current branch appears in results
Symptom:
The currently checked-out branch appears in the merged or inactive listCause:
- The current branch is merged into base or is inactive
- This is rare but possible if you're on an old branch
Solution: The skill filters out the current branch marker (*) from merged branch listings. If you still see it:
# Check which branch you're on
git branch --show-current
# Switch to base branch before running
git checkout main
/stale-branchesPrevention:
- The skill excludes branches marked with
*(current branch indicator) - Run from the base branch for cleanest results
---
Issue 8: Date calculation differs between macOS and Linux
Symptom:
Different results on macOS vs Linux for the same repository
Threshold calculation seems incorrectCause:
- macOS uses BSD
datecommand (date -v-3m) - Linux uses GNU
datecommand (date -d "3 months ago") - Slight differences in "3 months ago" calculation between implementations
Solution: The skill handles this automatically with platform detection:
if [[ "$OSTYPE" == "darwin"* ]]; then
threshold=$(date -v-${THRESHOLD_MONTHS}m +%s)
else
threshold=$(date -d "${THRESHOLD_MONTHS} months ago" +%s)
fiIf you suspect an issue:
# Verify the calculated threshold
echo "Threshold: $(date -r $threshold)" # macOS
echo "Threshold: $(date -d @$threshold)" # LinuxPrevention:
- The skill's cross-platform detection handles this automatically
- Results may differ by a few hours between platforms, which is negligible for month-level thresholds
---
Quick Diagnostics
Checklist Before Running
# 1. Verify git repository
git rev-parse --is-inside-work-tree
# 2. Check current directory
pwd
# 3. Check base branch exists
git rev-parse --verify main 2>/dev/null && echo "main exists" || echo "main NOT found"
git rev-parse --verify master 2>/dev/null && echo "master exists" || echo "master NOT found"
# 4. Count local branches
git branch | wc -l
# 5. Check remote connectivity (if using --remote)
git ls-remote origin 2>/dev/null && echo "Remote OK" || echo "Remote FAIL"Manual Branch Inspection
# See all branches with last commit dates
git for-each-ref --sort=-committerdate --format='%(refname:short) %(committerdate:relative)' refs/heads/
# Check if a specific branch is merged
git branch --merged main | grep "branch-name"
# Check divergence of a specific branch
git rev-list --left-right --count main...branch-nameGetting Help
Skill Documentation
SKILL.md— Overview, triggers, and inline workflowWORKFLOW.md— Detailed 5-phase methodologyEXAMPLES.md— Usage scenarios with sample output
External Resources
Version
1.1.0
Stale Branch Detection Workflow
Step-by-step methodology for identifying git branches that are candidates for cleanup.
Overview
The stale branch detection skill follows a 5-phase workflow: 1. Environment Validation — Verify git repository and parse arguments 2. Merged Branch Detection — Find branches already merged into base 3. Inactive Branch Detection — Find unmerged branches with no recent activity 4. Remote Analysis — Optionally analyze remote branches 5. Report Generation — Summarize findings with cleanup suggestions
Phase 1: Environment Validation
Git Repository Check
# Verify working directory is a git repo
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 three optional flags:
| Flag | Variable | Default | Description |
|---|---|---|---|
--threshold N | THRESHOLD_MONTHS | 3 | Months of inactivity |
--base BRANCH | BASE_BRANCH | main | Base branch for merge check |
--remote | INCLUDE_REMOTE | false | Include remote branches |
Base Branch Validation
# Check if base branch exists
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.
Threshold Calculation
Calculate the epoch timestamp for the inactivity cutoff. This must be cross-platform:
# 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)
fiOutput: Validated environment with threshold epoch, base branch name, and remote flag.
Phase 2: Merged Branch Detection
Purpose
Find branches that have been merged into the base branch but not deleted. These are always safe to delete.
Process
# List branches merged into base (excludes current and base branch)
git branch --merged "$BASE_BRANCH" | grep -v "^\*" | grep -v "$BASE_BRANCH"Enrichment
For each merged branch, fetch the last commit date for context:
git log -1 --format='%ci' "$branch" 2>/dev/null | cut -d' ' -f1This shows when the branch was last active, helping the user prioritize cleanup.
Output Format
=== MERGED BRANCHES (safe to delete) ===
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)
Total merged: 3Output: List of merged branches with last commit dates.
Phase 2.5: Squash-Merge Detection
Purpose
Detect 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 on the base branch rather than preserving the original branch commits as ancestors.
Mechanism: git cherry
git cherry uses git patch-id to compare the actual code changes (diffs) introduced by each commit, ignoring commit metadata (author, date, message). For each commit on the branch:
-prefix: An equivalent patch exists in the base branch (the change is already in base)+prefix: No equivalent patch exists (the change is NOT in base)
If ALL commits show -, the branch's entire contribution is already in base — it was squash-merged, rebase-merged, or cherry-picked.
Process
# For each local branch not already detected as merged:
git cherry "$BASE_BRANCH" "$branch"
# Count commits NOT in base
git cherry "$BASE_BRANCH" "$branch" | grep '^+' | wc -lFiltering Criteria
For each branch: 1. Skip the base branch 2. Skip branches already detected as merged (Phase 2) 3. Compute merge-base and count unique commits (skip if 0) 4. Run git cherry — if unpicked count is 0, branch is squash-merged
Output Format
=== SQUASH-MERGED BRANCHES (safe to delete) ===
feat/007-rfc-skills-migration (2 weeks ago)
chore/005-reference-doc-sync (3 weeks ago)
feat/003-icon-forge (4 weeks ago)
Total squash-merged: 3Edge Cases
- Partial squash: If only some commits were cherry-picked,
git cherryshows a mix of+and-. These are NOT flagged as squash-merged (they have genuinely unmerged work). - Amended commits: If the squash commit on base was amended after merge, patch-ids may not match. These remain undetected.
- Rebase merges: Also detected by
git cherry(same patch-id mechanism). This is correct — the changes are in base either way.
Output: List of squash-merged branches with relative dates.
Phase 3: Inactive Branch Detection
Purpose
Find branches NOT merged into the base branch that have had no commits within the threshold period. These require review before deletion since they may contain unmerged work.
Process
# Get all local branches with commit timestamps
git for-each-ref --sort=committerdate \
--format='%(refname:short) %(committerdate:unix) %(committerdate:relative)' \
refs/heads/Filtering Criteria
For each branch: 1. Skip the base branch 2. Skip branches with commits newer than the threshold 3. Skip branches that ARE merged (already reported in Phase 2)
Divergence Analysis
For inactive unmerged branches, calculate how far they've diverged from base:
# Get ahead/behind counts
git rev-list --left-right --count "$BASE_BRANCH"..."$branch"
# Output: BEHIND<tab>AHEAD- Ahead N: Branch has N commits not in base (potential unmerged work)
- Behind N: Base has N commits not in branch (branch is outdated)
Output Format
=== INACTIVE UNMERGED BRANCHES (review before delete) ===
experiment/old-feature (8 months ago) [ahead 3, behind 42]
spike/prototype (5 months ago) [ahead 1, behind 28]Output: List of inactive unmerged branches with age and divergence counts.
Phase 4: Remote Analysis
Trigger
Only executed when --remote flag is provided.
Preparation
# Fetch latest remote state and prune deleted remote branches
git fetch --pruneThis ensures the local remote-tracking references are up to date and removes tracking refs for branches that no longer exist on the remote.
Remote Merged Branches
# List remote branches merged into base
git branch -r --merged "$BASE_BRANCH" | grep -v "HEAD" | grep -v "$BASE_BRANCH"Enriched with last commit dates, same as Phase 2.
Remote Inactive Unmerged Branches
# Get all remote branches with timestamps
git for-each-ref --sort=committerdate \
--format='%(refname:short) %(committerdate:unix) %(committerdate:relative)' \
refs/remotes/origin/Same filtering criteria as Phase 3 but applied to remote-tracking refs.
Output Format
=== REMOTE MERGED BRANCHES ===
origin/feat/001-user-auth (last commit: 2025-08-15)
origin/fix/002-login-bug (last commit: 2025-09-01)
=== REMOTE INACTIVE UNMERGED BRANCHES ===
origin/experiment/old-feature (8 months ago)
origin/spike/prototype (5 months ago)Output: Remote branch analysis results.
Phase 5: Report Generation
Summary Block
current_branch=$(git branch --show-current)
total_local=$(git branch | wc -l | tr -d ' ')
merged_into_base=$(git branch --merged "$BASE_BRANCH" | grep -v "$BASE_BRANCH" | wc -l | tr -d ' ')Report Structure
=== SUMMARY ===
Current branch: feat/004-new-feature
Base branch: main
Inactivity threshold: 3 months
Total local branches: 12
Merged into main: 4
Cleanup commands (run manually):
Delete merged local: git branch -d <branch>
Delete unmerged local: git branch -D <branch>
Delete remote: git push origin --delete <branch>
Delete all merged: git branch --merged main | grep -v main | xargs git branch -dSafety Rules
1. Never execute deletion commands — only suggest them 2. Always show current branch — so the user knows where they are 3. Distinguish merged vs unmerged — merged branches are safe; unmerged require review 4. Warn about squash merges — branches merged via squash won't appear as "merged"
Output: Complete summary with cleanup suggestions.
Workflow Diagram
+---------------------------+
| 1. Environment Validation |
| - Git repo check |
| - Parse arguments |
| - Validate base branch |
| - Calculate threshold |
+-----------+---------------+
|
v
+---------------------------+
| 2. Merged Branch Detection|
| - git branch --merged |
| - Enrich: last commit |
| - Count total merged |
+-----------+---------------+
|
v
+---------------------------+
| 2.5 Squash-Merge Detection|
| - git cherry per branch |
| - Compare patch-ids |
| - Flag if all in base |
+-----------+---------------+
|
v
+---------------------------+
| 3. Inactive Branch Detect |
| - for-each-ref scan |
| - Filter by threshold |
| - Exclude merged |
| - Ahead/behind counts |
+-----------+---------------+
|
v (if --remote)
+---------------------------+
| 4. Remote Analysis |
| - git fetch --prune |
| - Remote merged branches |
| - Remote inactive branches|
+-----------+---------------+
|
v
+---------------------------+
| 5. Report Generation |
| - Summary stats |
| - Cleanup suggestions |
| - Squash merge warning |
+---------------------------+Error Handling
No Base Branch
Error: Branch 'main' does not exist
-> Try 'master' fallback
-> If neither exists, ask user for --base <branch>No Stale Branches Found
All branches are active and up to date.
No cleanup needed.Network Errors (--remote)
Warning: Could not reach remote 'origin'
-> Skip remote analysis
-> Report local results onlyNext Steps
- See
EXAMPLES.mdfor real-world usage scenarios - See
TROUBLESHOOTING.mdfor common issues and solutions
Version
1.1.0