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

Resolved 💬 2 comments Opened Jun 10, 2026 by omalleyandy 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 };
}

y6Jq resolves bare command names on Windows via Wm8ag6:

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 ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗