[BUG] Windows: isolation-worktree guard compares paths case-sensitively, so IDE-started worktree sessions cannot be resumed (regression in 2.1.222)

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 3 comments · opened Aug 9, 2026

What happened

Since 2.1.222, a session that entered an isolation worktree from the VS Code /
Cursor extension can no longer be resumed on Windows. The CLI exits 1 and the panel
shows Error: Claude Code process exited with code 1 followed by MCP shutdown
logging - the actual reason is cut off the head of the stderr snippet:

[ERROR] [worktree] declining to resume into c:\...\.claude\worktrees\x (work-tree-elsewhere):
Refusing to use c:\...\.claude\worktrees\x as an isolation worktree:
git resolves its working tree to C:/.../.claude/worktrees/x
(a core.worktree redirect, or a checkout discovered above it), so commands run there
would write outside the worktree.
Error: cannot resume into worktree ... This session was not started.

Both paths are the same directory. They differ only in the case of the drive letter.

Root cause

The isolation-worktree adoption guard compares the pinned path against
git rev-parse --show-toplevel case-sensitively, on a platform where paths are
case-insensitive.

In the 2.1.226 win32-x64 binary the guard is

let u = Fye(c.topLevel, e);
if (u === "distinct") return { ok:!1, reason:"work-tree-elsewhere", ... }

and Fye resolves to the case-sensitive arm of a shared helper, while the sibling
comparator used for the containment checks right next to it takes the case-insensitive
arm:

function gyr(e,t,r){ let n = r ? e.toLowerCase() : e, o = r ? t.toLowerCase() : t; if(n===o) return !0; ... }
function mBt(e,t){ return gyr(e,t,!0) }   // case-insensitive -> IJe -> uun / cun
function n6u(e,t){ return gyr(e,t,!1) }   // case-SENSITIVE   -> yyr -> Fye  (this guard)

The win32 canonicaliser normalises separators (/ -> \) but never touches letter
case, so c:\... and C:\... survive as distinct strings and the guard reads them as
two different directories.

The two spellings arise naturally and neither side is wrong:

  • The pin comes from the IDE. Every workspace entry VS Code / Cursor store is

file:///c%3A/..., URI.fsPath renders it as c:\..., and Node passes a spawn
cwd through verbatim (process.cwd() in the child returns exactly c:\...), so
the CLI records "worktreePath":"c:\\..." in the session's worktree-state.

  • --show-toplevel returns C:/....

Reproduction

Clean repo, real worktree, real session. Only the drive-letter case in the recorded
worktree-state changes between the two runs.

git init repo; cd repo
git commit --allow-empty -m init
git worktree add .claude/worktrees/wt -b wtbranch
claude -p "say ok" --output-format json      # run from .claude\worktrees\wt, note the session id

Then set the session's worktree-state record to each spelling and resume from the
worktree:

| recorded worktreePath | result |
| ------------------------------------------- | ------------------ |
| C:\...\worktrees\wt (what the OS reports) | resumes, exit 0 |
| c:\...\worktrees\wt (only the drive letter)| refused, exit 1 |
| C:/.../worktrees/wt (forward slashes) | resumes, exit 0 |
| C:\appdata\... (one segment miscased) | refused, exit 1 |

So separators are normalised and are not the trigger, and there is no drive-letter
special case - it is a plain case-sensitive comparison of the whole path.

Same lower-cased input across versions:

| CLI | result |
| ------- | --------------- |
| 2.1.221 | resumes, exit 0 |
| 2.1.222 | refused, exit 1 |
| 2.1.226 | refused, exit 1 |

The strings work-tree-elsewhere, as an isolation worktree and cannot resume into
worktree
are absent from the 2.1.220 and 2.1.221 win32-x64 binaries and present
from 2.1.222 - matching that release's changelog line, *"isolation now applies to file
edits and Bash in every session type"*.

Expected

c:\project and C:\project are the same directory on Windows and the guard should
treat them as such - i.e. Fye should use the case-insensitive comparator on win32,
like the containment checks beside it already do.

Impact

Every worktree session ever started from the IDE on Windows is unresumable, not just
new ones: the bad spelling is already persisted in the transcripts. On this machine
that was 65 of 98 sessions holding a worktree binding.

Environment

  • Claude Code 2.1.226 (CLI and the bundled VS Code extension), Windows 11 26200
  • Reproduced with the win32-x64 binaries of 2.1.221 / 2.1.222 / 2.1.226 from OpenVSX
  • Surfaces: Cursor and VS Code extensions. Terminal sessions are unaffected, because

a shell supplies the canonical C:\ spelling.

Workaround

Upper-case the drive letter in the worktreePath / originalCwd /
preEnterOriginalCwd fields of the {"type":"worktree-state"} records in
~/.claude/projects/**/<session>.jsonl, and launch the CLI with a canonical cwd.

View original on GitHub ↗

3 Comments

salisjuanjose · 20 days ago

Independent confirmation on another Windows 11 machine (VS Code extension 2.1.226), plus a second symptom of the same guard that this report doesn't cover: creation, not just resume.

EnterWorktree({name}) hits the same comparator, and fails open

Calling EnterWorktree with name: (no path:) fails every time in an IDE-started session:

Refusing to use c:\repo\.claude\worktrees\foo as an isolation worktree:
git resolves its working tree to C:/repo/.claude/worktrees/foo
(a core.worktree redirect, or a checkout discovered above it) ...

Same two spellings, same directory. The candidate path is built by concatenating onto the launch cwd — which, exactly as you describe, the IDE supplies as c:\....

Measured on the rejected worktree, to rule out the stated reason:

  • git config --get core.worktree → empty
  • .git file → gitdir: C:/repo/.git/worktrees/foo (correct)
  • git rev-parse --show-toplevelC:/repo/.claude/worktrees/foo (the worktree itself)

And git rev-parse --show-toplevel returns the uppercase spelling regardless of the case you pass in (-C c:\repo and -C C:\repo both return C:/repo), so on this path the mismatch is unconditional.

Why this one is worse than the resume case: it fails open. The refusal happens after git worktree add succeeds, so the session simply keeps running in the main checkout with no isolation. For anyone running several sessions against one repo — which is what worktree isolation is for — that silently reintroduces exactly the cross-session clobbering the feature prevents. The resume failure at least surfaces as exit 1.

Every failed attempt leaks a locked worktree plus its branch

The worktree is created and locked before the guard rejects it, and nothing releases it:

$ cat .git/worktrees/foo/locked
claude session foo (pid 15548)

$ git worktree list
C:/repo/.claude/worktrees/foo   abc1234 [worktree-foo] locked

The lock names the PID of the session that just failed, so it isn't recoverable by the retry either — these accumulate one per attempt. Cleanup is git worktree unlock <path>git worktree remove <path>git branch -D worktree-<n>git worktree prune.

Workaround for the creation path

Unlike the resume case, this one needs no jsonl surgery: create the worktree yourself and enter it by path with a canonical drive letter.

git worktree add C:\repo\.claude\worktrees\foo -b worktree-foo HEAD
EnterWorktree({ path: "C:\\repo\\.claude\\worktrees\\foo" })

That works because it hands the guard the same spelling git will return. It's been reliable across four uses here.

Two small data points for your analysis

  • Confirms "terminal sessions are unaffected" from a different angle: ~/.claude/projects on this machine has accumulated both spellings of the same project over months (c--Users-... and C--Users-...), depending on how each session was launched. The lowercase root key dates back to June, i.e. the bad spelling long predates 2.1.222 — it was simply harmless until this guard started comparing it.
  • The process path itself carries the lowercase drive: Get-Process claude | Select Pathc:\Users\...\.vscode\extensions\anthropic.claude-code-2.1.226-win32-x64\resources\native-binary\claude.exe.

So the fix you propose (Fye taking the case-insensitive arm on win32) would close both symptoms at once.

letrung1912-source · 3 days ago

Independent confirmation from a third entry point: Remote Control spawn mode (claude rc, "worktree" spawn choice, CLI not IDE).

Repro:

PS C:\claude\vn-market-data-v2> claude rc
...
Choose [1/2] (default: 1): 2
Error: Failed to create worktree for session cse_...: Refusing to use C:\claude\vn-market-data-v2\.claude\worktrees\bridge-cse_... as an isolation worktree: git resolves its working tree to C:/Claude/vn-market-data-v2/.claude/worktrees/bridge-cse_... (a core.worktree redirect, or a checkout discovered above it), so commands run there would write outside the worktree. Remove the redirect, restore the worktree's own .git, or recreate the worktree, then retry.

Same root cause confirmed independently, via the actual filesystem/git state after a failure:

  • git worktree list (from the main checkout) shows the rejected worktree correctly registered and locked — git's own bookkeeping is fine.
  • The worktree's .git file, its .git/worktrees/<name>/gitdir back-reference, and .git/worktrees/<name>/config.worktree are all intact; no core.worktree anywhere (checked the shared config and every per-worktree config.worktree — this repo has extensions.worktreeConfig = true).
  • git rev-parse --show-toplevel run against the rejected worktree resolves correctly, and returns the canonical uppercase-drive spelling (C:/Claude/...) regardless of whether it's invoked via the lower- or upper-case path — matching @salisjuanjose's measurement exactly.
  • The shell's own cwd was C:\claude\vn-market-data-v2 (lowercase c, typed by hand without tab-completion); the real on-disk casing is C:\Claude\vn-market-data-v2. That's the entire mismatch — nothing on disk or in git is actually broken.

Two more data points from this entry point:

  • The leak reproduces here too. 3 locked bridge-cse_<session-id> worktrees accumulated over ~2 hours of automatic retries in one claude rc session, one per failed spawn attempt, each still holding its locked file naming the (long-idle) host pid. Cleanup needed manual git worktree unlock + remove --force per orphan — the retry loop never releases them itself.
  • Unlike the EnterWorktree-in-IDE case reported above, this surface does not appear to fail open: the claude rc TUI's capacity counter stayed at 0/32 across every failed retry, i.e. no unisolated fallback session seems to actually run in the main checkout here. Worth the team confirming, but from the outside this entry point looks fail-closed (blocks spawn) rather than fail-open (silent unisolated session) — a useful contrast for prioritizing the fix.

Three independent trigger surfaces now (resume, EnterWorktree create, claude rc spawn create) all reducing to the same win32 case-sensitive comparator. +1 for the case-insensitive arm on win32 fixing all three at once.

jslitzkerttcu · 9 hours ago

Fourth entry point hitting the same guard, plus a filesystem-level workaround that doesn't require touching session storage.

Environment: Claude Code 2.1.251, Windows 11, git 2.55.0.windows.3, terminal/CLI session (not VS Code/Cursor extension, not claude rc).

Repro (a fourth code path — top-level Agent() tool call with isolation="worktree", not resume, not EnterWorktree, not Remote Control spawn):

Refusing to use C:\repos\<project>\.claude\worktrees\agent-<id> as an isolation worktree:
git resolves its working tree to C:/Repos/<project>/.claude/worktrees/agent-<id>
(a core.worktree redirect, or a checkout discovered above it), so commands run there
would write outside the worktree. Remove the redirect, restore the worktree's own .git,
or recreate the worktree, then retry.

Root cause matches the analysis already on this thread: the session's tracked cwd was C:\repos\... (lowercase parent dir) while the actual NTFS entry for the parent directory was stored as C:\Repos\... (uppercase) — same directory on a case-insensitive filesystem, but the guard's case-sensitive string compare against git rev-parse --show-toplevel's canonicalized output rejects it.

Workaround that fixed it, without editing any session storage: renamed the mismatched parent directory on disk to match the casing the harness already had tracked (C:\ReposC:\repos). After the rename, git rev-parse --show-toplevel returns the same casing the session already expects, and isolation="worktree" dispatch succeeds and cleans up correctly on both a no-op test run and real work afterward. This is a decent stopgap for anyone hitting this via a plain terminal session (not IDE-resume) where the documented JSONL-editing workaround doesn't obviously apply — but it only works if the mismatched directory isn't shared with other active projects/sessions, since it's a real filesystem rename with real blast radius.

Given four independent entry points now confirmed (resume, EnterWorktree create, Remote Control spawn, top-level Agent(isolation="worktree")), this looks like it's the single shared comparator (Fye/gyr(e,t,false) per the analysis above) used everywhere the harness validates a worktree's resolved toplevel against its tracked path — not a narrow IDE-resume-only bug. Given the scope, a one-line fix flipping that comparator to case-insensitive on Windows (matching the sibling containment check that already does this correctly) seems low-risk and would close all four reports at once.