Worktree: Edit/Read tools use main workspace paths instead of worktree paths
Bug Description
When Claude Code operates in a git worktree (created automatically by the harness), the Edit, Read, and Write tools consistently target files in the main workspace rather than the worktree directory, even though the system prompt correctly identifies the worktree as the primary working directory.
Steps to Reproduce
- Start a Claude Code session that creates/uses a git worktree (e.g. via the automatic branch creation feature)
- The system prompt correctly states:
- Primary working directory: /path/to/repo/.claude/worktrees/name
- Current branch: claude/name
- Ask Claude to edit a file (e.g. workspace_shell.py)
- The Edit tool targets /path/to/repo/workspace_shell.py (main workspace) instead of /path/to/repo/.claude/worktrees/name/workspace_shell.py (worktree)
Expected Behavior
All file operations should resolve relative to the worktree path when operating in a worktree. The system prompt explicitly provides the correct working directory, but file paths from Explore agents and subsequent tool calls use the main workspace root.
Actual Behavior
- Explore agents report file paths using the main workspace root
- Subsequent Edit/Read/Write calls use those paths verbatim
- All changes land in the main workspace (often main branch), not the worktree side branch
- The running app (via preview_start) executes from the worktree and sees none of the changes
- This is completely invisible to the user — no error is raised, edits succeed silently on the wrong files
Impact
In our session, every edit across ~15 iterations went to the wrong file. The app ran the original unmodified code the entire time. Hours of debugging a "not working" visual change were wasted because the changes were never in the file being executed.
Environment
- macOS (Darwin 25.3.0)
- Claude Code CLI with worktree support
- Model: claude-opus-4-6
Suggested Fix
When operating in a worktree, file tool calls should either:
- Resolve relative paths against the worktree root (not the main workspace), or
- Validate that absolute paths fall within the worktree and warn/redirect if they don't
14 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
👎
I am seeing this behavior with the Explore agent as well, which regularly tries to read from the project root rather than the worktree directory, even though I started the conversation in the worktree directory.
A PreToolUse hook can detect when Edit/Read/Write targets the main workspace instead of the worktree:
This compares
--git-dirvs--git-common-dirto detect worktree context, then checks iffile_pathpoints to the main repo instead. It warns but doesn't block (exit 0), since some cross-workspace reads may be intentional.I confirm that same exact issue happens on my sub-agents when they work in worktrees. Anecdotic or not, this started to happen around time when I switched to work from devcontainer
Additional reproduction: 4 occurrences in one day (v2.1.98, macOS)
Confirming this is still present in v2.1.98 on macOS (Darwin 25.4.0). Hit this 4 times today across 4 separate worktree sessions.
Reproduction pattern (plan mode → implementation)
.claude/worktrees/<name>on branchworktree-<name>No error or warning is raised. The edits succeed silently on the wrong files.
Key detail
This seems to get worse with Explore/Plan agent handoff. The explore agents during plan mode report paths from the main repo. When plan mode exits and implementation begins, those paths carry over into Edit calls. The worktree path is never substituted.
Environment
Adding a data point from the Ea-Lyssa repo (Python project, Linux/WSL2, current Claude Code as of 2026-04-18):
This bug bites recurring multi-PR workflows hard. In one week (2026-04-11 through 2026-04-17), I had 4 separate incidents of
Edit(and onceMultiEdit) landing in the main repo when the agent's cwd was a worktree at<repo>/.claude/worktrees/<branch>/. Documented in our project'smemory/MEMORY.mdas the "#576-family cwd-leak class" with three structural variants:Agentwithisolation: "worktree"leaks caller cwd on return — caller's cwd silently switches into the worktree.isolation: "worktree"silently ignore the parameter — teammates inherit the team-lead's cwd; recovery requires teaching every git-touching teammate to invokeEnterWorktreeas its first action.Editwith absolute paths (this issue): even when cwd is correctly the worktree perpwd, anEditcall with a main-repo-rooted absolute path (e.g./repo/src/foo.pyinstead of/repo/.claude/worktrees/branch/src/foo.py) lands the edit in the main repo and silently loses the worktree state. Recovery:git -C <main-repo> checkout -- <file>then re-apply the edit using a worktree-rooted absolute path.Workaround discipline we now hard-code in every contributor brief:
This costs us roughly 3-5 minutes per incident in recovery + re-edit + reviewer scope-check. Across a multi-PR session it adds real overhead.
The mental model gap is real: agents (and humans) treat absolute paths as authoritative and assume
cwdis the disambiguator for relative paths only. TheEdittool's actual behavior is the opposite — absolute paths win silently, cwd doesn't enforce a worktree boundary.Would strongly value an
Editmode that errors when the absolute path falls outside the current worktree's scope, OR a contract that says "absolute paths are resolved against cwd's worktree boundary, not the filesystem root."Corroborating repro observed — 2026-04-18
Environment: Claude Code v2.1.112, WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2).
Observation: After reconciling a private research repo, found ~13 untracked research files present in both the main repo working tree and a prior agent worktree (
isolation="worktree") at.claude/worktrees/agent-<id>/, byte-identical viadiff -rq.Why this matches the leak pattern, not benign duplication:
git worktree adddoes not copy untracked files from the source tree, so identical untracked files in both locations cannot be explained by worktree creation itself.cp— and (b) didn't happen.Matches the "partial leak" signature already reported on this thread: some writes resolve to the worktree correctly, others land in the main repo's absolute path. In our case the split favored main (most files leaked), consistent with paths having been cached/returned from tools run before the subagent entered its worktree context.
Workarounds we're adopting locally:
git rev-parse --git-dirvs--git-common-dirand warns (soon: blocks) whenfile_pathescapes the current worktree..git/worktrees/*/lockedfor dead-PID locks and cleans up (adjacent hygiene issue — agents crashing mid-write leave orphan locked worktrees whose untracked state then accumulates the leaked files).Happy to provide more forensic detail (mtime table, worktree branch log) if useful.
Partial-leak variant observed — 2026-04-19 (same env as my prior comment)
Follow-up repro on the same machine (CC 2.1.112, WSL2). Where the prior observation was all-leak, this one is 5-of-6 leaked, 1 correctly confined — diagnostically useful because it rules out "worktree isolation is completely broken" and narrows the cause to path-resolution timing.
Repro
Agent invocation:
Agent reported success:
.claude/worktrees/agent-ab64e217worktree-agent-ab64e217b96ec31,d40fcf9) landing all 6 files in the worktree branchActual state after agent returned —
git diff HEAD -- <f>in main vsgit diff HEAD..worktree-agent-ab64e217 -- <f>:| File | Worktree branch | Main working tree | sha256 of diff |
|---|---|---|---|
|
dhx/statusline-wrapper.js| ✅ complete | ❌ identical leak | SAME ||
tests/probes/probe-drift-detection.js| ✅ complete | ❌ partial leak (527/533 lines) | DIFFER ||
tests/probes/probe-migration.js| ✅ complete | ✅ untouched in main | — ||
docs/decisions.md| ✅ complete | ❌ partial leak | DIFFER ||
docs/backlog.md| ✅ complete | ❌ identical leak | SAME ||
docs/statusline-wrapper.md| ✅ complete | ❌ identical leak | SAME |Why 1 file stayed confined
tests/probes/probe-migration.jsis the only file whose path the agent almost certainly didn't resolve until after its worktree-entry sequence ran. The other 5 appear in the orchestrator's prompt by name, or were touched by a Read/Glob call the agent likely ran during its initial context-gathering pass — i.e., paths cached before the worktree branch-check reset.probe-migration.jswas referenced only indirectly in the plan ("extend migration probe"), so the agent had to resolve it via Glob/Read aftercwdwas the worktree. That resolution path produced a worktree-rooted absolute path, whichEditthen used correctly.This matches @mikehenson's 2026-04-18 comment on this thread: paths resolved before the worktree switch retain the main-repo prefix; paths resolved after get worktree-prefixed.
Edit/Writedo not re-validate against the worktree boundary, so pre-switch paths silently write to main.Workaround hook — shippable today
Tighter than the
git rev-parse --git-dir/--git-common-dirapproach I mentioned earlier — usescwdstring-prefix matching, no git spawn:Measured cost: ~3ms per Edit/Write/MultiEdit call (276ms / 100 invocations). Session overhead on a 200-edit day: +600ms. Imperceptible.
Caveat: this is a band-aid. It blocks the leak at hook time but doesn't recover the lost work — the agent proceeds thinking it succeeded, then the merge step fails when it tries to stage files that were never written to the worktree. The correct fix belongs in the tool resolver: when
cwdis inside a worktree,Edit/Writeshould reject (or rewrite) an absolutefile_paththat doesn't share the worktree prefix. At the string level the main-repo path and the worktree path are indistinguishable to the agent — only the resolver can tell them apart.Recovery cost for others hitting this
git stash push -uthe leaked partials in main (preserves for audit)git merge worktree-agent-<id> --no-fffrom main — brings in the complete authoritative version from the worktree commits~5 commands, no data loss, but requires operator attention. An agent that returned "complete" without post-merge parity verification would silently ship a partial fix.
---
Update 2026-04-19 (PM):
Bash-channel variant slips past the Edit-matcher hook aboveThird occurrence today — leaked through a vector the hook in the sketch above doesn't cover.
When the runtime's read-before-edit enforcement rejects an
Editon a file the subagent hasn'tReadyet, agents fall back to shell. From the subagent's own session summary:The absolute path piped into
sed -i/python3is pulled from the agent's existing context — typically a main-repo path sourced from an earlierReadof plan or reference files that live in main (not on the worktree branch). Result: byte-identical writes land in both the worktree's commit series (via an eventual successfulEditafter the requiredRead) and in main's working tree as uncommitted dirty files. Same signature as the partial-leak above, different channel.The
matcher: "Edit|Write|MultiEdit"hook posted earlier doesn't seeBash. Companion hook — scoped to common write-verbs to avoid false-positiving on legitimate main-repo reads (git log,ls,cat):Caveat matches the earlier hook: regex-based detection misses
dd,install, heredoc-to-file viacat <<EOF > /main/path, andpython3 -copen-for-write that isn't literalopen(. The proper fix is still the resolver-level one already proposed on this thread — applied uniformly toEdit,Write, andBashwhencwdis inside a worktree.---
Addendum — diagnostic signal:
Readtool log corroborates pre-switch path resolutionRunning a local
reads.jsonlaudit trail (logs everyReadtool call's resolved absolute path). During the leaking subagent's active window on the PM repro above (03:28:57 → 03:36:28), the only in-sessionReadrecorded resolved to a main-repo-rooted absolute path. Comparison against three earlier subagents in the same session that resolved correctly:Prior observations on the thread inferred the main-path resolution from post-dispatch state (commit diffs, dirty working-tree files). This is a direct real-time signal: the agent's
Readtool resolved to a main-repo path before anyEdit/Bashcall ran — directly consistent with @mikehenson's 2026-04-18 hypothesis ("paths resolved before the worktree switch retain the main-repo prefix").Orchestrator-side amplifier worth flagging: my workflow has the planner write
PLAN.mdto the main repo, and the executor (spawned later withisolation: "worktree") mustReadit to proceed —PLAN.mdisn't on the worktree branch because the worktree was created atHEADbefore the plan existed. That main-repoReadprimes context with a main-repo path, which becomes the cached resolution for later files. Workflows that pass plans inline as strings, or commit them to the branch pre-dispatch, should be less exposed to this amplifier.---
🤖 Generated with Claude Code
Getting this issue a lot as well
same issue. explore agent ALWAYS finds the files in the root, not in the worktree. This results in Edit being done in the root as well
Just confirming, EditTool (or WriteTool) still leaking ... impossible to work with multiple chats.
Working with desktop app on mac. (Claude 1.15200.0 (250bae) 2026-06-23T05:40:53.000Z)
same here
Same issue. Pollutes the context unnecessarily