How does an external SDK consumer connect to a running Claude Code session over the WebSocket/SSE bridge, authenticate, send a user message, receive streamed assistant events, and disconnect cleanly

en
100.0% sentence pass·24/24 cited·24/24 citations valid·113 fn·0 dec·919 sem
Bridge & Remote ConnectivityApplication Bootstrap & EntrypointsAuthentication & Credential Storage

Overview

The whitelist and source slices cover the CLI's outbound bridge/remote-control flow (local CLI → Anthropic session ingress) and inbound SDK stream-json consumers, but they do not contain an inbound WebSocket/SSE server that an external SDK consumer would "connect to" on a running Claude Code session . The closest real flow is the reverse: the CLI exposes itself as a bridge environment that relays SDK messages to/from a remote session .

Steps

  1. The external entry is claude remote-control, dispatched from the CLI bootstrap which gates on OAuth tokens and policy before handing off to bridgeMain . bridgeMain parses args, enables configs, initializes sinks, and resolves the bridge access token before any network I/O .
  2. Authentication uses the stored claude.ai OAuth token via getBridgeAccessToken; if absent, bridgeMain exits with BRIDGE_LOGIN_ERROR, and the REPL-side variant additionally proactively refreshes expired tokens before registering .
  3. After auth, the bridge registers an environment and enters runBridgeLoop, which long-polls the ingress for work items, tracks active sessions, and heartbeats each work item with its per-session ingress JWT .
  4. Inbound SDK messages arriving over the ingress transport are parsed by handleIngressMessage, which dedupes echoes via recentPostedUUIDs, routes control_response to the permission callback, control_request to the control handler, and user SDKMessages to onInboundMessage .
  5. A user message is then fed into the local query loop via QueryEngine.submitMessage, which builds the system prompt, wires canUseTool, and yields SDKMessage events (assistant, stream_event, result, status) as an async generator .
  6. For the inverse direction — an SDK/React consumer attached to a remote session — useRemoteSession's onMessage receives each SDKMessage, drops echoes by uuid, handles task_started/task_notification/status/compact_boundary system events, and converts the rest via convertSDKMessage .
  7. convertSDKMessage dispatches by msg.type to per-kind converters — convertAssistantMessage, convertStreamEvent, convertResultMessage, convertInitMessage, convertStatusMessage, convertCompactBoundaryMessage — producing renderable REPL messages .
  8. Headless SDK consumers using stream-json stdio instead go through runHeadlessrunHeadlessStreaming, where handleInitializeRequest processes the initial control_request (systemPrompt, hooks, agents, jsonSchema) and replies with a control_response carrying commands/agents/models .
  9. Subsequent prompts are queued and drained by drainCommandQueue, which batches consecutive prompt commands sharing a workload into one ask() turn and emits replay events for merged uuids .
  10. Clean disconnect on the bridge side happens when runBridgeLoop's abort signal fires: pending cleanups (stopWork, worktree removal) are awaited, heartbeats stop, and sessions are archived; on the REPL-consumer side onMessage observes isSessionEndMessage and flips loading off .
  11. The whitelist does not expose a server-side WebSocket/SSE accept path inside this repo — the ingress is an external Anthropic service, and this CLI is only a client of it, so the literal "SDK consumer connects to a running Claude Code session over WebSocket/SSE" scenario is not answerable from the provided source .

State touched

Decisions

(none in whitelist)