Desktop preview panel: relative launch.json cwd resolved against session worktree; stop orphans the dev server; panel state ignores tool-started servers

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 0 comments · opened Aug 12, 2026

Environment

  • Claude Desktop on macOS (local sessions; bundled claude-code harness 2.1.222)
  • Project uses .claude/launch.json dev-server configs and worktree-based sessions (EnterWorktree)
  • Dev server: astro 7.0.7 via pnpm dev (but the core bugs are server-agnostic)

Summary (plain language)

Our project's preview settings tell Claude Desktop which folder to run the dev server from, using a path relative to the project. When the AI agent works inside a session worktree (an isolated copy of the project) and a person starts or restarts the preview with the buttons in the Desktop preview panel, the app looks for that folder inside the isolated copy instead of the real project folder — and the preview fails to start. Around that core defect we hit five more, all reproducible in one scenario; together they routinely leave a non-technical user with a dead preview pane while a perfectly healthy server keeps running unseen.

The same launch started by the agent through the preview_start tool works correctly, because that code path resolves the folder against the project root, as documented. So the app resolves the same config differently depending on which button starts it.

Minimal reproduction

  1. In a git project, add .claude/launch.json with a relative cwd pointing at a folder that exists only in the project root (gitignored, so worktrees don't have it):

``json
{
"version": "0.0.1",
"configurations": [{
"name": "Docs",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "4323"],
"port": 4323,
"cwd": ".claude/preview-root"
}]
}
``

mkdir .claude/preview-root in the project root; add it to .gitignore.

  1. Open the project in Claude Desktop, start a session, have the agent call EnterWorktree (session now works in .claude/worktrees/<name>/).
  2. Ask the agent to start the preview via preview_start — works: the server spawns with cwd <project>/.claude/preview-root.
  3. Start/restart the same server from the preview panel UI (server picker / restart button) — fails.

The six defects (all from ~/Library/Logs/Claude/main.log, 2026-08-11, one project)

1. UI-initiated launches resolve the relative cwd against the session's worktree, not the project root.

21:07:39 [Preview] Starting server with config {
    cwd: '<project>/.claude/worktrees/<name>/.claude/preview-root', ... }

Minutes earlier the same session resolved the same config to <project>/.claude/preview-root and launched successfully. The resolution base flips between the project root and the session cwd depending on how the launch is triggered.

2. The spawn failure is attributed to the wrong thing.

21:07:39 [Preview] Process spawned { pid: undefined }
21:07:39 [Preview] Attempt 3/3 failed: Failed to start preview server:
    spawn /Applications/Claude.app/Contents/Helpers/disclaimer ENOENT

The disclaimer helper exists; the ENOENT is the nonexistent working directory (posix_spawn reports a bad cwd against the executable path). The message sends you debugging a broken app install instead of a missing folder.

3. A relaunch kills the healthy running server before the replacement has spawned successfully.

21:07:39 [Preview] Process exited { code: 143 }        ← healthy server killed first
21:07:39 [Preview] Attempt 3/3 failed: ...             ← replacement never started

A failed relaunch therefore leaves the user with no preview at all, where before the click they had a working one.

4. Stopping a server kills only the spawned wrapper, not the process tree. The app spawns e.g. pnpm dev and on stop terminates that pid; the actual dev server (a grandchild — astro here) can survive re-parented to launchd, still holding the port. Astro 7 then refuses every relaunch of that project with "Another astro dev server is already running" (its .astro/dev.json still names the live pid), while the panel shows no server running. Killing the process group on stop would fix it.

5. The preview panel's button state ignores agent-started servers. A server started via the preview_start tool is present in the app's own active-servers store with status: "running", yet the panel renders the config as inactive. The user cannot stop the running server from the UI, and clicking the button starts a duplicate launch instead — which triggers defect 6.

6. After a failed duplicate launch, the pane binds to the dead port. With an agent-started server healthy on port 4321, clicking the config (which the panel wrongly shows as inactive, see 5) spawns a duplicate on a random free port (autoPort picked 60302). The duplicate exits immediately (astro's one-server-per-project rule), but the preview pane still navigates to the duplicate's port and retries ERR_CONNECTION_REFUSED until giving up:

21:59:38 [Launch] Updating active servers store { count: 2, servers: '[
  {"port":4321,"status":"running", ...},
  {"port":60302,"status":"starting","cwd":"<project>/.claude/worktrees/<name>", ...}]' }
21:59:41 [Preview] Process exited { serverId: '<duplicate>', code: 1 }
21:59:41 [PreviewContext] Load failed, retry { errorDescription: 'ERR_CONNECTION_REFUSED', attempt: 5, maxRetries: 5 }

The user sees an empty pane while the healthy server keeps serving on 4321 the whole time.

Expected

  • Relative cwd in launch.json resolves against the launch/project directory on every code path (as documented and as preview_start already does).
  • Spawn errors name the missing working directory, not the command.
  • A relaunch keeps the old server until the replacement has spawned successfully.
  • Stop terminates the whole process group.
  • The panel reflects tool-started servers (running state, stop affordance) instead of launching duplicates.
  • If a launch fails, the pane falls back to a server that is actually running.

Workarounds we ship in our repo (for context)

  • A self-pointing .claude/preview-root symlink inside every worktree, so both resolution bases land in the session's own tree (defuses 1–2).
  • A pre-launch hook that stops an orphaned dev server — recorded pid alive but re-parented to launchd (defuses 4).

Defects 3, 5, 6 are not addressable from project code.

View original on GitHub ↗