[BUG] Worktree pool writes .git/worktrees/<name>/gitdir as literal ".git", flagging healthy worktrees "prunable"; dormant ones then get reclaimed/deleted

Open 💬 0 comments Opened Jul 9, 2026 by econosopher

Summary

The Claude desktop app's background worktree pool (the one tracked in ~/Library/Application Support/Claude/git-worktrees.json, schemaVersion: 2 — same registry as #75911) corrupts the git backward-link file of the worktrees it manages: it overwrites <mainrepo>/.git/worktrees/<name>/gitdir with the literal 5-byte string .git instead of the worktree's absolute .git path.

Because the path is now bogus, git worktree list flags every affected worktree prunable ("gitdir file points to non-existent location") — even though the worktree is completely healthy: its own forward link (<worktree>/.git → correct absolute admin-dir path) is intact, and cd into it + git status/diff/log/commit all work normally.

That false prunable flag is not cosmetic. It feeds the same pool-reclaim behavior described in #75911: dormant affected worktrees subsequently get reclaimed and fully deleted (working directory + branch), and a just-repaired one got its HEAD detached by the pool. So the corruption signature is an early warning for the data-loss path in #46444 / the orphaning in #69802.

This is filed separately from #75911 because the gitdir-truncated-to-.git signature itself isn't described there, and it's independently diagnosable and fixable (the pool should write the absolute gitdir path, and/or run the equivalent of git worktree repair after it touches a worktree).

Environment

  • Claude Code CLI 2.1.172
  • Claude desktop app (Claude.app) 1.19367.0
  • macOS 26.5.1 (Darwin 25.5.0, arm64), git 2.50.1 (Apple Git-155)
  • Two unrelated local repos affected simultaneously: one on a plain local-disk path, one under a cloud-synced folder. Both showed the identical signature, which rules out cloud-sync as the cause. Neither repo has any worktree-related script, git hook, launchd/cron job, or repo-local tooling — the only common factor is that every affected worktree was created under .claude/worktrees/<name> by the desktop app's worktree mechanism. A worktree in the same repo created by an unrelated third-party tool (elsewhere on disk) was not affected.

The corruption signature

Healthy worktree, correct links:

# forward link, inside the worktree — CORRECT:
$ cat <repo>/.claude/worktrees/<name>/.git
gitdir: <repo>/.git/worktrees/<name>

# backward link, in the main repo admin dir — CORRUPTED to literal ".git":
$ cat <repo>/.git/worktrees/<name>/gitdir
.git

Result:

$ git worktree list --porcelain
...
worktree <repo>/.git/worktrees/<name>/.git
HEAD <sha>
branch refs/heads/claude/<name>
prunable gitdir file points to non-existent location   # <-- false; worktree is fine

The corruption appears long after git worktree add (which always writes a correct backward link — verified). Observed gaps between admin-dir creation and gitdir corruption were ~2 days and ~29 days. Two different repos on two different volumes had their gitdir files corrupted at the same wall-clock second, i.e. one cross-repo sweep touched both — consistent with a single pool process, not per-repo activity.

Tie to the pool registry (#75911 mechanism)

The git-worktrees.json registry makes the actor concrete. Correlating registry entries against on-disk state and reflogs:

  1. A repaired worktree's HEAD-detach == a pool reclaim, to the second. After I fixed one worktree's backward link with git worktree repair, it went from on-branch to detached HEAD minutes later. Its registry entry recorded pooledAt equal — to the second — to both the registry file's mtime and the checkout: moving from claude/<name> to HEAD line in that worktree's reflog. It was leasedBy: null (dormant) at the time. Reattached with git checkout <branch>; no data loss (clean tree, branch ref intact). This is exactly the premature-reclaim detach in #75911, and repairing the worktree appears to be what made the pool consider it reclaimable again.
  1. Fully-deleted worktrees are gone from the registry too. Two months-dormant affected worktrees went from present-on-disk to directory + branch both gone within ~1–2 minutes, live, mid-investigation, with no session touching them — and both are absent from git-worktrees.json. Plain git worktree prune never deletes the working dir or branch (only the admin entry), so this is the pool's own remove path, not git's.
  1. An actively-leased worktree was corrupted anyway. The worktree of the live session doing this investigation had the same .git gitdir corruption, and its registry entry was present with leasedBy set to that live session. So whatever guard should exclude in-use worktrees from the corrupting sweep either isn't present or is racy.

Impact

  • git worktree list reports healthy, in-use worktrees as prunable, which is confusing and, worse, actionable-looking: a user or script that "cleans up prunable worktrees" will git worktree prune and silently drop the admin entry of a live worktree (it becomes prunable because of this bug, not because it's actually abandoned).
  • The same false-abandoned state feeds the pool's reclaim/delete path → detached HEADs mid-session and, for dormant ones, full directory+branch deletion with no warning and no tool-call trace (data-loss risk; see #46444, #69802, #75911).

Repro / detection

Creation is always correct, so this needs the pool to run its sweep (hours-to-days):

  1. Let the desktop app create a worktree (.claude/worktrees/<name>), work in it, end the session, leave it dormant.
  2. Periodically compare <repo>/.git/worktrees/<name>/gitdir against what <repo>/.claude/worktrees/<name>/.git points back to. When the former reads literally .git while the latter is still a correct absolute path, the bug has fired.
  3. Cross-check git-worktrees.json: pooledAt on the entry lines up with the reclaim/detach event in the worktree's reflog.

Workaround

git worktree repair <repo>/.claude/worktrees/<name> re-derives the correct backward link from the intact forward link and clears the false prunable immediately. (Note: on a leasedBy: null entry this can itself trigger a subsequent pool reclaim/HEAD-detach per point 1 above — harmless, reattach with git checkout <branch>.)

Suggested fix

  • When the pool writes/rewrites a worktree admin entry, write the absolute gitdir path, not a relative .git; or run the equivalent of git worktree repair after any pool operation that touches the admin dir.
  • Gate the corrupting/reclaiming sweep on leasedBy so worktrees held by a live session are never touched (point 3 shows a leased worktree was hit).
  • Don't treat this tool's own prunable-via-truncated-gitdir as "abandoned" when deciding what to reclaim/delete — it's self-inflicted.

Related

  • #75911 (same pool registry; HEAD-detach on reclaim) — this issue adds the gitdir.git truncation signature it doesn't cover.
  • #69802 (ExitWorktree orphans dir + admin + branch).
  • #46444 (worktree auto-cleanup deletes uncommitted work without warning).

View original on GitHub ↗