preview_start resolves .claude/launch.json from the session cwd, not the calling agent's, so concurrent worktree lanes must contend for one config file
Summary
preview_start resolves .claude/launch.json from the session's working
directory rather than from the working directory of the agent that called it. A
subagent running in its own git worktree therefore cannot have its own preview
configuration: its name argument is matched against the orchestrator'slaunch.json, and the server it gets (or the error it gets) belongs to another
directory tree.
The consequence is worse than a wrong path, because the only way for such a lane
to make preview_start work is to write its own entry into a file that every
other concurrently running lane is also reading and writing. Four lanes each
needing their own port and their own -C <worktree> argument, all editing one.claude/launch.json, is a silent last-writer-wins race: nothing errors, and a
lane ends up serving a sibling lane's worktree under its own port.
Environment
- Claude Code with concurrent
Agentsubagents usingisolation: "worktree"
(each lane's cwd is its own git worktree, either .claude/worktrees/<name>/
or an external checkout).
- Observed 2026-08-24 on macOS during a five-lane wave in a private repository.
- Also reachable with
EnterWorktreerather than agent isolation, since the
distinguishing factor is only that the caller's cwd differs from the session's.
Minimal reproduction
- In a git project, create
.claude/launch.jsonwith one configuration that is
specific to the project root:
``json``
{
"version": "0.0.1",
"configurations": [
{
"name": "root-web",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "3000"],
"port": 3000
}
]
}
- Create a worktree and give it a different configuration under its own
.claude/launch.json, with a distinct name and port:
``bash``
git worktree add ../proj-lane -b lane
mkdir -p ../proj-lane/.claude
cat > ../proj-lane/.claude/launch.json <<'JSON'
{
"version": "0.0.1",
"configurations": [
{
"name": "lane-web",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "3101"],
"port": 3101
}
]
}
JSON
- From a session opened at the project root, launch a subagent whose cwd is
../proj-lane (isolation: "worktree", or any agent invoked with that cwd),
and have it call:
````
preview_start({ name: "lane-web" })
Observed. The call does not find lane-web. It resolves.claude/launch.json from the session's cwd — the project root — so the only
configuration in scope is root-web, and the attempt proceeds against that
config's port 3000 (in our case failing on it, since the orchestrator already
held 3000).
Expected. preview_start resolves .claude/launch.json from the calling
agent's own working directory, so lane-web is found and started with cwd../proj-lane on port 3101. Falling back to the session's file when the caller's
tree has none would be a reasonable addition; reading the session's file *instead
of* the caller's is the defect.
Why the fallback is not a workaround
The natural workaround — adding a lane-web entry with"cwd": "<absolute worktree path>" to the session's .claude/launch.json — is
what makes this a data-integrity problem rather than an ergonomics one. With N
concurrent lanes:
- every lane needs a distinct port and a distinct absolute
cwd; - all N write the same file, and no write fails;
- the last writer wins, and an earlier lane's
preview_startthen spawns a
server rooted in another lane's worktree, on a port that lane believes it owns.
Nothing in that sequence produces an error, so the first signal is a developer
looking at a preview pane that is serving the wrong branch's code.
Suggested fix
Resolve .claude/launch.json relative to the calling agent's cwd, walking up to
the enclosing git worktree root, and only then fall back to the session's
project root. That makes a lane's preview configuration lane-private by
construction and removes the shared file entirely.
Related
#86039 reports the
mirror-image defect one level down: relative cwd values inside launch.json
resolving against the session worktree on UI-initiated launches. That issue
notes preview_start resolves those values correctly against the project root —
this issue is about the discovery of the launch.json file itself, whichpreview_start resolves from the session cwd rather than the caller's, and is
independent of how paths inside the file are then interpreted.
Tracked downstream as
narduk-enterprises/agent-infrastructure#845,
where the local mitigation is a PreToolUse hook that refuses a write to a.claude/launch.json outside the calling agent's own working tree — it blocks
the dangerous workaround, but it cannot make preview_start usable from a lane.