[BUG] ExitWorktree refuses clean worktrees with 'Could not verify worktree state' — failed where.exe git resolution cached as null for entire session (Windows desktop)

Status Fixed / completed
Reported on v2.1.146
Maintainer reply None cached
Activity 3 comments · opened Jun 10, 2026 · closed Jun 16, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest matches #34496, #50320 are different surfaces of git detection; nothing covers this resolver/cache path)
  • [x] This is a single bug report (one root defect with three contributing code issues in the same path; a related-but-separate observation is clearly marked at the end)
  • [x] I am using the latest version of Claude Code available to the desktop app (2.1.146 bundled; forensics cross-checked against standalone 2.1.150)

What's Wrong?

ExitWorktree(action: "remove") on a completely clean, just-created worktree refuses with:

Could not verify worktree state at <path>. Refusing to remove without explicit
confirmation. Re-invoke with discard_changes: true to proceed — or use action:
"keep" to preserve the worktree.

The refusal is instant and deterministic for the whole session. Manually running the exact verification commands in the same worktree succeeds (git -C <wt> status --porcelain → exit 0, clean; git -C <wt> rev-list --count <base>..HEAD → exit 0).

Proof git never runs: I set git config --global trace2.eventTarget <dir> (one trace file per git process), snapshotted the directory, triggered the refusal, and diffed: zero git processes were spawned during the ExitWorktree call. The failure is fabricated inside the CLI before any git execution.

Root cause (from the shipped bundle, minified names as found)

The remove-verification helper (Z94 in both 2.1.146-adjacent and 2.1.150 bundles) is the only piece of the worktree code that shells out with a bare "git" string:

async function Z94(H, q) {
  let K = await y6("git", ["-C", H, "status", "--porcelain"]);
  if (K.code !== 0) return null;                 // → "Could not verify"
  let $ = countNonEmpty(K.stdout);
  if (!q) return null;
  let _ = await y6("git", ["-C", H, "rev-list", "--count", `${q}..HEAD`]);
  if (_.code !== 0) return null;
  return { changedFiles: $, commits: parseInt(_.stdout.trim(), 10) || 0 };
}

y6 → Jq resolves bare command names on Windows via Wm8 → ag6:

function ag6(H) {
  let q = og6.get(H);
  if (q !== void 0) return q;                      // cache hit — including cached null
  let $ = path.join(SYSTEMROOT, "System32", "where.exe");
  try {
    let f = execFileSync($, [H], { timeout: 5000, env: process.env, ... })
              .trim().split(/\r?\n/).filter(Boolean);
    let A = process.cwd();
    for (let z of f) { if (vR8(z, A)) continue; return og6.set(H, z), z; }
    return null;
  } catch (_) {
    if (Ar_(_)) og6.set(H, null);                  // where.exe exit 1 → null cached FOREVER
    return null;
  }
}

and Jq fabricates a result without spawning anything when resolution returns null:

let w = Wm8(H);
if (w === null) return Promise.resolve({ stdout: "", code: 127,
  stderr: `Command '${H}' not found or is in an unsafe location (current directory)` });

So the chain is:

  1. The CLI process (spawned by the Windows desktop app, AppData\Roaming\Claude\claude-code\2.1.146\claude.exe, child of the MSIX-packaged Electron Claude.exe) inherits a PATH without Git's directories — even though the registry User PATH has C:\Program Files\Git\mingw64\bin and C:\Program Files\Git\bin (stale/sanitized environment snapshot in the Electron→CLI chain).
  2. First y6("git", ...) → where.exe git → exit 1 → null is cached in og6 for the lifetime of the process.
  3. Every subsequent y6("git", ...) returns a fabricated exit 127 instantly, without running git.
  4. Z94 maps any non-zero code to null, and the caller maps null to "Could not verify worktree state".

Three defects in this path

  1. Failed resolution is cached permanently. A single transient where.exe exit 1 (or a PATH quirk at startup) poisons every bare-"git" invocation for the rest of the session. There's no retry, no TTL, no invalidation.
  2. The error is swallowed and conflated. The fabricated 127's stderr ("Command 'git' not found or is in an unsafe location") never reaches the user; it surfaces as "Could not verify worktree state", which reads as "your worktree is in a weird state" and steers users toward discard_changes: true as a reflex — on a worktree the tool literally hasn't examined. A "git could not be located" message would be honest and actionable.
  3. Inconsistent git resolution within the same feature. Worktree creation (wHK) uses the pre-resolved git binary path (Jq(nq(), ...) — full path, bypasses Wm8/ag6), which is why creation works while removal verification fails in the same session. If Z94 used the same resolved path, this failure class would not exist.

Steps to Reproduce

On a Windows machine where the desktop-app-spawned CLI's PATH lacks git (verifiable indirectly: every ExitWorktree remove refuses instantly with "Could not verify" while git -C <wt> status --porcelain is clean and exits 0 from a terminal):

  1. Start a Claude Code session from the Windows desktop app.
  2. EnterWorktree (any name) — creation succeeds (uses resolved git path).
  3. Confirm cleanliness manually: git -C <worktree> status --porcelain → empty, exit 0.
  4. ExitWorktree(action: "remove") → "Could not verify worktree state", instantly, every attempt, all session.
  5. (Optional proof) git config --global trace2.eventTarget <dir> before step 4 → no trace file is created during the refusal → git never spawned.

What Should Happen?

  • ExitWorktree verification should use the same resolved git path as worktree creation.
  • A resolution failure should surface as "git could not be located", not "could not verify worktree state".
  • A failed where.exe lookup should not be cached as a permanent session-wide null (retry on next call, or at minimum on each ExitWorktree invocation).

Environment

  • Claude Code 2.1.146 (bundled with Windows desktop app Claude_1.11847.5.0_x64, MSIX/WindowsApps), forensics cross-checked against standalone 2.1.150 — identical Z94/ag6/Jq logic in both bundles
  • Windows 11 Pro 10.0.26200
  • Git for Windows installed at C:\Program Files\Git, present on the registry User PATH (mingw64\bin, bin) — i.e., git is "correctly installed" by any normal standard
  • Possibly related (different surface, plausibly same env-inheritance family): #34496, #50320

Workaround for affected users

Verify by hand that git -C <worktree> status --porcelain is empty, then re-invoke with discard_changes: true. Restarting the desktop app (recomposing its environment from the registry) should clear the condition; the cached null cannot heal within a running session.

Related observation (separate minor issue, mentioning for context)

Even with resolution fixed, worktrees using worktree.symlinkDirectories (e.g. .venv) will show the symlink as ?? .venv when the repo's .gitignore uses the very common directory-only form .venv/ — gitignore dir-patterns never match symlinks — making every clean worktree look like "1 uncommitted file" to the verifier. Repo-side fix is dropping the trailing slash, but the CLI could make symlinkDirectories robust by appending the symlinked names to the worktree's .git/info/exclude at creation time. Happy to file separately if preferred.

View original on GitHub ↗

3 Comments

ndwuhuangwei · 4 months ago

Second symptom surface of this same defect, for anyone arriving here: the Windows /desktop command ("Failed to open Claude Desktop. Please try opening it manually.", e.g. #59824) launches its deep link by spawning the bare name "cmd", which goes through this exact resolver/negative-cache/fabricated-127 chain — in the 2.1.173 bundle the functions are x7q/C7q/Iq/egH, logic identical to the ag6/og6/Jq/Wm8 you decompiled from 2.1.146/2.1.150, so the defect is alive in the latest release.

Full analysis with a forensic timeline and a differential fingerprint (same invocation resolves bare "reg" successfully right before "cmd" returns the cached null, which rules out any live PATH breakage) posted in https://github.com/anthropics/claude-code/issues/59824#issuecomment-4684457577.

+1 to all three proposed fixes here — especially not caching failed resolutions for the process lifetime. For /desktop specifically there's an even simpler hardening: the resolver already uses an absolute %SYSTEMROOT%\System32\where.exe, so cmd should be spawned the same way (or replaced with a native ShellExecute-equivalent), removing the PATH dependency entirely.

omalleyandy · 4 months ago

Update: still reproduces on desktop 1.12603.1.0 / bundled CLI 2.1.170 — and the "restart the desktop app" workaround from the original report does NOT clear it.

Re-ran the acceptance test today (2026-06-12) on the same machine, after the desktop app had updated 1.11847.5.0 → 1.12603.1.0 (bundled CLI 2.1.146 → 2.1.170, multiple full app restarts in between):

  1. EnterWorktree → worktree created cleanly.
  2. git -C <wt> status --porcelain → exit 0, zero lines (verifiably clean — the #b7ae0fc1 gitignore fix means the .venv symlink no longer shows as untracked).
  3. ExitWorktree(action: "remove") → instant refusal: "Could not verify worktree state…" — the resolver-failure message, not the "N uncommitted files" one.
  4. Confirmed the session is the desktop-spawned CLI: parent C:\Program Files\WindowsApps\Claude_1.12603.1.0_x64_…\app\Claude.exe → CLI %APPDATA%\Claude\claude-code\2.1.170\claude.exe.

Two implications:

  • The environment defect is persistent across desktop app restarts and app updates, not a one-time stale snapshot — please disregard the restart remedy I suggested when filing. Affected machines hit this in every session, indefinitely; the only path out is the discard_changes: true reflex this issue warns about.
  • Repro is confirmed on the current desktop bundle (2.1.170), not just the 2.1.146/2.1.150 examined at filing time.

Related: the same resolver + permanent negative-cache defect also breaks /desktop on Windows — bare "cmd" goes through the same where.exe lookup and the real exit-127 error is swallowed behind the generic "Failed to open Claude Desktop" message. Filed separately with full forensics as #67821.

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.