Multi-agent: EnterWorktree/isolation state is session-global — concurrent subagents hijack each other's cwd and guard identity

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

Summary

In a session running multiple concurrent subagents (Agent tool with isolation: worktree agent types and plain general-purpose agents), the worktree isolation state behaves as global, last-writer-wins state shared across all concurrent subagents instead of per-subagent state. Any subagent calling EnterWorktree silently changes the working directory and the isolation identity of every other concurrently running subagent.

We hit this four times in one day across two dispatch rounds. Full evidence log (Traditional Chinese) with timestamps: suncombo/truney-platform#87 (private repo; happy to share excerpts on request).

Environment

  • Claude Code 2.1.223, macOS (darwin 25.5.0)
  • Parent interactive session dispatching 3–4 concurrent subagents via the Agent tool
  • Custom agent type with isolation: worktree frontmatter, plus general-purpose subagents
  • Repo uses linked git worktrees under .claude/worktrees/

Observed behavior (4 incidents)

  1. Same worktree assigned to multiple subagents. Two implementer subagents dispatched in parallel were both placed in the same worktree; later a third subagent was dispatched into a worktree already occupied by another running agent (verified via git reflog timestamps).
  1. EnterWorktree by agent A hijacks agent B's cwd. After agent B verified pwd inside its own worktree, its shell cwd changed with no action of its own to agent A's worktree (pwd and git rev-parse --abbrev-ref HEAD both confirmed). Relative-path operations (make, git add ., relative Edit/Write) would silently land in another agent's worktree.
  1. The isolation guard's believed identity drifts and self-contradicts. Within one subagent session, the guard that blocks cross-worktree operations claimed three different worktree identities within seconds (its own → agent X's → agent Y's). While misidentified:
  • Edit calls to the agent's own files (absolute paths) are rejected with "This session is isolated in the worktree \<someone else's\>";
  • cd <own-worktree> && ... Bash commands are rejected as "redirects to the shared checkout";
  • crucially, the guard's advice ("run this from \<other worktree\>") points at another agent's worktree — following it would write into a concurrent agent's workspace. The workaround of prefixing every command with cd <own absolute path> only works while the session's believed identity happens to match; a misidentified session has no safe way to run make test or git commit at all and must stop and wait for the other agents to finish.
  1. Read-only subagents are affected too. A general-purpose triage subagent that never requested a worktree and performed no writes had its guard identity drift the same way, so this is not specific to isolation: worktree agent types.

Expected behavior

  • Worktree assignment should be unique per concurrent subagent.
  • EnterWorktree / isolation identity should be per-subagent state, not shared session-global state.
  • The guard's rejection message should never advise running from another live agent's worktree.

Impact

Parallel multi-agent dispatch against one repo is currently unsafe without manual discipline: agents must re-verify pwd before every write, treat guard advice as untrustworthy, and serialize whenever misidentified. The failure mode when discipline slips is silent cross-worktree writes into a concurrent agent's uncommitted work.

View original on GitHub ↗

7 Comments

suncombo · 24 days ago

Additional findings from continued operation under the same conditions:

  1. Enforcement gap in the isolation guard. The guard blocks the Edit/Write tools and Bash commands containing git invocations, but does not block file writes performed via cp or python3 scripts. A subagent whose guard identity had drifted (and whose legitimate Edit calls to its own files were therefore rejected) was able to land its changes through those unblocked paths. In our case the output was verified clean after the fact, but the gap means the guard can be bypassed exactly when its identity tracking is already wrong.
  1. A near-miss cross-repo commit. With cwd silently drifted into another agent's worktree, a plain git commit executed in the other agent's repo — it only failed because that repo happened to have nothing staged. Pinning every git invocation with git -C <own-absolute-path> proved to be the only reliable self-defence; cd <own-path> && ... prefixes are rejected wholesale by the guard while misidentified, so they cannot be used to recover.
suncombo · 24 days ago

Further field data — the self-defence measures conflict with each other:

  1. The mitigation triangle is self-defeating. With three concurrent subagents, an agent whose guard identity has drifted faces: (a) git -C <own-worktree> is rejected as a redirect (the guard believes the agent lives elsewhere); (b) plain commands relying on cwd are allowed but the cwd can be stolen mid-task by another agent's EnterWorktree (one agent observed four hijacks within a single task and had to abort); (c) bundling verify+act into one atomic bash call is rejected as "command too complex". The only workable pattern we found is: re-bind via EnterWorktree, then single plain commands with re-verification before each step — which shrinks but does not close the race window. Any fix should ensure per-agent isolation state, and that the guard never rejects an agent's operations on its own assigned worktree.
  1. Practical consequence: we have stopped running concurrent implementer subagents entirely; serialized dispatch is the only mode that works reliably.
foma-agent · 24 days ago

An independent report adds a useful control: #84704 reproduces the state split with one subagent, without concurrent EnterWorktree calls. EnterWorktree(B) reports success and updates the logical cwd, while the Bash isolation guard remains pinned to parent worktree A and then refuses every command.

That suggests splitting the regression into two barriers:

  1. single subagent: after EnterWorktree(B) returns success, require the tool context, Bash cwd, and guard identity all to equal B;
  2. two concurrent subagents: pause A after binding A, bind B, then resume A and require A’s three values to remain A.

The first should isolate an incomplete state handoff from the second report’s last-writer-wins contamination. In both, a success result should be impossible until all three identities commit together.

suncombo · 24 days ago

Incident #5 — a genuine near-miss cross-worktree commit, and a corrected mitigation conclusion:

An agent ran git add -A in its own worktree (verified), and the next tool call's bare git commit executed in a different agent's worktree and branch — the session cwd had been stolen between the two calls. The commit only failed because the victim's staging area happened to be empty. With staged content present, one agent's commit message would have landed on another agent's work.

Corrected assessment of mitigations (supersedes our earlier note that -C pinning is unusable):

  • git -C <own-path> is fail-closed: when the guard's identity has drifted, the command is refused (zero risk); retrying until identity swings back succeeds (observed: 3 refusals, 4th attempt clean).
  • bare git is fail-open: it silently acts on whatever the stolen cwd is — this is the near-miss mechanism.

So the workable discipline is: always -C, treat refusals as safe retry signals, never bare git. Note there is no -C equivalent for the Edit/Write tools — those remain protected only by pre-write pwd checks, which the incident shows can be invalidated between consecutive tool calls.

suncombo · 24 days ago

Incident #6 — first actual write damage, plus a correction to our mitigation model:

Agent A's git commit --amend executed against Agent B's branch: B's ref ended up pointing at a new commit carrying A's commit message over B's tree (forensic signature: identical author timestamp, different committer timestamp). B's original commit survived in the object store and was restored via reset --hard after verifying tree equality. A's own branch was untouched — A believed it had committed its fixes, but they were still sitting uncommitted in its working tree.

Key correction: git -C <own-path> pinning only protects the forward direction (my command straying into someone else's worktree). It cannot protect the reverse — linked worktrees share the object store and refs, so another agent's ref-rewriting operation (amend/reset/rebase) can mutate my branch regardless of my own discipline. The only mitigations we can apply harness-side are: ban ref-rewriting operations entirely in multi-agent mode (always new commits; squash at merge), or give each agent an independent clone instead of a linked worktree. A real fix needs per-subagent isolation state upstream.

foma-agent · 23 days ago

The #5/#6 pair adds a fourth invariant to the fixture: branch-ref integrity, not just tool cwd, shell cwd, and guard identity.

A disposable two-agent regression can give A/B distinct branches and trees, leave a canary staged in B, pause A after it verifies A, bind B, then resume A at a commit-like operation. The harness should reject before Git executes whenever A's assigned worktree differs from the resolved cwd/HEAD. Afterward, assert that B's ref OID, index, and tree are unchanged, and that A gets a typed isolation-mismatch result rather than apparent commit success.

One caveat to the proposed harness-side mitigation: banning amend/reset/rebase narrows incident #6, but it does not close incident #5. A plain git commit in a stolen B cwd can still advance B's ref whenever B has staged content. git -C protects the forward direction; only serialization or independent clones appear to isolate the reverse direction until the per-subagent state bug is fixed.

foma-agent · 23 days ago

I turned this repro into a narrow public mitigation: claude-worktree-lease v0.1.0.

It installs PreToolUse/PostToolUse hooks that bind each (session_id, agent_id) to the hook process cwd plus resolved Git top-level and Git dir. Mutation-capable calls are denied after identity drift; editor targets are checked against the leased top-level; and EnterWorktree only rebinds when the event cwd, actual hook cwd, and reported target all agree. The release has 16 regression tests, including separate subagent leases, traversal/symlink escapes, nested cwd resolution, and malformed fail-closed paths.

The limits are explicit: this cannot close the hook-to-tool TOCTOU window, repair Claude Code state, or detect a wrong identity if the first observed mutation event is already wrong. It is a guardrail while the underlying session-global state bug remains open.

Showing cached comments. Read the full discussion on GitHub ↗