npx @reticlehq/server doctor first. It checks Chromium, the daemon, the port and the connected sessions in one command, and it names the problem in most cases. If sessions reads zero, your app is not reaching the bridge, and a port mismatch is the usual reason.
Reticle is a verification layer that runs a dev-only SDK inside your running web app. Two halves have to be working: your agent needs the MCP server registered, and your app needs the SDK connected to the local daemon. Almost every failure below is one of those two halves being absent.
Most of these were hit while writing these docs. The diagnostics quoted are real output.
Start here
sessions line is the one to read first. Zero connected pages is a failing check, not a footnote, because it is the state a setup most often stalls in: everything installed, the daemon up, and nothing having dialled in. When it is zero, doctor prints the same diagnosis described below underneath the checklist, so you do not have to call a tool to see it.
”No browser session connected”
By far the most common. Reticle’s own diagnostic is unusually good here. It tells you whether the wiring was ever correct:why. If it says one was connected earlier, stop debugging your config and just reopen the tab. If it says nothing ever connected, the wiring itself is wrong, so work through the list below.
If nothing ever connected:
- Is the app running, and open in a browser? If nothing is serving it, see Who starts the dev server: this is the agent’s job, not a question to put to you.
- Is
reticle.connect()actually executing? Check the dev guard is true. - Port mismatch. This is the big one.
Who starts the dev server
The agent does. If no dev server is listening, the agent reads your project’s own dev script out ofpackage.json, runs it in the background, tells you in one line that it is running and how to stop it, and carries on. It used to be told the opposite, “Reticle never starts the dev server for you, that’s your job”, and agents obeyed it exactly: they read the rule, declined to start anything, and ended the turn with nothing verified.
The daemon still does not do it, on purpose. A build process started by a long-lived background daemon is invisible to whoever’s machine it is running on, was never consented to, cannot easily be stopped, and orphans when the daemon exits. It would also mean a piece of standing infrastructure runs arbitrary package.json scripts. An agent already has shell access in your repo, already runs install and build commands, and runs inside a host that asks you before it does. A dev server the agent starts is in the transcript, attributable to a step you approved, and stoppable.
The agent does not have to work out the command. When reticle_session { action: "list" } comes back with nothing connected, it carries a next_action. Its action names the case: daemon_split (the app is on a different daemon; start nothing), start_dev_server, run_init, open_app or reopen_app. Where one applies, it also gives the literal command and the port. In a project that was never wired, it also carries next_step. Its lease checks the running app now with nothing installed. Its consent is the question to ask before reticle init changes files. Run init only when the user agrees. The command is read from this project’s own scripts, so a project with no recognisable dev script gets no command rather than a guess.
The guards, which apply wherever this instruction appears:
- Never start a second one. If something is already listening on the app’s port, it is adopted.
- Never guess the command. It comes from
package.jsonscripts. If there is no recognisable dev script, the agent says so and stops rather than inventing one. - Never kill anything: not a dev server, not a daemon, not a port holder, including one it started.
- Background it, and say so. You must know a server is running and how to stop it. A dev server you do not know about is the same failure one step later.
- The permission prompt is your agent host’s. Nothing in Reticle bypasses or auto-approves it, and you can always decline.
The dev server runs in a container
Everything below the CLI assumes the daemon and the dev server share a filesystem. Three things break when they do not (Docker, a devcontainer, WSL, or a dev server on another machine), and all three look identical from the page: the SDK loads, the socket opens, and no session appears. 1. The pairing token. The daemon writes~/.reticle/pairing-token on the host. The build plugin reads it from $HOME inside the container, finds nothing, mints its own, and inlines that into every page, which the bridge then refuses. The console says bridge refused the connection: authentication failed, and reticle status names this case in full.
Mount the host’s token read-only and point the plugin at it:
.reticle.json, the file reticle init writes. If that file is not visible inside the container (it is not in the build context, or the source is not mounted), the plugin falls back to deriving one from the absolute project root. That is /app inside and your host path outside, so the two disagree. Make sure .reticle.json is mounted with the source, or pass the id explicitly: reticle({ projectId: 'your-id-from-.reticle.json' }).
3. The dependencies. A container that installs its own node_modules into an anonymous volume will not see the two packages init added by restarting. Rebuild the image and renew the volume:
reticle init prints all three as a notice when it finds a Dockerfile or a compose file near the app, so on a fresh install you should meet this page having already been told.
The port mismatch
Your app dials a port; the daemon listens on one. When they disagree, the app runs perfectly and silently never connects. The browser console says so:reticle_lease, which is the path init steers it to, the daemon reads that address off the page’s own console, compares it with the port it is bound to, and puts the mismatch in the tool result:
bridge port from doctor and set whichever is wrong:
RETICLE_PORT when the dev server starts. You can also set reticle({ port: 4400 }) in vite.config, or pass reticle.connect({ url: 'ws://localhost:4400/reticle' }) if you wired connect() by hand.
The click did nothing
Check whether the tab is throttled. Every response tells you:-
Drive a lease. A context Reticle owns, never backgrounded:
Release it when you finish:
{ "action": "release", "sessionId": "lease-…" }. - Focus the tab.
-
Fail loudly instead of pretending. Pass
refuseWhenThrottled: trueto your action.
occluded on the element. A click that lands on a modal overlay is the other half of “the button doesn’t work”:
“ref no longer resolves to an element”
reticle_act_and_wait { until } for actions that change the page, so the next ref is taken after things settle.
The verdict says unknown
verified: "unknown" means Reticle drove the app and could not tell. It is not a pass.
The usual cause is a throttled tab: throttling suppresses the quiescence detection used to decide the page has settled, so even a successful action can come back unsettled. Drive a lease and try again.
If it persists on an unthrottled context, that is worth reporting. Reticle’s own response says so:
A negative result you are not sure about
Before concluding “no request fired”, read the buffer note:The agent cannot find an element
reticle_look { action: "page", mode: "interactive" }shows everything actionable.- Add a
data-testidfor a stable handle. - Narrow with
scope. A CSS selector or a ref.
Assertions are flaky on async UIs
- Use
timeout_msonassertandwait_for. - By default an assertion counts only the events after your last act. Pass a
sincecursor fromreticle_actorreticle_observeto set that window yourself.
reticle_look { action: "state" } returns nothing
storeNames empty means no store was registered. reticle_look { action: "state" } reads what your app registered in src/reticle-dev.ts.
Source file is not resolving on React 19
React 19 dropped_debugSource. Wire up @reticlehq/babel-plugin. Or just use @reticlehq/vite-plugin or @reticlehq/next, which both include it. Without it you get component identity but no file:line.
A lease on an https dev server with a local certificate
A dev server onhttps:// with a self-signed or mkcert certificate makes reticle_lease { "action": "acquire" } fail with net::ERR_CERT_AUTHORITY_INVALID: the browser does not trust a certificate that no CA vouches for. The result says so and names the fix. If the server is yours, acquire again with ignoreHTTPSErrors set:
The page never connects under a strict CSP
A build that sends a strict Content-Security-Policy blocks the bridge socket. A production build that you run locally (next start, vite preview) often sends one. The app runs, and the SDK never connects, with no error in the app. Add the bridge to connect-src, with your bridge port if it is not 4400:
script-src stops a pasted connect snippet before it runs: serve that snippet as an external module. reticle doctor reads the policy that the page serves and names the line to add, and a lease result says the same. For production mode, add the wss:// bridge from the ticket instead. Desktop apps have their own note in Desktop.
ERESOLVE installing next to a prerelease
@reticlehq/next declares peer next >=13 and @reticlehq/react declares peer react >=18. On a Next canary or React RC, npm refuses. See the prerelease notes.
The tool list is empty
Some clients read their MCP tool list once, at startup, and never again. Cursor Cloud Agent is the usual report: the daemon is listening on port 4400, the app is instrumented, andreticle_* is absent for the whole session. Restarting the client picks the tools up when a restart is possible. When it is not, the same tools are on the HTTP transport.
GET /mcp/sse opens the session. The first SSE frame is an endpoint event whose data is the path to POST to, including the session id. POST JSON-RPC there. The POST answers 202 with an empty body, and the result comes back on the SSE stream. The handshake and a working client are in HTTP transport.
Restarting things
Sessions piling up
Stale sessions pointing at dev servers you restarted an hour ago look completely plausible in a list, and one of them will get picked.reticle_session { action: "end" } when the whole task is done, { action: "yield" } when pausing. A yielded session revives on your next action; an ended one stays ended.
Frequently asked questions
Why does reticle_session { action: "list" } return nothing when my app is clearly running?
Why does reticle_session { action: "list" } return nothing when my app is clearly running?
Your app running is not the same as your app being connected. The SDK has to dial the daemon’s
bridge port over a WebSocket. If
npx @reticlehq/server doctor shows the daemon up and
sessions at zero, either reticle.connect() never executed (check the dev guard is actually
true) or the app dialled the wrong port. The browser console names the exact address it tried.Which port should my app dial?
Which port should my app dial?
The bridge port, which defaults to
4400. It is not your dev-server port. A dev server on
4312 and a daemon on 4400 is the normal, correct arrangement. doctor prints the bridge port
under bridge port.I restarted my agent and the reticle tools still are not there.
I restarted my agent and the reticle tools still are not there.
If you registered the MCP server while the agent was running, restart the agent process itself:
quit and reopen Claude Code, reload the window in Cursor, or press Start in
.vscode/mcp.json in
VS Code. /mcp only manages servers that were already loaded, so it cannot discover a new one. If
the tools are still missing, run claude mcp list and check reticle is present.Does a verdict of unknown mean the test failed?
Does a verdict of unknown mean the test failed?
No, and it does not mean it passed either.
verified: "unknown" means Reticle drove the app and
could not tell what happened, usually because the page never settled inside the observation
window. Report it as unknown. If you hit one on an unthrottled context and believe it is wrong,
that is a Reticle defect worth sending with reticle_session { action: "feedback" }.Is it safe to kill port 4400 to restart things?
Is it safe to kill port 4400 to restart things?
No.
lsof -ti tcp:4400 | xargs kill -9 matches your agent’s MCP proxy as well as the daemon and
takes the connection down with it. Use npx @reticlehq/server stop, or at minimum add
-sTCP:LISTEN so only the listener matches.Still stuck?
reticle feedback --agent works even when nothing is installed and the daemon will not start.