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 --version reports 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:

  1. 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.
  2. 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 remove for 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

  1. mkdir -p <repo>/.claude/worktrees/<slug>/.claude && echo '{}' > <repo>/.claude/worktrees/<slug>/.claude/settings.local.json
  2. Start/resume a desktop session that resolves to <slug> as its worktree dir.
  3. Observe: the slug is absent from git worktree list; the parent reflog gains a checkout: moving from <old> to <session-branch> entry stamped at session start; from inside the session, git rev-parse --show-toplevel returns the parent root and git status shows ../../..-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 add normally;
  • 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 (EnterWorktree is never called).

View original on GitHub ↗

4 Comments

Shawheen-A · 1 month ago

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.

wshallwshall · 1 month ago

Still reproduces on Claude Code 2.1.207 (Windows 11, NTFS) — survived the v2.1.205 worktree-cleanup fix. Same mechanism as reported:

  • Parent-repo reflog shows checkout: moving from <sha> to <session-branch> correlating 1:1 with session creation; the parent's HEAD is left on the session's claude/<slug> branch.
  • The designated .claude/worktrees/<slug> dir is an empty stub (only .claude/ with the app's settings.local.json), no .git file, absent from git worktree list; the session cwd resolves upward to the parent.
  • Ghost sessions show cwd-slug ≠ branch-slug; healthy ones match — as described.

Two triage notes:

  • A project-scoped SessionStart hook 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.
  • With concurrent sessions standing in the parent, the HEAD flip swaps their files mid-task. 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.

DX-JHS · 1 month ago

Confirming this on another workspace — it's not specific to one repo.

  • Windows 11 Pro (10.0.26200).
  • Claude desktop app 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, since claude --version in a terminal is misleading for this app-side bug.)
  • Hit the exact "ghost session" symptoms across multiple connected repos, not just one: the session's designated .claude/worktrees/<slug>/ dir contained only a .claude/ folder (no checkout), the slug was absent from git worktree list and .git/worktrees/, and the session's branch was checked out in the parent repo instead. From inside the session git rev-parse --show-toplevel returned 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.

wshallwshall · 1 month ago

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_DIR accounts (~/.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 in git worktree list on 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:

cd ".../.claude/worktrees/reverent-wilbur-329679" && git checkout -B <its-branch> origin/main

…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's git 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 reflog checkout: moving from … to … entries in the shared worktree to the second. Across all transcripts, 14 root sessions all recorded cwd = .../.claude/worktrees/reverent-wilbur-329679, spanning branches throughput-thursday, tray-service-manager, asvs-handoff, and asvs-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/worktrees dir. 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 routine git checkout from 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):

  1. Refuse or warn when session creation would place a session in a .claude/worktrees dir that another live session already occupies — enforce one session ↔ one worktree.
  2. Warn when an agent is about to git checkout/switch a branch in a worktree another live session is using.
  3. On Windows especially, don't reuse an existing .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 PreToolUse hook that blocks switching a linked worktree onto an already-existing branch, plus a SessionStart drift-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.