[BUG] preview_start cannot verify Docker-managed dev servers — auto-assigns a port that nothing serves on
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When a project uses Docker Compose (or any externally-managed process) to run its dev server on a fixed host port, preview_start cannot perform real verification against that server.
The tool detects the port is in use, falls back to autoPort and assigns a random high port (e.g. 51769), but nothing is actually serving on that new port — the docker compose up command in runtimeArgs just attaches to the already-running container, which keeps binding to its original fixed port (3050). The assigned port stays empty.
Subsequent preview_eval, preview_snapshot, preview_inspect, and preview_screenshot calls either return empty bodies or chrome-error://chromewebdata/ because the headless browser is pointed at the empty auto-assigned port. Navigating the browser manually to the real port (window.location.href = 'http://localhost:3050/il') also fails because the preview sandbox cannot reach arbitrary host ports.
This breaks the <verification_workflow> expected by the built-in Stop hook: the hook insists on preview_start after any code edit, but there is no way for preview_* tools to actually observe the running site, so every turn ends with a noisy "call preview_start and follow verification_workflow" warning even when the user has already verified changes via curl or an external browser.
What Should Happen?
Either of:
- Detect externally-managed server on the configured port — if launch.json has
port: 3050and the port is already responding with HTTP 200, treat that as the live preview target instead of starting a new server on a different port. The preview sandbox should route its headless browser tolocalhost:3050in that case.
- Let autoPort opt-out explicitly — when
autoPort: false, fail fast with a clear message and instructions, rather than silently assigning a fake port the user can't interact with.
- Support
urlfield in launch.json as an alternative toruntimeExecutable— if the dev server is external (not managed by Claude Code), the user should be able to declare{ "name": "storefront", "url": "http://localhost:3050" }and have preview_* tools target that URL directly, skipping runtime spawning entirely.
Any of these would make preview_* tools usable with the Docker Compose workflow that is the default for Medusa, T3 stack, full-stack Next.js apps with separate API containers, etc.
Error Messages/Logs
# preview_start output (showing the port reassignment)
{
"serverId": "8edd9e34-9682-4f65-b727-b97b254c4601",
"port": 51769,
"name": "storefront",
"reused": false
}
Server started successfully. Configured port 3050 was in use, so port 51769 was assigned instead (autoPort is enabled). The preview is available at http://localhost:51769.
# preview_logs — docker compose detects the container is already running
Container techstore_redis Running
Container techstore_postgres Running
Container techstore_backend Running
Container techstore_storefront Running
Attaching to techstore_storefront
Container techstore_postgres Waiting
Container techstore_postgres Healthy
# preview_eval navigation to the real Docker port returns chrome-error
window.location.href = 'http://localhost:3050/il'; 'navigated'
-> subsequent preview_inspect on body returns empty {"text": "", "boundingBox": {...}}
Steps to Reproduce
- Create a minimal Next.js project managed by Docker Compose that binds a storefront container to
localhost:3050(or pull down any project with this pattern — the PuzzlePC repo, any Medusa v2 storefront template, etc.)
- Start the stack:
docker compose up -d
- Create
.claude/launch.jsonwith a storefront entry that uses docker as runtimeExecutable:
```json
{
"version": "0.0.1",
"configurations": [
{
"name": "storefront",
"runtimeExecutable": "docker",
"runtimeArgs": ["compose", "up", "storefront"],
"port": 3050,
"autoPort": true
}
]
}
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.86
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
## Why this matters
Docker Compose managed dev servers are the default shape for:
- Medusa v2 projects (backend + storefront + postgres + redis as separate services)
- Most full-stack monorepos with T3, Next.js + Prisma, RedwoodJS, etc.
- Any project where the dev loop spans multiple services that need to be on the same Docker network
- WSL + Windows Docker Desktop workflows (which is the setup in this report)
For these workflows, the current preview_* tooling is unusable and the built-in Stop hook becomes noise rather than signal — it fires after every edit but there's no way to satisfy it, so users either learn to ignore the callback error (bad — dulls attention for real problems) or disable the hook entirely (bad — loses the real benefit).
## What I actually use to verify code changes in this setup
In the current session I verified my edits through:
1. `docker compose restart storefront` + `sleep 15` to pick up the new code
2. `curl -s -b '_medusa_cache_id=test' 'http://localhost:3050/PATH' -o /tmp/out.html` to fetch rendered HTML
3. `grep -oE '<pattern>' /tmp/out.html` to check for expected markup
4. The Claude in Chrome MCP (separate from preview_*) to take real visual screenshots against the Docker-managed server when I needed eyes-on verification
This workflow works well but is entirely external to preview_* and cannot satisfy the Stop hook. Exposing a "use an external URL" mode in launch.json — even as an opt-in — would let the Stop hook actually validate against the real running site instead of insisting on a broken path.
## Related context
- Not a duplicate of #39057 (permission mode revert) — different subsystem, different symptom, same user friction pattern of "built-in verification path doesn't fit Docker/WSL workflow".
- launch.json schema isn't well-documented anywhere I could find — would be helpful to have a reference doc enumerating the supported fields (`port`, `autoPort`, `runtimeExecutable`, `runtimeArgs`, and the proposed `url`).This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗