Skip to content

worktree cleanup can destroy unpushed commits it cannot distinguish from stale ones #1977

Description

@kyle-sexton

Problem

/worktree status reports age and PR state. /worktree cleanup removes on that
basis and then emits git branch -D for the user to run. Neither step can answer
the only question that matters before a removal: would this destroy a commit
that exists nowhere else?

A worktree that is old, quiet, and clean is indistinguishable, on every signal the
skill currently collects, from a worktree holding four unpushed commits that exist
in no other reachable place. git worktree remove succeeds on both — a clean tree
is not evidence of safety — and the emitted git branch -D finishes the job one
step later.

This is not hypothetical. A fleet sweep found 17 worktrees whose branches had no
upstream at all, and a cleanup pass over them was reconstructible only because the
branch refs happened to survive removal.

Why the obvious detections do not work

Each of these looks like the answer and is wrong in a way that only shows up on
real data:

  • git log --branches --not --remotes reports every OTHER branch in the
    repository. On a detached HEAD — the one case where removal makes commits
    unreachable immediately, with no branch ref left holding them — it reads clean
    at exactly the moment it matters most.
  • @{upstream}..HEAD returns nothing for a branch with no upstream, which
    described all 17.
  • A per-commit patch-id, git cherry included, cannot see a multi-commit
    squash-merge: the squash collapses N commits into one patch that no individual
    commit's id can match. In a repository that squash-merges by policy that is the
    dominant case, not an edge.
  • git rev-parse --is-inside-work-tree returns true for an empty leftover
    directory inside a repository, and a git -C probe of that directory then
    reports the containing repository's clean state as the directory's own.

What is needed

A detection engine that decides, per worktree, whether its unpushed commits'
content is already on the base — fail-closed, so that only affirmative proof
yields "safe" and every ambiguity is treated as unsafe — and the guards in
status and cleanup that consume it, at both the removal site and the branch
deletion that follows it.

Placement belongs in scope too: the same invariant is why worktrees must live
outside every repository, and the creation paths that bypass /worktree create
(claude --worktree, subagent isolation: "worktree", background sessions) have
no guard at all today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions