---
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"
---

> 🤖 *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 |
