Prune stale git branches after a merge wave: delete closed/merged remote branches, remove local tracking branches that no longer exist on the remote, and optionally clean up local branches already merged to main. Use after running dependabot-triage, after a sprint wind-down, or when the user says "clean up branches", "prune branches", "delete merged branches", or "tidy up git".
Installs into .claude/skills of the current project.
Are you the author of Branch Cleanup?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/oleg-koval-branch-cleanup)
---
name: branch-cleanup
description: >
Prune stale git branches after a merge wave: delete closed/merged remote branches,
remove local tracking branches that no longer exist on the remote, and optionally
clean up local branches already merged to main. Use after running dependabot-triage,
after a sprint wind-down, or when the user says "clean up branches", "prune branches",
"delete merged branches", or "tidy up git".
license: MIT
allowed-tools: Bash
compatibility: Codex, Claude Code, Cursor, GitHub Copilot, Windsurf, Kiro, and other Agent Skills compatible tools. Requires git and gh (GitHub CLI) authenticated.
metadata:
targets: [_source-only]
author: Oleg Koval
tags:
- git
- branches
- cleanup
- maintenance
- dependabot
- github
source: weekly-pattern-learner
source_reason: "dependabot-triage was added to merge dependency PRs in bulk but leaves stale remote branches behind; no skill follows up to prune them"
source_date: "2026-08-10"
---
<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
> π€ *Auto-generated by **weekly-pattern-learner** Β· dependabot-triage was added to merge dependency PRs in bulk but leaves stale remote branches behind; no skill follows up to prune them*
# Branch Cleanup
Remove stale remote and local branches safely after a merge wave. Defaults to
dry-run so you see exactly what will be deleted before anything is removed.
## Inputs
- **Remote** (optional, default: `origin`)
- **Base branch** (optional, default: `main`, detect from `gh repo view`)
- **--dry-run** (optional, default: on): preview deletions without executing them
- **--execute**: actually delete branches (requires explicit flag, safety default)
- **--include-local**: also clean up merged local branches (off by default)
## Step 1: Fetch and prune remote tracking references
Always start here. This removes local tracking refs for remote branches that are
already gone, without touching anything else:
```bash
REMOTE="${Remote:-origin}"
git fetch --prune "$REMOTE"
```
After pruning, print the count of removed tracking refs:
```bash
git branch -vv | grep ': gone]' | wc -l
```
## Step 2: Identify stale remote branches
Find remote branches whose associated PR is merged or closed:
```bash
# Get all non-default remote branches
BASE=$(gh repo view --json defaultBranchRef -q '.defaultBranchRef.name' 2>/dev/null || echo "main")
git branch -r --merged "$REMOTE/$BASE" \
| grep -v "HEAD" \
| grep -v "$REMOTE/$BASE" \
| sed "s|$REMOTE/||" \
| sort
```
Cross-reference with GitHub to confirm each branch's PR state. Only include branches
with a confirmed merged or closed PR. An empty result means the branch is not eligible:
```bash
STALE_BRANCHES=()
for branch in $(git branch -r --merged "$REMOTE/$BASE" \
| grep -v HEAD | grep -v "$REMOTE/$BASE" | sed "s|$REMOTE/||"); do
PR_DATA=$(gh pr list --head "$branch" --state merged \
--json number,title,mergedAt \
--jq '.[0] | "\(.number) merged \(.mergedAt | .[0:10])"' 2>/dev/null)
if [ -z "$PR_DATA" ]; then
PR_DATA=$(gh pr list --head "$branch" --state closed \
--json number,title,closedAt \
--jq '.[0] | "\(.number) closed \(.closedAt | .[0:10])"' 2>/dev/null)
fi
if [ -n "$PR_DATA" ]; then
echo "$branch β $PR_DATA"
STALE_BRANCHES+=("$branch")
else
echo "$branch β no confirmed PR (skipped)"
fi
done
```
This produces a table like:
```
dependabot/npm_and_yarn/lodash-4.17.21 β 42 merged 2026-07-31
dependabot/npm_and_yarn/eslint-8.57.0 β 43 merged 2026-07-31
feature/login-refresh β 38 merged 2026-07-28
```
## Step 3: Safety filter
**Never propose deletion of:**
| Branch | Reason |
|--------|--------|
| `main`, `master`, `develop` | Protected base branches |
| `beta`, `staging`, `production` | Release/deploy targets |
| Any branch with an open PR | May still be in review |
| Any branch not yet merged to base | Would lose commits |
| Branches the user has modified locally in the last 24h | Work-in-progress |
Check for open PRs before including a branch in the deletion list:
```bash
OPEN=$(gh pr list --head "$branch" --state open --json number --jq 'length')
[ "$OPEN" -gt 0 ] && echo "SKIP (open PR): $branch"
```
## Step 4: Preview or execute
### Dry-run (default)
Print the deletion plan and stop. Never touch the remote without `--execute`:
```
Branch cleanup preview, dry-run (pass --execute to apply)
Remote branches to delete (all merged to main):
origin/dependabot/npm_and_yarn/lodash-4.17.21 (PR #42, merged 2026-07-31)
origin/dependabot/npm_and_yarn/eslint-8.57.0 (PR #43, merged 2026-07-31)
origin/feature/login-refresh (PR #38, merged 2026-07-28)
Local tracking refs already pruned: 3 (by git fetch --prune)
Would NOT delete:
origin/beta (protected release branch)
origin/feature/signup (open PR #47)
Run with --execute to apply.
```
### Execute
```bash
# Delete remote branches
for branch in "${STALE_BRANCHES[@]}"; do
git push "$REMOTE" --delete "$branch" && echo "deleted: $REMOTE/$branch"
done
```
## Step 5: Local branch cleanup (optional, --include-local)
Only runs when `--include-local` is explicitly passed.
Find local branches already merged to base and not checked out:
```bash
git branch --merged "$BASE" \
| grep -v "^\*" \
| grep -v "$BASE" \
| grep -v "main\|master\|develop\|beta\|staging"
```
For each candidate, confirm there is no uncommitted work and the tracking ref is
gone before deleting:
```bash
git branch -vv "$branch" # should show ': gone]' if remote is deleted
git branch -d "$branch" # safe delete: fails if unmerged work exists
```
Never use `-D` (force delete): let `-d` protect you from accidental data loss.
## Report
```
Branch cleanup complete
Remote:
Deleted: 12 branches (10 Dependabot, 2 feature branches, all merged)
Protected: 2 skipped (beta, open PR)
Local tracking refs:
Pruned: 12 stale refs (git fetch --prune)
Local branches (--include-local):
Deleted: 3 branches already merged to main
Kept: 1 branch (has uncommitted work)
Repo now has: N remote branches, M local branches
```
## Chaining
| Before this skill | After this skill |
|---|---|
| `olko:dependabot-triage` (merged patch PRs) | next `olko:morning-routine` starts clean |
| Sprint wind-down / milestone close | `git fetch` confirms clean state |
| Any large batch merge | nothing: maintenance complete |