Windows: session creation reuses stale non-empty .claude/worktrees dir, skips worktree registration, and checks the session branch out in the PARENT repo (HEAD flip under concurrent sessions)
Status Fixed / completed
Reported on v2.1.198
Maintainer reply None cached
Activity 4 comments · opened Jul 11, 2026 · closed Aug 25, 2026
Environment
- Windows 11 Pro (10.0.26200), NTFS
- Claude Code desktop app (sessions created via the app's worktree affordance);
claude --versionreports 2.1.198 - One repo, many concurrent desktop sessions, each assigned a dir under
.claude/worktrees/<slug>/
Summary
When the desktop app creates (or resumes) a session whose designated worktree directory already exists and is non-empty, worktree registration is silently skipped — git worktree add refuses a non-empty dir at every force level — and the harness instead checks the session's branch out in the parent repository. Two consequences:
- The parent checkout's HEAD flips onto the new session's branch as a side-effect of session creation/resume, under any concurrent session or automation using that checkout.
- The new session runs with cwd = a plain, unregistered dir. Every git command resolves upward to the parent repo, while the session banner still claims "You are operating in a git worktree." The session is effectively un-isolated (a "ghost session").
Why stale dirs exist (the loop)
- On Windows,
git worktree removefor a closed session often exits 255: the app's file-watcher holds a lock, so git deregisters the worktree but cannot delete the folder. The leftover retains.claude/settings.local.json(the settings copy the app writes into each worktree). - The app later reuses such a leftover dir as the worktree target for a brand-new or resumed session (observed: one dir hosted five successive sessions over four days). The first session in a fresh dir gets a healthy registered worktree; every session reusing a leftover becomes a ghost.
Evidence (2026-07-09/10, one workspace)
- Five parent-repo reflog entries
checkout: moving from X to <new-session-branch>correlate 1:1, within seconds, with ghost session creations/resumes. - Every ghost session's cwd dir pre-existed its session by 1-4 days; every healthy session's dir was created fresh at registration time.
- In the session list, every ghost session shows cwd-slug != branch-slug; healthy sessions match.
- Concrete damage: at 15:56:03 a new session's creation flipped the parent's HEAD mid-operation of a live ghost session — the ghost's cherry-pick, aimed at its own branch, landed on the seconds-old other session's branch instead, stranding a commit (recovered from the reflog).
- Healing observation: two sessions whose leftover dirs had been fully deleted re-registered healthy worktrees on resume; two others resumed into surviving leftovers, ghosted, and flipped the parent's HEAD.
Reproduction
mkdir -p <repo>/.claude/worktrees/<slug>/.claude && echo '{}' > <repo>/.claude/worktrees/<slug>/.claude/settings.local.json- Start/resume a desktop session that resolves to
<slug>as its worktree dir. - Observe: the slug is absent from
git worktree list; the parent reflog gains acheckout: moving from <old> to <session-branch>entry stamped at session start; from inside the session,git rev-parse --show-toplevelreturns the parent root andgit statusshows../../..-prefixed paths.
Expected behavior (any of)
- Refuse to start the session (or fall back to a fresh slug) when the target dir exists but is not a registered worktree;
- Clear/recreate the dir, then run
git worktree addnormally; - Never fall back to checking the session's branch out in the parent repo — that mutates shared state that concurrent sessions and scheduled automation depend on.
Related but distinct
- #45024 added deliberate reuse of an existing worktree for a branch — this report concerns reuse of an existing unregistered dir.
- #61630 covers a worktree disappearing mid-run; here the worktree never exists in the first place.
- #60624 was
EnterWorktree-specific; this is the desktop session-creation path (EnterWorktreeis never called).
4 Comments
Confirming on Windows 11 / Claude Desktop (bundled claude-code 2.1.205). Same symptom: a session's designated .claude/worktrees/<name> dir already exists and is non-empty, git worktree add is skipped, and the session binds to the PARENT repo — git rev-parse --show-toplevel returns the parent root while the banner still says "You are operating in a git worktree."
New data point: confirmed live that in this state the SessionStart hook never fires, so PreToolUse hooks never load and cannot be restored mid-session — switching into the real worktree (EnterWorktree) recovers the git binding but the hooks stay inert for the session's whole life. So the session is git-usable but unguarded.
Husk origin: we had ~22 leftover deregistered-but-folder-survived dirs (the file-watcher-lock mechanism you describe). Worsened by many concurrent sessions + accumulated stale worktree folders. Windows-specific. Repro matches this issue exactly.
Still reproduces on Claude Code 2.1.207 (Windows 11, NTFS) — survived the v2.1.205 worktree-cleanup fix. Same mechanism as reported:
checkout: moving from <sha> to <session-branch>correlating 1:1 with session creation; the parent's HEAD is left on the session'sclaude/<slug>branch..claude/worktrees/<slug>dir is an empty stub (only.claude/with the app'ssettings.local.json), no.gitfile, absent fromgit worktree list; the session cwd resolves upward to the parent.Two triage notes:
SessionStarthook does not fire in a ghost session — its cwd is the empty stub, which lacks the project's.claude/settings.json, so any project-level "you're not in a worktree" guardrail is silently bypassed. Only user-scope (~/.claude/) hooks fire.git checkout <home-branch>in the parent (when clean) recovers it, but it recurs every session until the desktop per-session worktree feature is disabled (tengu_worktree_mode).Related: #68630, #70069.
Confirming this on another workspace — it's not specific to one repo.
1.20186.7(MSIX / Store package). The version actually driving the sessions is the app-managed bundle:%APPDATA%\Roaming\Claude\claude-code\2.1.205\claude.exe— i.e. claude-code 2.1.205, the same bundle version as the report above. (A separate standalone CLI on PATH reports 2.1.196, but that's an independent install and not what the desktop sessions run — worth calling out, sinceclaude --versionin a terminal is misleading for this app-side bug.).claude/worktrees/<slug>/dir contained only a.claude/folder (no checkout), the slug was absent fromgit worktree listand.git/worktrees/, and the session's branch was checked out in the parent repo instead. From inside the sessiongit rev-parse --show-toplevelreturned the parent root.One thing worth adding from a user's angle: because the session banner still says "operating in a git worktree" and nothing errors, the failure is completely silent — I never verify worktree status on session creation, so I just assumed I was isolated. I only discovered it because an external Git GUI (SourceTree) pointed at the parent repo kept showing the checked-out branch flipping / not matching what I expected — which lines up with the "HEAD flip under concurrent sessions" mechanism in this report.
I can't confirm concurrency was the trigger in every case (I wasn't tracking whether sessions overlapped), but the stale-reused-dir + parent-HEAD-flip signature matches exactly.
Additional forensic evidence — a registered-worktree variant of this bug (Windows, many concurrent sessions)
Adding hard evidence for what looks like the same root cause as this issue — session creation mapping multiple concurrent sessions onto one
.claude/worktrees/<name>directory — with a slightly different downstream symptom than the parent-HEAD flip in the title.Environment: Windows 11, Claude Code launched from VS Code across several
CLAUDE_CONFIG_DIRaccounts (~/.claude-account-N), many parallel sessions against one repo.Symptom (variant): here the
.claude/worktrees/<name>dir is a correctly registered git worktree — it shows ingit worktree liston its own branch. The failure is that ~14 distinct sessions were all given that same worktree as their cwd, working four different branches. Each session's agent then runs, on its own initiative, something like:…which force-switches the shared working tree onto its branch, silently swapping every file out from under whatever other session was mid-task in that same directory.
Evidence — from the session transcripts (
~/.claude-account-*/projects/.../*.jsonl) cross-checked against the worktree'sgit reflog. Two confirmed hijacks in one night, both as agent Bash tool calls:| Local time | Session | Command (verbatim from transcript) |
|---|---|---|
| 02:34 CDT |
6795bbf9…|git checkout -B claude/asvs-handoff origin/main|| 06:45 CDT |
16a0fa00…|git checkout -B claude/asvs-drive-to-pass origin/main|Both match
git reflogcheckout: moving from … to …entries in the shared worktree to the second. Across all transcripts, 14 root sessions all recordedcwd = .../.claude/worktrees/reverent-wilbur-329679, spanning branchesthroughput-thursday,tray-service-manager,asvs-handoff, andasvs-drive-to-pass. The 06:45 instance hijacked a session that was actively editing files at that moment; git carried its uncommitted work onto the wrong branch.Why I believe it's the same root as this issue: the common factor is concurrent session creation resolving to a single existing
.claude/worktreesdir. In #76590 that dir is unregistered, so the checkout lands in the parent repo (HEAD flip). In our case it's registered, so the checkout lands in the shared linked worktree — but either way, multiple live sessions end up sharing one working tree, and a routinegit checkoutfrom any one of them swaps it under the others.Impact: silent mid-task working-tree swaps; two sessions' work colliding on one branch; uncommitted work stranded on the wrong branch. It's invisible until something breaks, because each session believes it owns its cwd.
Suggested guards (any one helps):
.claude/worktreesdir that another live session already occupies — enforce one session ↔ one worktree.git checkout/switcha branch in a worktree another live session is using..claude/worktrees/<name>dir for a new session — mint a fresh one (this is the crux of the reuse described here).We ended up building repo-side guardrails to defend against this — a
PreToolUsehook that blocks switching a linked worktree onto an already-existing branch, plus aSessionStartdrift-warning — which suggests it's worth handling inside Claude Code itself. Happy to share those or a fuller repro if useful. Possibly related: #72502, #76119.