Skip to content

Worktrees accumulate unreaped: 115 of 142 were clean and disposable, across 6 roots including one the convention forbids #1776

Description

@kyle-sexton

Summary

Worktrees accumulate without bound across this fleet and nothing reaps them. A
hygiene pass today found 142 linked worktrees across 6 different roots,
of which 115 were clean and disposable — no tracked changes, no untracked
files, not locked. They had simply never been removed. Only 27 held real
uncommitted state.

Nothing in the current tooling classifies a clean-but-stale worktree as
reclaimable, so they are invisible to every audit and survive indefinitely.

The measured state before cleanup

Root Registrations
<ghq>/melodic-software/claude-code-plugins/.claude/worktrees 55
<ghq>/melodic-software (siblings) 58 (incl. 11 canonical)
~/.claude/plugins/data/source-control-melodic-software/worktrees 37
~/.claude-loop-worktrees 38
C:/tmp 6
~/.codex/worktrees/* 2
system temp / dead-session scratchpad 2

After removing every clean one: 11 canonical + 27 linked remain, all 27 holding
uncommitted work or locked.

Defect 1: no disposability lane

repo-fleet-hygiene:audit classifies worktrees as merged-worktree,
prunable-worktree, missing-worktree, worktree-admin-mismatch, or
locked-worktree. There is no category for clean worktree, branch not
merged
— which was the state of most of the 115.

That gap matters because worktree disposability is independent of merge state.
Removing a clean worktree destroys nothing: the branch ref and every commit live
in the canonical repository. The only thing a worktree uniquely holds is
uncommitted work. So the correct predicate is:

disposable  <=>  no tracked changes AND no untracked files AND not locked

Merge status is irrelevant. During this pass I initially gated removal on "the
branch is also deletable" and consequently removed only 55 of 115 — the same
conservatism the audit's taxonomy encodes.

Requested: a reclaimable-worktree finding for any clean, unlocked linked
worktree, with age or last-commit recency as the ranking signal rather than merge
state.

Defect 2: git stash list in a linked worktree reports the whole repository

A trap worth encoding in whichever helper computes worktree state. refs/stash
is shared per repository, so git stash list run inside a linked worktree
returns the canonical repo's entire stash list, not that worktree's. It reads
non-zero for every worktree of any repo that has ever stashed.

I gated removal on stash == 0 and it silently excluded 113 of 115 candidates.
Stashes survive worktree removal, so stash count must not be part of a
disposability predicate. Any shared worktree helper should either not expose it
per-worktree or name it repo_stash_count.

Defect 3: six roots, one of which the documented convention forbids

The documented policy (user-scope doc ghq-layout-sibling-pr-worktrees.md) is a
sibling layout, <canonical>-pr-<number>, and it explicitly rules out
.claude/worktrees/ inside the repo when the canonical checkout is a ghq tree.
Yet 55 registrations were in exactly that forbidden location, nested inside
claude-code-plugins' own working tree, and three more roots exist that the doc
does not mention at all.

Naming is equally scattered. Observed schemes among siblings alone:

  • standards-pr-223 (the documented form — 1 of 23)
  • ci-workflows-pr281 (no separator before the number)
  • claude-code-plugins-issue-1017 (issue, not PR)
  • claude-code-plugins-1211 (bare number)
  • claude-code-plugins-1285-argparse (number plus slug)
  • medley-plugin-user-scope (no number at all)

and elsewhere worktree-agent-<17-hex>, ccp-1269, 609, 848-sigfix,
drive-1503-v2, wip/lane-snapshot/<slug>.

Requested: one resolver that every worktree-creating skill calls, so location and
naming cannot drift per caller, plus a check that fails when a registration
appears outside the sanctioned roots.

Defect 4: removal leaves shells behind when workspace symlinks exist

git worktree remove succeeded but left a directory tree behind for a medley
worktree, because npm workspace symlinks under
.claude/skills/*/extraction/node_modules/@melodic/ were not created by git and
are not removed by it. The links dangled (their targets were inside the removed
tree), so find -type f reported zero files while rmdir refused with
"Directory not empty" — the residue is symlinks, which neither predicate sees.

Worth handling in whichever skill wraps worktree removal: after a successful
git worktree remove, if the path still exists, report it, and check for
non-directory non-file entries rather than assuming "no files" means empty.

Impact

Beyond disk, the accumulation actively blocks hygiene: an attached branch cannot
be deleted, so 115 stale worktrees were pinning branches that were otherwise
provably merged. Cleanup had to run worktree-removal before branch-deletion for
55 branches in this pass.

Environment

  • 11 canonical checkouts, melodic-software org, ghq layout on D:/repos
  • Git 2.54.0.windows.1, Claude Code on Windows 11 Pro 10.0.26200
  • Observed 2026-07-29

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions