Worktree: Edit/Read tools use main workspace paths instead of worktree paths

Status Open
Maintainer reply None cached
Activity 14 comments · opened Mar 19, 2026

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

  1. Start a Claude Code session that creates/uses a git worktree (e.g. via the automatic branch creation feature)
  2. The system prompt correctly states:
  • Primary working directory: /path/to/repo/.claude/worktrees/name
  • Current branch: claude/name
  1. Ask Claude to edit a file (e.g. workspace_shell.py)
  2. 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:

  1. Resolve relative paths against the worktree root (not the main workspace), or
  2. Validate that absolute paths fall within the worktree and warn/redirect if they don't

View original on GitHub ↗

14 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/17927
  2. https://github.com/anthropics/claude-code/issues/34877
  3. https://github.com/anthropics/claude-code/issues/35230

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

primum-mobile · 5 months ago

👎

ishaan-mehta · 5 months ago

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.

yurukusa · 5 months ago

A PreToolUse hook can detect when Edit/Read/Write targets the main workspace instead of the worktree:

#!/bin/bash
# ~/.claude/hooks/worktree-path-validator.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
[ -z "$FILE_PATH" ] && exit 0

# Check if we're in a worktree
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
[ "$GIT_DIR" = "$GIT_COMMON" ] && exit 0  # Not a worktree

WORKTREE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
MAIN_ROOT=$(cd "$GIT_COMMON/.." && pwd 2>/dev/null) || exit 0

# Warn if targeting main workspace instead of worktree
if echo "$FILE_PATH" | grep -q "^$MAIN_ROOT" && ! echo "$FILE_PATH" | grep -q "^$WORKTREE_ROOT"; then
  echo "WARNING: File targets main workspace, not this worktree." >&2
  echo "  Got: $FILE_PATH" >&2
  echo "  Expected prefix: $WORKTREE_ROOT" >&2
fi
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|Read",
        "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/worktree-path-validator.sh" }]
      }
    ]
  }
}

This compares --git-dir vs --git-common-dir to detect worktree context, then checks if file_path points to the main repo instead. It warns but doesn't block (exit 0), since some cross-workspace reads may be intentional.

dearlordylord · 5 months ago

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

0l0v3r1 · 4 months ago

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)

  1. Start session — worktree is created at .claude/worktrees/<name> on branch worktree-<name>
  2. Enter plan mode — Explore agents are dispatched. They return file paths rooted at the main repo instead of the worktree
  3. Exit plan mode, begin implementation
  4. All Edit/Write calls use the main repo paths from the explore agents
  5. Commit lands on a branch in the main repo, not the worktree branch
  6. Worktree branch stays untouched at its original commit

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

  • Claude Code v2.1.98
  • macOS Darwin 25.4.0
  • Model: claude-opus-4-6
  • Triggered via plan mode workflow (plan → explore → implement)
mikehenson · 4 months ago

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 once MultiEdit) landing in the main repo when the agent's cwd was a worktree at <repo>/.claude/worktrees/<branch>/. Documented in our project's memory/MEMORY.md as the "#576-family cwd-leak class" with three structural variants:

  1. Agent with isolation: "worktree" leaks caller cwd on return — caller's cwd silently switches into the worktree.
  2. Teammate spawns with isolation: "worktree" silently ignore the parameter — teammates inherit the team-lead's cwd; recovery requires teaching every git-touching teammate to invoke EnterWorktree as its first action.
  3. Edit with absolute paths (this issue): even when cwd is correctly the worktree per pwd, an Edit call with a main-repo-rooted absolute path (e.g. /repo/src/foo.py instead 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:

Use absolute paths rooted at /path/to/repo/.claude/worktrees/<branch>/... for every Edit. NEVER use main-repo-rooted absolute paths even if you 'know' the cwd is the worktree — Edit resolves the path independently of cwd.

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 cwd is the disambiguator for relative paths only. The Edit tool's actual behavior is the opposite — absolute paths win silently, cwd doesn't enforce a worktree boundary.

Would strongly value an Edit mode 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."

0xdhx · 4 months ago

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 via diff -rq.

Why this matches the leak pattern, not benign duplication:

  1. The worktree was active 2026-04-18 working on an unrelated task (backfill scripts for project orphans — its own committed history has zero mention of the research files in question).
  2. Main's copies of the research files were mtimed inside the worktree's active window (2026-04-18 16:12 → 21:32; worktree's final commit 23:19).
  3. git worktree add does not copy untracked files from the source tree, so identical untracked files in both locations cannot be explained by worktree creation itself.
  4. Only remaining explanations: (a) writes landed in both paths (this bug), or (b) manual 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:

  • User-level PreToolUse hook on Edit/Write that compares git rev-parse --git-dir vs --git-common-dir and warns (soon: blocks) when file_path escapes the current worktree.
  • SessionStart hook that sweeps .git/worktrees/*/locked for 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.

0xdhx · 4 months ago

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:

Task({
  subagent_type: "gsd-executor",
  isolation: "worktree",
  prompt: "…multi-file change across 6 files…"
})

Agent reported success:

  • Worktree: .claude/worktrees/agent-ab64e217
  • Branch: worktree-agent-ab64e217
  • 2 commits (b96ec31, d40fcf9) landing all 6 files in the worktree branch

Actual state after agent returned — git diff HEAD -- <f> in main vs git 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.js is 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.js was referenced only indirectly in the plan ("extend migration probe"), so the agent had to resolve it via Glob/Read after cwd was the worktree. That resolution path produced a worktree-rooted absolute path, which Edit then 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/Write do 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-dir approach I mentioned earlier — uses cwd string-prefix matching, no git spawn:

#!/bin/bash
# PreToolUse:Edit|Write|MultiEdit — worktree boundary guard
IFS=$'\t' read -r CWD FILE < <(jq -r '[.cwd, .tool_input.file_path // ""] | @tsv')
[[ "$CWD" == *".claude/worktrees/"* ]] || exit 0
WT_ROOT=$(echo "$CWD" | sed -E 's|(.*\.claude/worktrees/[^/]+).*|\1|')
if [[ "$FILE" == /* ]] && [[ "$FILE" != "$WT_ROOT"/* ]]; then
  echo "BLOCK: $FILE escapes $WT_ROOT" >&2
  exit 2
fi
exit 0

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 cwd is inside a worktree, Edit/Write should reject (or rewrite) an absolute file_path that 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 -u the leaked partials in main (preserves for audit)
  • git merge worktree-agent-<id> --no-ff from main — brings in the complete authoritative version from the worktree commits
  • Verify with probes / tests post-merge
  • Drop stash once satisfied

~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 above

Third 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 Edit on a file the subagent hasn't Read yet, agents fall back to shell. From the subagent's own session summary:

…PreToolUse READ-BEFORE-EDIT hook rejected several writes, causing the tool's virtual view to diverge from disk. I worked around by falling back to sed -i / python3 for the actual disk mutations, then verified each write via bash after.

The absolute path piped into sed -i / python3 is pulled from the agent's existing context — typically a main-repo path sourced from an earlier Read of 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 successful Edit after the required Read) 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 see Bash. Companion hook — scoped to common write-verbs to avoid false-positiving on legitimate main-repo reads (git log, ls, cat):

#!/bin/bash
# matcher: "Bash"
IFS=$'\t' read -r CWD CMD < <(jq -r '[.cwd, .tool_input.command // ""] | @tsv')
[[ "$CWD" == *".claude/worktrees/"* ]] || exit 0
WT_ROOT=$(echo "$CWD" | sed -E 's|(.*\.claude/worktrees/[^/]+).*|\1|')
MAIN_ROOT=$(dirname "$(dirname "$(dirname "$WT_ROOT")")")
if echo "$CMD" | grep -Eq "(sed -i|tee |>>? |printf .* > |python3? -c)" \
   && echo "$CMD" | grep -qE "$MAIN_ROOT/[^[:space:]]*" \
   && ! echo "$CMD" | grep -qE "$WT_ROOT/"; then
  echo "BLOCKED: Bash write-verb escapes worktree (#36182 shell variant)"; exit 2
fi
exit 0

Caveat matches the earlier hook: regex-based detection misses dd, install, heredoc-to-file via cat <<EOF > /main/path, and python3 -c open-for-write that isn't literal open(. The proper fix is still the resolver-level one already proposed on this thread — applied uniformly to Edit, Write, and Bash when cwd is inside a worktree.

---

Addendum — diagnostic signal: Read tool log corroborates pre-switch path resolution

Running a local reads.jsonl audit trail (logs every Read tool 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-session Read recorded resolved to a main-repo-rooted absolute path. Comparison against three earlier subagents in the same session that resolved correctly:

02:09:38  /home/dhx/repos/forgefinder/.claude/worktrees/agent-a7e20c0c/scripts/hub.js     ← earlier subagent, correct
02:19:23  /home/dhx/repos/forgefinder/.claude/worktrees/agent-a1d8174e/scripts/hub.js     ← earlier subagent, correct
02:39:48  /home/dhx/repos/forgefinder/.claude/worktrees/agent-a2731866/scripts/hub.js     ← earlier subagent, correct
03:31:45  /home/dhx/repos/forgefinder/tests/hub.test.js                                   ← PM leaker, main path

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 Read tool resolved to a main-repo path before any Edit/Bash call 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.md to the main repo, and the executor (spawned later with isolation: "worktree") must Read it to proceed — PLAN.md isn't on the worktree branch because the worktree was created at HEAD before the plan existed. That main-repo Read primes 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

dylanmoz · 3 months ago

Getting this issue a lot as well

CyanoFresh · 2 months ago

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

oranzo · 2 months ago

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)

romanek-adam-b2c2 · 2 months ago

same here

AdityaRanjanSingh · 1 month ago

Same issue. Pollutes the context unnecessarily