isolation: worktree subagents silently write outside the worktree via absolute file_path (Edit/Write target primary clone, not the isolated worktree)
Summary
A subagent spawned with isolation: "worktree" has its cwd correctly set to the isolated worktree, but its Edit/Write/Read calls routinely target an absolute file_path rooted at the primary clone rather than the worktree. Because the harness nests worktrees under the primary clone (<repo>/.claude/worktrees/agent-<id>/), that absolute path resolves to a real, writable file on the primary clone's checked-out branch (e.g. main). The edit succeeds silently — no error — and the subagent's work lands on the wrong branch entirely, outside its isolation.
The isolation boundary is therefore advisory, not enforced: nothing stops a subagent from editing the parent checkout, and the most natural failure mode (absolute path = the canonical repo path the agent carries in context) points directly at the parent.
Why this is serious
- Silent loss of isolation: the entire point of
isolation: worktreeis that a subagent can't touch the parent. It can, trivially, via any absolute path. - Silent wrong-branch writes: edits land uncommitted on the primary clone's branch (often
main). The subagent's own verification (relative-pathgit diff/md5) inspects the worktree, sees no change, and misreports the write as a "flush failure" — so the agent doesn't even know it leaked. - Cross-session accumulation: leaked edits sit uncommitted on the primary branch indefinitely and get attributed to later sessions.
Evidence (observed in a real session)
Two subagents in one run, cwd correctly the worktree for 100% of records:
- doc-updater: all 4
Edit/Writecalls used the primary-clone absolute path -> all 4 leaked ontomain. Agent concluded (wrongly) that the "write-path silently failed to flush," because its relative-path verification saw the unmodified worktree copy. - issue-developer: first 5
Editcalls used the primary-clone path (leaked), then it empirically detected the dirty primary clone, hand-reconstructed the worktree-rooted absolute path, and the next 5 calls were correct. So the correct absolute worktree path is constructible — the agent just defaults to the wrong root.
Across every subagent transcript in this project, 51/51 Edit/Write/MultiEdit calls used absolute paths; the agents never use relative. The absolute path they assemble defaults to the canonical repo root from context (the primary clone), not git rev-parse --show-toplevel of the worktree.
Root cause
Edit/Write/Read require absolute file_path. The agent assembles it from the canonical repo path in its context (= primary clone), not from its actual worktree root. The harness does not fence Edit/Write to the worktree, and worktree-under-primary-clone nesting means the wrong path hits a real file. Result: mandated-absolute-path + salient-path-is-primary-clone + nested-worktree = silent escape.
Suggested fix (harness-side)
Guard Edit/Write (and Read/MultiEdit) in an isolation: worktree session: if the resolved absolute file_path falls outside the worktree root (git rev-parse --show-toplevel), reject the call with an error naming the worktree-relative path the agent should use. Deterministic; doesn't depend on prompt compliance. (Optionally: don't nest worktrees under the primary clone, so an out-of-worktree path can't resolve to a sibling file — but the guard is the robust fix.)
Related but distinct
- #52958 — cwd leaks into the parent. Here cwd is correct; the file_path escapes.
- #47548 — setup-time isolation failure (parent branch switched). Here isolation sets up fine and is breached post-setup via path.
Environment
- Claude Code: 2.1.150
- Model: claude-opus-4-7 (Opus 4.7) — both the orchestrator and the two affected subagents (issue-developer, doc-updater) ran on this model
- Platform: darwin 25.5.0
- Repo shape: standard git repo, worktrees at
<repo>/.claude/worktrees/agent-<id>/ - Reproduced with both
doc-updaterandissue-developersubagent types in a single/issue-addressorchestration run.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗