[BUG] ExitWorktree refuses clean worktrees with 'Could not verify worktree state' — failed where.exe git resolution cached as null for entire session (Windows desktop)
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:
- The CLI process (spawned by the Windows desktop app,
AppData\Roaming\Claude\claude-code\2.1.146\claude.exe, child of the MSIX-packaged ElectronClaude.exe) inherits a PATH without Git's directories — even though the registry User PATH hasC:\Program Files\Git\mingw64\binandC:\Program Files\Git\bin(stale/sanitized environment snapshot in the Electron→CLI chain). - First
y6("git", ...)→where.exe git→ exit 1 →nullis cached inog6for the lifetime of the process. - Every subsequent
y6("git", ...)returns a fabricated exit 127 instantly, without running git. Z94maps any non-zero code tonull, and the caller mapsnullto "Could not verify worktree state".
Three defects in this path
- Failed resolution is cached permanently. A single transient
where.exeexit 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. - 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: trueas a reflex — on a worktree the tool literally hasn't examined. A "git could not be located" message would be honest and actionable. - Inconsistent git resolution within the same feature. Worktree creation (
wHK) uses the pre-resolved git binary path (Jq(nq(), ...)— full path, bypassesWm8/ag6), which is why creation works while removal verification fails in the same session. IfZ94used 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):
- Start a Claude Code session from the Windows desktop app.
EnterWorktree(any name) — creation succeeds (uses resolved git path).- Confirm cleanliness manually:
git -C <worktree> status --porcelain→ empty, exit 0. ExitWorktree(action: "remove")→ "Could not verify worktree state", instantly, every attempt, all session.- (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?
ExitWorktreeverification 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.exelookup 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 — identicalZ94/ag6/Jqlogic 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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗