Worktree cleanup
Remove worktrees and branches whose PR is merged or closed. Destructive — never discard uncommitted or unpushed work.
Find the repo
ROOT="$(git rev-parse --show-toplevel)" || { echo "not a git repo"; exit 1; }
If $ROOT/WORKTREES.md exists, follow its naming/location — the repo's doc is the source of truth. Defaults below: worktrees under .claude/worktrees/<slug>/, branches worktree-<slug>.
Decide what's done
List worktrees and check each branch's PR state (MERGED / CLOSED / OPEN, or no PR):
git -C "$ROOT" fetch --prune origin
git -C "$ROOT" worktree list --porcelain # path + branch per worktree
# per candidate branch:
gh pr view "$BRANCH" --json state,url --jq '"\(.state)\t\(.url)"' # no PR ⇒ non-zero exit
A worktree is a cleanup candidate only if its PR is MERGED or CLOSED. Leave OPEN and no-PR worktrees alone.
Verify it's safe (per candidate)
Before removing, confirm nothing would be lost — skip and report any that fail:
git -C "$WORKTREE" status --porcelain # must be empty (no uncommitted/untracked)
git -C "$WORKTREE" log --oneline "@{u}.." 2>/dev/null # must be empty (no unpushed commits)
- Dirty or unpushed → stop, show the user, do not
--force. - Present the full candidate list and get a go-ahead before deleting.
Remove
Run from $ROOT (never from inside the worktree you're removing):
git -C "$ROOT" worktree remove ".claude/worktrees/${SLUG}" # refuses if dirty — that guard is intentional
git -C "$ROOT" branch -d "worktree-${SLUG}" \
|| git -C "$ROOT" branch -D "worktree-${SLUG}" # -D only after PR confirmed MERGED/CLOSED (squash-merge leaves -d "not fully merged")
git -C "$ROOT" worktree prune
Notes
- Squash/rebase merges make
git branch -dreport "not fully merged"; the-Dfallback is safe because the PR state is already confirmed merged/closed — not as a blanket habit. - Never delete the main checkout or its default branch.
- Unreachable ≠ deletable: a failed
ghcall (network/auth) is not a closed PR. Confirm state before removing.