Agent isolation:"worktree" reuses a stale, per-session cached worktree path — survives manual teardown AND filesystem deletion
Summary
Agent(isolation: "worktree") dispatch fails immediately with Failed to resolve HEAD in <path>: git rev-parse failed during "Initializing…". The path it names is a worktree the session had already created and torn down manually (via a repo-provided git worktree wrapper) earlier in the same session. The harness appears to cache "this session's worktree slot for this repo" and reuse it on every subsequent isolation: "worktree" dispatch, without checking whether that path is still a real, registered git worktree.
Repro
- In a session, manually create and fully tear down a
git worktreeat some pathPfor repoR(i.e.git worktree add, do work,git worktree remove— outside of any Agent-tool dispatch). - Dispatch
Agent(isolation: "worktree")targeting repoR. - It fails:
Failed to resolve HEAD in <R>/P: git rev-parse failed.
What we ruled out
- Not shared-checkout contention. Ran a 20-second, 50ms-interval poll of
git rev-parse HEAD(and lock-file state) against the shared checkout, concurrently with a freshisolation:"worktree"dispatch that reproduced the failure. The shared checkout resolved HEAD successfully on every single poll, zero failures, throughout. - Not stale directory content.
git worktree listand the repo's own worktree registry both correctly showed pathPas gone. We deleted the physical directory entirely (find P -delete, confirmed vials) and retried the dispatch immediately — it reproduced the identical failure against the identical path, with nothing on disk there at all. This rules out leftover files/state as the cause. - Not a permission-hook rejection. The repo has an extensive
PreToolUsehook pipeline (custom bash-guard/path-guard scripts). We checked its own telemetry log for the exact failure window: every recorded hook evaluation in that window showsoutcome: allow— zero denials. Structurally, the "Initializing…" worktree-provisioning step appears to run before the spawned agent ever issues a tool call, so it never reaches repo-levelPreToolUsehooks at all — consistent with the failure being internal to the harness's own spawn bootstrap, before any hookable action occurs. - First step of provisioning still runs. Immediately before the failure, we observed (via
ps aux) the target directory being recreated on disk with the harness's own internal bookkeeping written into it (a conductor/presence-tracking subdirectory, keyed to our session ID) — so the harness clearly attempts something at that path before failing the HEAD check. It just never gets to actually creating a real linked worktree there.
What we think is happening
The harness maintains some form of cached association — "this session's designated worktree path for this repo" — that:
- Persists across separate
isolation:"worktree"dispatches within the same session. - Is not invalidated when that worktree is torn down through means other than the harness's own dispatch lifecycle (in our case, a repo-provided worktree-management CLI wrapping
git worktree, used directly by the session, not via the Agent tool). - Is not invalidated by the path no longer existing on disk, or no longer being a registered git worktree.
Environment
- Claude Code 2.1.278 (macOS)
Agenttool,isolation: "worktree",subagent_type: general-purpose/fork.- Heavy concurrent usage on the host: 64 concurrent Claude Code processes running against the same repo at the time of reproduction — mentioning this since it may correlate with cache-invalidation pressure, though the bug reproduced cleanly in a controlled, low-noise retest too.
Related, but not exact matches
We found several open issues in this area that share partial symptoms but don't match this specific "cache survives filesystem deletion" mechanism:
- #95644 — worktrees never auto-cleaned when a subagent finishes.
- #83895 — a stale worktree path reused/reassigned across separate dispatches, causing a leaked process to run against the wrong session's directory.
This report's distinguishing evidence is the deletion test: the path was fully removed from disk and the failure still reproduced identically, which points at an in-memory/cached association rather than a filesystem or git-registry state issue.
Impact
Once triggered, this reliably blocks every subsequent isolation:"worktree" dispatch for the rest of the affected session (workaround: avoid isolation:"worktree" for the rest of that session; create the worktree manually and dispatch a non-isolated agent against the fixed path instead). A fresh session (new session ID) did not reproduce it in our testing, consistent with a session-ID-keyed cache.
4 Comments
Second data point: fresh session does NOT avoid this, and it's a different failure shape
Tested the "cache is session-ID-keyed" hypothesis from the report above with a genuinely fresh (non-resumed) session against the same repo. Result: does not confirm that hypothesis, and looks like a related-but-distinct failure mode.
What happened: 3/3
Agent(isolation:"worktree", subagent_type:"fork")dispatches failed on the first try, every time, with the same error shape:Failed to resolve HEAD in <repo>: git rev-parse failed.Key difference from the original report: the path named in the error is the main checkout itself, not a stale worktree-slot subpath. It doesn't match any of the previously-used worktree names from the other session's history. So a stale, session-cached worktree-slot path is not the (sole) mechanism here — this fresh session had no such cache to inherit, and still failed identically.
Sanity check, same as before:
git rev-parse HEADagainst that exact main checkout succeeds every time when run directly, both before and after the failed dispatches. So whatever pre-flight HEAD check the harness runs internally is failing against a path where HEAD demonstrably resolves fine outside the harness.Possible contributing factor, not root-caused:
git worktree liston this repo currently returns 75 entries (45 of them ephemeralagent-*/wf_*names from past isolation:"worktree" dispatches across many sessions over time — apparently never garbage-collected, consistent with #95644's "worktrees never auto-cleaned" report). If the harness's own pre-flight step enumerates or validates existing worktrees as part of provisioning a new one, a repo with 75 accumulated entries is a very different cost/timing profile than a repo with a handful, and could plausibly explain a HEAD-check that's fine standalone but fails/times out inside the harness's own pre-flight.Net: this repo may simply be in a bad state (worktree-list bloat from #95644 never firing) that makes the underlying
isolation:"worktree"provisioning path fail regardless of session/cache state. Flagging as a lead for maintainers to check, not a confirmed root cause on our end — we don't have visibility into what the pre-flight check actually does.Third data point: isolation:"worktree" is NOT globally broken -- it's specific to this one repo
Ran a controlled comparison after fixing an unrelated internal gate that was blocking a prior retest:
Failed to resolve HEAD in <repo>: git rev-parse failed.pwd,git rev-parse --show-toplevel,git remote get-url origin) ran fine, no error at all.Both sessions confirm
isolation:"worktree"sandboxes the caller's own primary repo (matching #85448), not anything named in the dispatch prompt -- so this was a genuinely controlled comparison, not an artifact of targeting.Working theory now: this isn't a global harness bug or a purely session-cache bug -- it's specific to (or triggered by) something about this one repo's state. The strongest candidate so far: this repo's
git worktree listhad accumulated 75 entries (45 of them ephemeralagent-*/wf_*names from pastisolation:"worktree"dispatches, apparently never cleaned up per #95644). We've since manually swept ~20 stale ones down to ~55 and will report back if that changes anything, but haven't yet seen it flip to working.If useful for debugging on your end: this repo is a large monorepo with many git submodules and a custom PreToolUse hook pipeline, and has been used heavily with
isolation:"worktree"across many concurrent sessions over an extended period -- which is exactly the kind of accumulated state (stale worktree registrations, or whatever internal bookkeeping tracks "known worktrees for this repo") that could plausibly degrade only after sustained heavy use, rather than reproducing in a fresh/lightly-used repo.Root cause found: Agent-tool isolation:"worktree" mis-resolves its base path for a repo that is itself a git submodule
This explains everything reported in this thread, and narrows the bug to one specific code path.
The repo in question (
families/supernal-coding) is itself a git submodule of a parent superproject (supernal-nova) -- its own.gitis a gitlink FILE (gitdir: ../../.git/modules/families/supernal-coding), not a directory, and it in turn has ~11 submodules of its own.Comparing where different worktree-creating mechanisms physically place their content, for the SAME repo:
| Creator | Naming | Placement | Correct? |
|---|---|---|---|
| A repo-provided worktree CLI wrapper (
git worktree addunder the hood) |wf_*/ feature names, under.claude/worktrees/<name>or.worktrees/<name>|<repo>/.claude/worktrees/<name>/.git-- a normal sibling of the visible checkout | ✅ correct ||
Agent(isolation:"worktree")|agent-<hash>, under.claude/worktrees/<name>|<REPO'S OWN GITDIR>/.claude/worktrees/<name>/.git-- i.e..git/modules/families/supernal-coding/.claude/worktrees/<name>/, physically INSIDE the gitdir tree, not beside the checkout | ❌ wrong |Verified directly via
git worktree list --porcelainand thegitdirfile each worktree's admin entry points to. Everyagent-*worktree for this repo has its actual working-tree content nested inside.git/modules/families/supernal-coding/...instead of alongsidefamilies/supernal-codingitself. Everywf_*worktree for the exact same repo is placed correctly.A repo that is NOT a submodule of anything (tested: a small standalone repo,
.gitis a real directory) does not show this problem --Agent(isolation:"worktree")places its worktree correctly there (<repo>/.claude/worktrees/agent-<hash>/, a normal sibling location), and the diagnostic dispatch succeeds cleanly, norev-parseerror.Hypothesis for the actual bug: whatever resolves the base path for a new Agent-tool worktree likely calls something equivalent to
git rev-parse --git-dir(or similar) and appends the worktree subpath to that result, rather than resolving the visible working-tree root (git rev-parse --show-toplevel, or the submodule's own working directory). For a normal top-level repo these two resolve to essentially the same place, so the bug is invisible. For a repo that is ITSELF a submodule,--git-dirresolves to the nested.git/modules/<path>location while the actual checkout stays at a completely different, non-nested path -- and a worktree created relative to the former is structurally broken from the start, which plausibly explains both the originalFailed to resolve HEADfailures and the accumulated worktree-list bloat reported earlier in this thread (any operation on these malformed worktrees, including cleanup, likely also misbehaves).This also explains why the bug reproduced reliably for
families/supernal-codingspecifically across every attempt in this thread, while a controlled test against a non-submodule repo (dispatched from a session whose own primary repo was that non-submodule repo) worked cleanly every time.Correction to my earlier root-cause claim: it's submodule + extensions.worktreeConfig, not submodule alone
My prior comment claimed "any repo that is itself a git submodule" triggers this. That was wrong, and I have a clean, reproducible test proving the narrower, precise condition.
Test setup: a fresh, genuinely vanilla submodule pair (outer repo + inner submodule,
git submodule add, nothing else configured).extensions.worktreeConfigset): dispatchedAgent(isolation:"worktree")from a session rooted in the submodule. Succeeded cleanly -- worktree placed correctly at<submodule>/.claude/worktrees/agent-<hash>, no error.extensions.worktreeConfigtotrueon that same submodule, nothing else changed. Re-ran the identical dispatch. Failed, with the harness's own error message naming the exact mechanism:``
``Refusing to use <path>/.claude/worktrees/agent-<hash> as an isolation worktree:
git resolves its working tree to <path>/../.git/modules/<name>
(a core.worktree redirect, or a checkout discovered above it),
so commands run there would write outside the worktree.
extensions.worktreeConfigback off, re-ran again. Succeeded again.Off -> on -> off tracked success -> fail -> success exactly, on the identical repo, with the identical dispatch. This isolates the real trigger to a single git config value:
extensions.worktreeConfig=trueon a repo that is itself a submodule -- not submodule-ness alone.This also explains why
families/supernal-coding(the repo in every prior report in this thread) reproduces reliably: it hasextensions.worktreeConfig=truedeliberately enabled (a real, legitimate git feature, turned on there to fix a separate, earliercore.bare/core.worktreecorruption issue this repo hit from heavy concurrent worktree usage). It is not a misconfiguration on our end -- it's a documented, load-bearing setting for that repo's own concurrency needs, and it happens to trigger this harness bug as a side effect.Likely mechanism, now narrower: when
extensions.worktreeConfigis enabled, git stores certain config (including, plausibly,core.worktree) per-worktree rather than in the shared submodule config. If the Agent tool's own worktree-provisioning code reads/writes this value in the wrong scope, or doesn't account for the per-worktree config file at all, a newly created worktree could end up with acore.worktreevalue that still points at (or resolves back to) the submodule's own gitdir rather than the new worktree's own directory -- consistent with the error text above.We've updated our own internal gate to the corrected, narrower condition (submodule AND worktreeConfig enabled, not submodule alone) so it no longer over-blocks a plain submodule that doesn't have this problem.