Studio's chat on your plan, on the web, through your own computer
Status: built, 2026-10-05, opt-in and off by default. Builds on Claude on the web over MCP
(m12-remote-mcp.md) and the desktop's opt-in chat on a plan (claude-plan.md, "Studio's
chat on the plan").
On the desktop, Studio's own chat can run on the person's Claude Pro or Max plan, through an
unmodified claude -p of the Claude Code on their computer, when they turn it on. The web
edition's chat runs in the service's daemon, which may never touch a plan: Anthropic does not
let a product hold a plan's sign-in or route a plan's use for its users, and the service never
sees a Claude login. So the web edition's chat on a plan runs where the desktop's does, on the
person's own computer, through a small program of Reelwright's there, the bridge:
browser (web Studio) ──wss──▶ reelwright host ◀──wss── reelwright claude bridge (the person's
chat.send / chat.event │ (bridge.ts) │ own computer)
│ ├─ claude -p (unmodified Claude Code,
│ │ signed in with their own plan)
│ │ │
◀──── MCP, https ─────────┼─────┘ its only tools: the service's
<base>/mcp?show=<id> │ MCP endpoint, as the person
(m12-remote-mcp.md)
- The person's choice. Off by default. In the web edition's Connect Claude, "Use your Claude
subscription", the switch "Also use my plan for Studio's chat (experimental)", with the same
disclosure as the desktop's: the chat runs on their own computer, through the Claude Code
there, on their plan; messages count against its limits; Anthropic's terms for Claude Code
apply; Reelwright never sees the sign-in; and Anthropic's rules ask for its agreement before
this is offered by default, so it stays their choice. It is kept per person
(
plan_chatinreelwright.db;POST /api/mcp/plan-chat {on}, auditedmcp.plan_chat_on/_off), and changing it closes their sockets (1012) so Studio says hello again. - The bridge is
reelwright claude bridge --service https://reelwright.diy/app, run on the person's computer (core/src/claude/bridge.ts,runBridge). It signs in with one of their personal tokens (rwp_…: a pairing code's, below, kept by the helper; or made in Connect Claude, given inREELWRIGHT_TOKENor a file--token-filenames: never on a command line), over https only (http only to this computer's loopback). It asks the service where its socket is (GET <base>/bridgewith the token:{"ws"}, on the service's own origin, since the proxy in front carries no sockets), keeps that WebSocket open, outward, and comes back after a drop (1 s, doubling to 30 s). A token the service refuses is not tried again: the 401 says why (reason:disconnectedby the person, with Disconnect or Sign out everywhere;rotatedout by newer connections;expired;inactive, their account;unknown), kept per token hash inmcp_endedonce the connection is gone. The bridge says so (refusedwith thatreason, with--json) and ends with exit 1, for a new token. On connecting it tells the service what its Claude Code is (installed, version, signed in with a plan and which:claude auth status, never the account), which Connect Claude shows. - A turn. For a person whose chat is on their plan, and only an owner or editor of the show
(a viewer's or reviewer's
chat.sendgoes on to the daemon, which refuses it), the host's pipe answerschat.senditself:{"turnId":"turn.p<12 hex>"}, then hands the bridge{"type":"turn","turnId","show":{"key","title"},"text","context"?,"focus"?,"dryRun"?}(contextis where the person is in Studio as the message carried it;focusthe note the service says of it without the show). The bridge asks the service'sstudio_contextwith that context for the page's brief, drawn from the show as a daemon's showrunner hears it, and says it before the message; it saysfocuswhen the brief does not come (an older service). The bridge keeps a chat per show: the desktop'sPlanChatover aPlanShowwhose tools are the service's MCP endpoint (RemoteShow): it lists them there (decide tools are left out of Claude Code's, as on the desktop), reads the show's context through the endpoint's resources (reelwright://show,reelwright://doc/<id>), and gives Claude Code one MCP server,reelwright-chat,{"type":"http","url":"<base>/mcp?show=<id>","headers":{"Authorization":"Bearer ${REELWRIGHT_MCP_TOKEN}"}}, on its command line, the token itself in Claude Code's environment only, which Claude Code fills the header from: no file holds it, so none is left when a bridge is killed outright (one an older bridge left is removed when the next starts), and no argument names it. The endpoint's?show=starts that session on the turn's show. Claude Code's tool calls act on the show on the service as the person, with their role, through its daemon; what it proposes waits as a proposal,bythe person, and the Studio open on the show hears it land. - Events back. The bridge sends
{"type":"event","turnId","event"}for each ofPlanChat's chat events: text as it streams, a tool card for each call and its result, a proposal card (read throughreelwright://changes/<id>), anddoneorerror. The service numbers them (seq) and passes them to the Studio socket that asked aschat.event, exactly as the daemon's own chat sends them. An event that is not a validChatEventends the turn withbridge.bad_event. - Cancel.
chat.cancelfor a bridge turn goes to its bridge ({"type":"cancel"}), which stops the turn as the desktop does (SIGINT to Claude Code); a Studio socket that closes cancels its turns. - One turn at a time per person (a second is
chat.busy), as the daemon's chat. - No bridge. The turn's only event is an error,
bridge.offline: "Studio's chat is set to use your Claude plan through your own computer, and Reelwright's bridge is not running there. Open Reelwright Studio on your computer, signed in, and turn on “Let this computer run my web chat on my Claude plan” in Connect Claude. Or run it from a terminal there:reelwright claude bridge --service …, with a token from Connect Claude. Or switch Connect Claude to an Anthropic API key." Studio's chat offers Connect Claude beside it. A bridge that goes away mid-turn ends it withbridge.lost. - Chat is offered. The answer to such a person's
session.hellogainschatin its capabilities whether or not they connected an API key: the bridge, online or not, takes the turns.
Paired from the web Studio, no token pasted (claude-easy, 2026-10-13)
The web Studio's chat, while Claude is not connected, offers Chat using my Claude plan (needs a
small helper on your computer) (RwClaudeSetup, docs/audits/claude-setup-2026-10.md):
- A pairing code. Pressed, Studio asks the service for one (
POST /api/mcp/pairings, the person's session and Studio's header, auditedmcp.pairing_made): eight letters from an alphabet with nothing that reads as another (no 0, O, 1, I), shownABCD-EFGH, good once for ten minutes, kept in the service's memory only; a new one replaces the person's earlier ones. The answer carries the one command to copy:reelwright claude bridge --service <base> --pair ABCD-EFGH. - The helper trades it.
--pairposts the code toPOST /api/mcp/pairings/redeem(no sign-in: the code is its one credential) with the computer's name, and gets a personal token named "Helper on" ( mcp.helper_paired), which it keeps owner-only inreelwright/bridge-tokenunder the config folder (REELWRIGHT_BRIDGE_TOKEN_FILEnames another), the way the API key is kept. The same command without--pairstarts the helper again later from it. A used, unknown or expired code is refused in words that say to make a new one in Studio's chat; one network address that tries ten codes nobody made in ten minutes waits (429), even for a good one, and one person makes ten codes in ten minutes at most. The token is the account's that made the code, one of its connections (Disconnect in Settings › Claude ends it at once), and is only ever in the redeem answer's body and the helper's file: never a URL, a command line or a log. - The page notices. While the command is shown, Studio asks
GET /api/mcpevery few seconds (and on Check): once the helper is online, signed in with a plan, it turns the person's chat onto the plan (POST /api/mcp/plan-chat {on:true}), the service has Studio say hello again, and the chat's header says "Connected: your Claude plan, through your computer". A helper that stops later is said there, in red, with the command that starts it again.
No session-protocol change: the pairing is the service's HTTP API and a flag of the command.
Tests: core/test/host-bridge.test.ts "pairing a helper from the web Studio…"; the web
Studio's side engine/tests/app/rw_studio_web_test.gd "the plan on the web pairs a helper…",
and end to end in a browser tools/web/claude-setup-check.ts.
Studio on the desktop keeps the bridge
Signed in to Reelwright (M13's device sign-in), the installed Studio runs the bridge itself, so nothing is typed or pasted:
- The switch. Connect Claude's plan page offers "Let this computer run my web chat on my Claude plan" once Claude Code here is installed and signed in with a plan, and only to a Studio signed in to Reelwright. Off by default, with the same disclosure as the web switch, plus that Studio makes the bridge's token and keeps it on this computer.
- The token. Turning it on, Studio's device session makes a personal token for the bridge
(
POST /api/mcp/tokens {"name":"Bridge in Studio on <OS>"}) and puts the person's chat on their plan (POST /api/mcp/plan-chat {on:true}). The token is kept with the account, inreelwright-account.json(mode 0600), and read only throughRwAccount.bridge_token(). Turning it off, or signing out, sets plan chat off, revokes the token (DELETE /api/mcp/connections/<id>) and forgets it. - A refused token. When the service refuses the bridge's token, the bridge says
refusedwith the reason and ends, andRwPlanBridgedoes not start it again with that token. The person's own Disconnect on the web (disconnected) means stop: Studio turns the switch off at once, forgets the token and says "You disconnected the plan bridge on the web. Turn it on here to start again.", under the switch and as a notice with Connect Claude. Any other reason (expired, rotated out) is not the person's doing: Studio makes a new token through its device session (RwAccount.renew_bridge_token), once, and starts the bridge with it; refused again, or with no token made, it turns the switch off the same way ("Reelwright refused the plan bridge's token, and a new one too. Turn it on here to start again."). No endless retry. Turning itself off leaves the person's web choice alone: plan chat stays on, so their next web turn saysbridge.offline, which names Studio's switch first. - The process.
RwPlanBridgerunsreelwright claude bridge --service <base> --token-stdin --json --runner "Studio on <OS>"while Studio is open: the token is the first line of its input, never a command line or a variable;--jsonsays each change of its state as a line (connecting,onlinewith what Claude Code here is,turnwith the show it answers in,offlinewith why,stopped), which Connect Claude shows under the switch ("Running: your chat on Reelwright goes through Claude Code here, on your Max plan."). A bridge that exits is started again after 2 s, doubling to a minute. Turning the switch off, signing out or Studio closing stops it: SIGTERM, so it closes its chats, then a kill only if it lingers past 3 s. Its input ending stops it too, so Studio crashing ends it. - What the web shows. The bridge's hello names its runner (
runner, at most 80 characters), and the web edition's Connect Claude says "The bridge is running in Reelwright Studio on Linux, since …", and only that while the installed Studio runs it. Otherwise it names Studio's switch first, and the terminal's way (a token inREELWRIGHT_TOKEN, the command to copy) waits folded behind "Run it from a terminal instead". - A show on Reelwright in the installed Studio has no key on this computer: its chat's empty state offers the account's API key, kept on the service, or this computer's bridge, and Connect Claude for that show keeps the key on the service.
Who gets what
- A person's turns go to their own bridge and nobody else's: bridges are kept by the person their token signs in, and a turn by the person who sent it. A bridge's events reach only the turns it was given (an event for another turn id is dropped).
- The bridge's socket takes a personal token only: an access token Claude Code or a Claude
app signed in with works at
/mcpand nowhere else. A newer bridge of the same person replaces the older (4000). Revoking the token, or Sign out everywhere, closes its bridge. During maintenance only the owners' bridges connect. The service pings each bridge every 30 s and lets a silent one go. - Nothing about a plan reaches the service: no sign-in, no token of Anthropic's, no model traffic. The service carries the person's own messages from their browser to their own computer and Reelwright's chat events back.
Proof
core/test/host-bridge.test.ts (real daemons): the hello gains chat for a plan-chat person
only; a turn reaches its person's bridge with the show and what Studio has open, and nobody
else's; another bridge's events for it are dropped; events come back numbered and in order; a
malformed event ends the turn; no bridge says bridge.offline with the command; cancel reaches
the bridge and a second turn meanwhile is chat.busy; a bridge going away ends the turn
(bridge.lost); an OAuth access token that works at /mcp is refused at /bridge; revoking
the token closes the bridge; and runBridge with a stand-in claude (signed in "with a
plan") whose turn really calls revise_character through the endpoint, so the chat's events
are text, the tool call, its result, the proposal and done, the proposal in the turn's show
even after the person opened another in another tab. tools/web/bridge-check.ts proves it on
a real service with reelwright claude bridge as its own process and the same stand-in
(never the real Claude Code, never a model): node tools/web/bridge-check.ts --out out/bridge-check.
The installed Studio's bridge: core/test/host-bridge.test.ts runs reelwright claude bridge --token-stdin --json --runner against a real service (its states as JSON lines, the runner in
GET /api/mcp, the token in no output, and it stops when its input ends), and runBridge
says it waits again once a turn is done. engine/tests/app/rw_studio_reelwright_test.gd
turns the switch on against a stand-in service and core (the token made, plan chat on, the
token on the bridge's input and in the 0600 account file only; off revokes it and stops the
bridge), and starts a bridge left on again with Studio, which stops it when Studio closes (a
stand-in deaf to its input ending, so only Studio's own stop ends it); a token refused as
expired is made again once and the bridge runs with the new one, refused again the switch
goes off with the reason, the bridge run twice and no more, and one the person disconnected
turns the switch off at once with no new token. host-bridge.test.ts has the service say
why it refuses (rotated, disconnected, signed out everywhere, expired a year on, unknown),
the bridge end with exit 1 and the reason when its token is disconnected or rotated out (the
service closing a rotated connection's bridge at once), and bridge.offline name Studio's
switch before the command. engine/tests/ui/rw_connect_dialog_test.gd has the web dialog fold the terminal's
way behind its disclosure, and show only the running line for a Studio-run bridge.
tools/web/studio-bridge-check.ts proves it whole: a real service, the installed Studio
headless in Xvfb (engine/tests/support/rw_bridge_tour.gd) signing in with the mailed code
and turning the switch on, the web export in Chromium as the same person seeing the bridge
online in Connect Claude and sending a chat turn whose answer (text, tool call, proposal,
done) comes back from the bridge Studio runs, with the stand-in claude; the person making
newer tokens until the bridge's is rotated out, and Studio making a new one and answering the
next turn; then the person's Disconnect, Studio turning its switch off at once and saying
why, and the browser's next turn saying bridge.offline with Studio's switch first: node tools/web/studio-bridge-check.ts --web-studio out/web-studio --out out/studio-bridge-check.
Not yet
- Anthropic's agreement. As on the desktop, offering this by default, or to everyone, needs Anthropic's Commercial Terms and its approval first; until then it stays an experimental choice each person makes for themselves.