Cleanup Dead Worktrees

A dead worktree is one that is safe to delete because all meaningful work is either gone or already preserved in the remote. There are exactly two conditions that make a worktree dead:

  1. No changes — the worktree has no uncommitted changes (staged or unstaged) AND no commits that haven’t been pushed somewhere already.
  2. Branch deleted from origin — the worktree’s branch was pushed to origin at some point, but the remote branch no longer exists (i.e., the PR was merged or the branch was deleted).

The main worktree (the one at the repo root) is never a candidate for deletion.


Steps

1. Fetch and prune remote tracking refs

Run this in the repo root (wherever the user is working):

git fetch --prune

This removes stale remote-tracking refs (e.g. origin/feature-x that no longer exist on the remote). Without this step, condition 2 cannot be evaluated correctly.

2. List all worktrees

git worktree list --porcelain

Parse the output to get each worktree’s:

Skip the first entry — that is always the main worktree.

3. Evaluate each worktree

For every non-main worktree, run the checks below. A worktree is dead if either condition is true.

Condition 1 — No changes

First, check the working tree:

git -C <worktree-path> status --porcelain

If the output is not empty, the worktree has uncommitted changes — stop here and do NOT mark it dead under this condition.

If the working tree is clean, determine whether unpushed commits exist. The approach depends on whether the branch has an upstream configured:

Case A — upstream is configured:

git -C <worktree-path> config --get branch.<branch-name>.merge

If this returns a value, an upstream exists. Check for unpushed commits:

git -C <worktree-path> log @{u}..HEAD --oneline

If this returns nothing, there are no unpushed commits. Mark as dead under condition 1.

Case B — no upstream configured:

Find the default branch reference. Try these in order until one succeeds:

# Option 1: use origin/HEAD if it resolves
git -C <worktree-path> rev-parse --verify refs/remotes/origin/HEAD 2>/dev/null
# If resolved, get the branch name:
git -C <worktree-path> symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/||'

# Option 2: check for origin/main
git -C <worktree-path> rev-parse --verify refs/remotes/origin/main 2>/dev/null

# Option 3: check for origin/master
git -C <worktree-path> rev-parse --verify refs/remotes/origin/master 2>/dev/null

Use the first one that resolves as DEFAULT_REF. If none resolves, skip this worktree — cannot safely evaluate it.

Now check for commits ahead of the default:

git -C <worktree-path> log ${DEFAULT_REF}..HEAD --oneline

If there are any commits ahead of the default branch, skip this worktree — it has unshared work. Do NOT mark it dead.

If the output is empty (no commits ahead of default), mark as dead under condition 1.

Condition 2 — Branch pushed then deleted from origin

After git fetch --prune (Step 1), any remote-tracking ref that no longer exists on the remote has already been removed locally. So no second network call is needed — check the local ref store.

First, confirm the branch was configured to track origin:

git -C <worktree-path> config --get branch.<branch-name>.remote

If this returns origin, the branch was tracking a remote at some point. Now check if the remote-tracking ref still exists locally:

git -C <worktree-path> rev-parse --verify refs/remotes/origin/<branch-name> 2>/dev/null

If this returns empty output (non-zero exit), the remote branch is gone. Mark as dead under condition 2.

Also handle the case where the worktree is in detached HEAD state — git worktree list --porcelain emits detached instead of a branch: line. For detached HEAD worktrees, skip condition 2 entirely. Apply condition 1 only (check working tree cleanliness and commits ahead of default branch).

4. Present findings to the user

Display a table like this:

Dead worktrees found:

PATH                          BRANCH              REASON
/path/to/worktree-a           feature/foo         No changes, no upstream commits
/path/to/worktree-b           fix/bar             Branch deleted from origin
/path/to/worktree-c           chore/baz           Both conditions met

If no dead worktrees are found, tell the user and stop.

5. Ask for confirmation

Do not delete anything without explicit user confirmation.

Ask: “Shall I delete all of these, or do you want to pick specific ones?”

Wait for the user’s answer. Never auto-delete.

6. Delete confirmed worktrees

For each confirmed worktree, run:

git worktree remove <worktree-path>

If the worktree has uncommitted changes that somehow slipped through (shouldn’t happen, but git worktree remove will refuse by default), report the error and do NOT use --force without asking the user explicitly.

After deletion, run:

git worktree prune

to clean up any leftover administrative files.

7. Report results

Tell the user which worktrees were deleted and which (if any) were skipped due to errors.


Safety rules