claude-code/spec/09_bridge_cli_remote.md
2026-04-01 01:20:27 +05:30

74 KiB
Raw Permalink Blame History

Claude Code — Bridge Protocol, CLI Framework & Remote Systems

Table of Contents

  1. Bridge System Overview
  2. Bridge Types & Core Data Structures
  3. Bridge API Client
  4. Bridge Configuration & Auth
  5. Bridge Entitlement & Feature Gating
  6. Session Lifecycle: Standalone Bridge (bridgeMain.ts)
  7. REPL Bridge (replBridge.ts / initReplBridge.ts)
  8. Env-Less Bridge Core (remoteBridgeCore.ts)
  9. Transport Layer
  10. Message Protocol (bridgeMessaging.ts)
  11. JWT Authentication (jwtUtils.ts)
  12. Session ID Compatibility (sessionIdCompat.ts)
  13. Work Secrets & CCR v2 Registration (workSecret.ts)
  14. Bridge Pointer: Crash Recovery (bridgePointer.ts)
  15. Permission Callbacks (bridgePermissionCallbacks.ts)
  16. Inbound Messages & Attachments
  17. Session Runner (sessionRunner.ts)
  18. Bridge Debug & Fault Injection (bridgeDebug.ts)
  19. Bridge Utilities
  20. CLI Framework
  21. CLI Transports
  22. Remote Session System
  23. replLauncher.tsx
  24. Configuration Defaults & GrowthBook Flags
  25. Complete API Endpoint Reference
  26. WebSocket & SSE Protocol Reference

1. Bridge System Overview

The "Bridge" (Remote Control) system allows a local Claude Code CLI session to be driven from the claude.ai web application. It creates a bidirectional communication channel between the running CLI process and the cloud backend (CCR — Cloud Code Runner).

Architecture: Two Bridge Variants

V1 — Environment-Based Bridge (env-based)

  • Uses the Environments API (/v1/environments/bridge).
  • Bridge registers as an "environment", polls for "work" (session dispatches).
  • Session transport: WebSocket (v1) or SSE+CCRClient (CCR v2) via HybridTransport or SSETransport.
  • initBridgeCore() in replBridge.ts handles the REPL side.
  • runBridgeLoop() in bridgeMain.ts handles the standalone claude remote-control side.

V2 — Environment-Less Bridge (env-less)

  • No Environments API layer whatsoever.
  • Direct flow: POST /v1/code/sessions → POST /v1/code/sessions/{id}/bridgecreateV2ReplTransport().
  • Only for REPL sessions; daemon/print stay on env-based.
  • Gated by tengu_bridge_repl_v2 GrowthBook flag.

Two Deployment Modes

REPL Bridge (always-on / /remote-control)

  • Initialized by initReplBridge(), called from useReplBridge hook or print.ts.
  • Runs inside the existing REPL process; messages from claude.ai are injected as user input.
  • Lives in bridge/replBridge.ts, bridge/initReplBridge.ts, bridge/remoteBridgeCore.ts.

Standalone Bridge (claude remote-control)

  • Spawns child claude processes per session.
  • Main loop in bridge/bridgeMain.tsrunBridgeLoop().
  • Supports multi-session spawn modes: single-session, worktree, same-dir.

2. Bridge Types & Core Data Structures

File: bridge/types.ts

Constants

DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000   // 24 hours
BRIDGE_LOGIN_INSTRUCTION: string  // "Remote Control is only available with claude.ai subscriptions..."
BRIDGE_LOGIN_ERROR: string        // Full error printed when not authenticated
REMOTE_CONTROL_DISCONNECTED_MSG = 'Remote Control disconnected.'

WorkData

type WorkData = {
  type: 'session' | 'healthcheck'
  id: string  // session ID
}

WorkResponse

The response from polling for work (GET .../work/poll):

type WorkResponse = {
  id: string               // work item ID
  type: 'work'
  environment_id: string
  state: string
  data: WorkData
  secret: string           // base64url-encoded JSON WorkSecret
  created_at: string
}

WorkSecret

Decoded from WorkResponse.secret (base64url JSON):

type WorkSecret = {
  version: number                          // Must be 1
  session_ingress_token: string            // JWT for session-ingress API calls
  api_base_url: string
  sources: Array<{
    type: string
    git_info?: { type: string; repo: string; ref?: string; token?: string }
  }>
  auth: Array<{ type: string; token: string }>
  claude_code_args?: Record<string, string> | null
  mcp_config?: unknown | null
  environment_variables?: Record<string, string> | null
  use_code_sessions?: boolean              // Server-driven CCR v2 selector
}

BridgeConfig

Configuration object passed to the main loop and API client:

type BridgeConfig = {
  dir: string                    // Working directory
  machineName: string            // Hostname
  branch: string                 // Git branch
  gitRepoUrl: string | null
  maxSessions: number            // Capacity for multi-session mode
  spawnMode: SpawnMode           // 'single-session' | 'worktree' | 'same-dir'
  verbose: boolean
  sandbox: boolean
  bridgeId: string               // Client-generated UUID identifying this bridge instance
  workerType: string             // Sent as metadata.worker_type (e.g. 'claude_code')
  environmentId: string          // Client-generated UUID for idempotent registration
  reuseEnvironmentId?: string    // Backend-issued ID to reuse on re-register
  apiBaseUrl: string
  sessionIngressUrl: string      // May differ from apiBaseUrl in local dev
  debugFile?: string
  sessionTimeoutMs?: number
}

SpawnMode

type SpawnMode = 'single-session' | 'worktree' | 'same-dir'
  • single-session: One session, bridge tears down when it ends.
  • worktree: Persistent server, each session gets an isolated git worktree.
  • same-dir: Persistent server, sessions share cwd (may conflict).

BridgeWorkerType

type BridgeWorkerType = 'claude_code' | 'claude_code_assistant'

SessionActivity

type SessionActivityType = 'tool_start' | 'text' | 'result' | 'error'

type SessionActivity = {
  type: SessionActivityType
  summary: string   // e.g. "Editing src/foo.ts", "Reading package.json"
  timestamp: number
}

SessionHandle

Interface returned by SessionSpawner.spawn():

type SessionHandle = {
  sessionId: string
  done: Promise<SessionDoneStatus>       // 'completed' | 'failed' | 'interrupted'
  kill(): void
  forceKill(): void
  activities: SessionActivity[]          // Ring buffer of last ~10 activities
  currentActivity: SessionActivity | null
  accessToken: string                    // session_ingress_token
  lastStderr: string[]                   // Ring buffer of last stderr lines
  writeStdin(data: string): void
  updateAccessToken(token: string): void
}

SessionSpawnOpts

type SessionSpawnOpts = {
  sessionId: string
  sdkUrl: string
  accessToken: string
  useCcrV2?: boolean            // Spawn child with CCR v2 env vars
  workerEpoch?: number          // Required when useCcrV2=true
  onFirstUserMessage?: (text: string) => void
}

BridgeApiClient Interface

type BridgeApiClient = {
  registerBridgeEnvironment(config: BridgeConfig): Promise<{
    environment_id: string
    environment_secret: string
  }>
  pollForWork(
    environmentId: string,
    environmentSecret: string,
    signal?: AbortSignal,
    reclaimOlderThanMs?: number,
  ): Promise<WorkResponse | null>
  acknowledgeWork(environmentId, workId, sessionToken): Promise<void>
  stopWork(environmentId, workId, force): Promise<void>
  deregisterEnvironment(environmentId): Promise<void>
  sendPermissionResponseEvent(sessionId, event, sessionToken): Promise<void>
  archiveSession(sessionId): Promise<void>
  reconnectSession(environmentId, sessionId): Promise<void>
  heartbeatWork(environmentId, workId, sessionToken): Promise<{
    lease_extended: boolean
    state: string
  }>
}

PermissionResponseEvent

type PermissionResponseEvent = {
  type: 'control_response'
  response: {
    subtype: 'success'
    request_id: string
    response: Record<string, unknown>
  }
}

BridgeLogger Interface

Full interface for the bridge UI/logging system (defined in types.ts):

type BridgeLogger = {
  printBanner(config, environmentId): void
  logSessionStart(sessionId, prompt): void
  logSessionComplete(sessionId, durationMs): void
  logSessionFailed(sessionId, error): void
  logStatus(message): void
  logVerbose(message): void
  logError(message): void
  logReconnected(disconnectedMs): void
  updateIdleStatus(): void
  updateReconnectingStatus(delayStr, elapsedStr): void
  updateSessionStatus(sessionId, elapsed, activity, trail): void
  clearStatus(): void
  setRepoInfo(repoName, branch): void
  setDebugLogPath(path): void
  setAttached(sessionId): void
  updateFailedStatus(error): void
  toggleQr(): void
  updateSessionCount(active, max, mode): void
  setSpawnModeDisplay(mode): void
  addSession(sessionId, url): void
  updateSessionActivity(sessionId, activity): void
  setSessionTitle(sessionId, title): void
  removeSession(sessionId): void
  refreshDisplay(): void
}

3. Bridge API Client

File: bridge/bridgeApi.ts

Exports

validateBridgeId(id: string, label: string): string

Validates that a server-provided ID is safe for URL path interpolation. Uses pattern /^[a-zA-Z0-9_-]+$/. Throws on unsafe characters to prevent path traversal attacks.

class BridgeFatalError extends Error

Non-retryable bridge errors. Carries:

  • status: number — HTTP status code
  • errorType: string | undefined — server-provided error type (e.g. "environment_expired")

createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient

Factory for the HTTP client. Dependencies:

type BridgeApiDeps = {
  baseUrl: string
  getAccessToken: () => string | undefined
  runnerVersion: string
  onDebug?: (msg: string) => void
  onAuth401?: (staleAccessToken: string) => Promise<boolean>
  getTrustedDeviceToken?: () => string | undefined
}

Request Headers (on all API calls):

Authorization: Bearer <token>
Content-Type: application/json
anthropic-version: 2023-06-01
anthropic-beta: environments-2025-11-01
x-environment-runner-version: <runnerVersion>
X-Trusted-Device-Token: <token>  (optional, when tengu_sessions_elevated_auth_enforcement)

OAuth 401 Retry: On 401, calls onAuth401(staleToken). If token refresh succeeds, retries the request once. If the retry also returns 401, throws BridgeFatalError.

Poll endpoint (pollForWork): Uses environmentSecret (not OAuth token) as Bearer auth. Logs empty polls every 1st time and then every 100th consecutive empty poll.

isExpiredErrorType(errorType: string | undefined): boolean

Returns true if the error type string contains 'expired' or 'lifetime'.

isSuppressible403(err: BridgeFatalError): boolean

Returns true for 403 errors involving external_poll_sessions or environments:manage scope — these are permission errors for non-critical operations that should not be surfaced to users.

Error Status Handling

HTTP Status Behavior
200, 204 Success
401 BridgeFatalError with login instruction
403 (expired errorType) BridgeFatalError: "session has expired"
403 (other) BridgeFatalError: access denied / org permissions
404 BridgeFatalError: not found
410 BridgeFatalError with errorType='environment_expired'
429 Plain Error: rate limited
Other Plain Error with status code

4. Bridge Configuration & Auth

File: bridge/bridgeConfig.ts

Consolidates auth/URL resolution. Two layers: dev overrides (ant-only) and production OAuth.

Exports

getBridgeTokenOverride(): string | undefined

Returns process.env.CLAUDE_BRIDGE_OAUTH_TOKEN if process.env.USER_TYPE === 'ant', else undefined.

getBridgeBaseUrlOverride(): string | undefined

Returns process.env.CLAUDE_BRIDGE_BASE_URL if process.env.USER_TYPE === 'ant', else undefined.

getBridgeAccessToken(): string | undefined

Dev override first, then getClaudeAIOAuthTokens()?.accessToken.

getBridgeBaseUrl(): string

Dev override first, then getOauthConfig().BASE_API_URL.


5. Bridge Entitlement & Feature Gating

File: bridge/bridgeEnabled.ts

Exports

isBridgeEnabled(): boolean

Synchronous. Requires:

  1. Build flag feature('BRIDGE_MODE') must be true
  2. isClaudeAISubscriber() — excludes Bedrock/Vertex/API key users
  3. GrowthBook flag tengu_ccr_bridge (cached, may be stale)

isBridgeEnabledBlocking(): Promise<boolean>

Like isBridgeEnabled() but awaits GrowthBook server fetch if disk cache says false. Use at entitlement gates to avoid unfair denials from stale cache.

getBridgeDisabledReason(): Promise<string | null>

Returns a user-facing reason string if bridge is unavailable, or null if enabled. Checks:

  1. Not a claude.ai subscriber
  2. Missing user:profile scope (setup-token / env-var OAuth tokens)
  3. Missing organizationUuid in OAuth account info
  4. tengu_ccr_bridge gate off

isEnvLessBridgeEnabled(): boolean

Gates tengu_bridge_repl_v2 — the V2 (env-less) REPL bridge path. Cached, may be stale.

isCseShimEnabled(): boolean

Kill-switch for cse_*session_* retag shim. Reads tengu_bridge_repl_v2_cse_shim_enabled (default true). When false, toCompatSessionId() is a no-op.

checkBridgeMinVersion(): string | null

Returns error string if CLI version is below tengu_bridge_min_version config (default '0.0.0').

getCcrAutoConnectDefault(): boolean

Returns true when feature('CCR_AUTO_CONNECT') and tengu_cobalt_harbor gate are both enabled. Used as default for remoteControlAtStartup config.

isCcrMirrorEnabled(): boolean

Returns true when feature('CCR_MIRROR') and either CLAUDE_CODE_CCR_MIRROR env var is truthy or tengu_ccr_mirror gate is enabled.


6. Session Lifecycle: Standalone Bridge (bridgeMain.ts)

File: bridge/bridgeMain.ts

This is the main loop for claude remote-control (standalone mode).

BackoffConfig

type BackoffConfig = {
  connInitialMs: number       // Default: 2,000ms
  connCapMs: number           // Default: 120,000ms (2 min)
  connGiveUpMs: number        // Default: 600,000ms (10 min)
  generalInitialMs: number    // Default: 500ms
  generalCapMs: number        // Default: 30,000ms
  generalGiveUpMs: number     // Default: 600,000ms (10 min)
  shutdownGraceMs?: number    // SIGTERM→SIGKILL grace. Default: 30s
  stopWorkBaseDelayMs?: number // stopWork retry base. Default: 1000ms
}

Constants

STATUS_UPDATE_INTERVAL_MS = 1_000    // Live display refresh rate
SPAWN_SESSIONS_DEFAULT = 32          // Default max sessions

runBridgeLoop(config, environmentId, environmentSecret, api, spawner, logger, signal, backoffConfig?, initialSessionId?, getAccessToken?): Promise<void>

Main exported function. Manages:

  • activeSessions: Map<string, SessionHandle> — currently running sessions
  • sessionStartTimes: Map<string, number> — for elapsed time display
  • sessionWorkIds: Map<string, string> — work ID per session
  • sessionCompatIds: Map<string, string>session_* ID per session (stable per-session)
  • sessionIngressTokens: Map<string, string> — JWT per session for heartbeat auth
  • sessionTimers: Map<string, ReturnType<setTimeout>> — per-session timeout timers
  • completedWorkIds: Set<string> — prevents double-stop
  • sessionWorktrees: Map<string, {...}> — worktree cleanup state
  • timedOutSessions: Set<string> — sessions killed by timeout watchdog
  • titledSessions: Set<string> — sessions already titled (suppresses auto-title)
  • capacityWake: CapacityWake — signals early wake from at-capacity sleep

Heartbeat Logic:

heartbeatActiveWorkItems() iterates all activeSessions, calls api.heartbeatWork() with each session's ingress token. On BridgeFatalError 401/403 (JWT expired), calls api.reconnectSession() to trigger server re-dispatch. Returns:

  • 'ok' — at least one heartbeat succeeded
  • 'auth_failed' — one or more sessions had expired JWTs (re-queued via reconnect)
  • 'fatal' — 404/410 errors (environment expired)
  • 'failed' — all heartbeats failed for other reasons

Token Refresh (Proactive):

createTokenRefreshScheduler() fires 5 minutes before each session's JWT expires.

  • V1 sessions: calls handle.updateAccessToken(oauthToken) to inject the new OAuth token directly to the child process stdin.
  • V2 sessions (v2Sessions set): calls api.reconnectSession() to trigger server re-dispatch with a fresh JWT (V2 children validate the JWT's session_id claim, so OAuth tokens cannot be used directly).

Session Done Handler:

onSessionDone() cleans up all maps, fires capacityWake.wake(), then:

  • status = 'completed': logs completion
  • status = 'failed': logs failure with stderr (unless it was shutdown)
  • status = 'interrupted': logs verbose only

For non-interrupted sessions with a workId, calls stopWorkWithRetry() (3 retries with 1s/2s/4s backoff). For worktree sessions, calls removeAgentWorktree().

Status Display:

Tick every 1s via setInterval. Calls logger.updateSessionStatus() for each active session showing: elapsed time, current activity, last 5 tool activities as a trail.

Session Spawn: V1 vs V2

When work arrives:

  1. Decodes WorkSecret from work.secret.
  2. If workSecret.use_code_sessions === true: uses CCR v2 path (buildCCRv2SdkUrl, registerWorker(), useCcrV2=true).
  3. Otherwise: uses V1 path (buildSdkUrl(), useCcrV2=false).

SpawnMode: Worktree

When config.spawnMode === 'worktree':

  • Calls createAgentWorktree() to create an isolated git worktree.
  • Passes the worktree path as the session's working directory.
  • On session completion, calls removeAgentWorktree().

Graceful Shutdown Sequence

  1. Abort loopSignal.
  2. Stop all status update timers.
  3. For each active session: call handle.kill(), await handle.done.
  4. Await all pending cleanups (stopWork + worktree removal).
  5. Call api.deregisterEnvironment().
  6. If session(s) were not fatal-exited, show resume hint.

7. REPL Bridge (replBridge.ts / initReplBridge.ts)

ReplBridgeHandle (bridge/replBridge.ts)

type ReplBridgeHandle = {
  bridgeSessionId: string
  environmentId: string
  sessionIngressUrl: string
  writeMessages(messages: Message[]): void
  writeSdkMessages(messages: SDKMessage[]): void
  sendControlRequest(request: SDKControlRequest): void
  sendControlResponse(response: SDKControlResponse): void
  sendControlCancelRequest(requestId: string): void
  sendResult(): void
  teardown(): Promise<void>
}

BridgeState

type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed'

BridgeCoreParams

The explicit-parameter interface to initBridgeCore() (enabling daemon/non-REPL callers):

type BridgeCoreParams = {
  dir: string
  machineName: string
  branch: string
  gitRepoUrl: string | null
  title: string
  baseUrl: string
  sessionIngressUrl: string
  workerType: string
  sessionId: string             // REPL's own session ID
  getAccessToken: () => string | undefined
  onAuth401?: (staleAccessToken: string) => Promise<boolean>
  toSDKMessages: (messages: Message[]) => SDKMessage[]
  initialHistoryCap: number
  pollConfig?: PollIntervalConfig
  // ... plus InitBridgeOptions callbacks
}

InitBridgeOptions (bridge/initReplBridge.ts)

type InitBridgeOptions = {
  onInboundMessage?: (msg: SDKMessage) => void | Promise<void>
  onPermissionResponse?: (response: SDKControlResponse) => void
  onInterrupt?: () => void
  onSetModel?: (model: string | undefined) => void
  onSetMaxThinkingTokens?: (maxTokens: number | null) => void
  onSetPermissionMode?: (mode: PermissionMode) => { ok: true } | { ok: false; error: string }
  onStateChange?: (state: BridgeState, detail?: string) => void
  initialMessages?: Message[]
  initialName?: string
  getMessages?: () => Message[]
  previouslyFlushedUUIDs?: Set<string>
  perpetual?: boolean
}

replBridgeHandle.ts

Global pointer to the active REPL bridge handle:

setReplBridgeHandle(h: ReplBridgeHandle | null): void
getReplBridgeHandle(): ReplBridgeHandle | null
getSelfBridgeCompatId(): string | undefined  // Returns session_* compat ID

8. Env-Less Bridge Core (remoteBridgeCore.ts)

File: bridge/remoteBridgeCore.ts

Direct-connect bridge that bypasses the Environments API entirely.

Connection Flow

  1. POST /v1/code/sessions with { title, bridge: {} }session.id (cse_*)
  2. POST /v1/code/sessions/{id}/bridge{ worker_jwt, expires_in, api_base_url, worker_epoch }
  3. createV2ReplTransport(worker_jwt, worker_epoch) — SSE + CCRClient
  4. createTokenRefreshScheduler(scheduleFromExpiresIn) — proactive /bridge re-call
  5. On 401 SSE: rebuild transport with fresh /bridge credentials (same seq-num)

EnvLessBridgeParams

type EnvLessBridgeParams = {
  baseUrl: string
  orgUUID: string
  title: string
  getAccessToken: () => string | undefined
  onAuth401?: (staleAccessToken: string) => Promise<boolean>
  toSDKMessages: (messages: Message[]) => SDKMessage[]
  initialHistoryCap: number
  initialMessages?: Message[]
  onInboundMessage?: (msg: SDKMessage) => void | Promise<void>
  onUserMessage?: (text: string, sessionId: string) => boolean
  onPermissionResponse?: (response: SDKControlResponse) => void
  onInterrupt?: () => void
  onSetModel?: (model: string | undefined) => void
  onSetMaxThinkingTokens?: (maxTokens: number | null) => void
  onSetPermissionMode?: (mode: PermissionMode) => { ok: true } | { ok: false; error: string }
  onStateChange?: (state: BridgeState, detail?: string) => void
  perpetual?: boolean
}

Connect Cause Telemetry

type ConnectCause = 'initial' | 'proactive_refresh' | 'auth_401_recovery'

Sent with tengu_bridge_repl_v2_ws_connected analytics event.


9. Transport Layer

ReplBridgeTransport Interface (bridge/replBridgeTransport.ts)

Abstracts over V1 (HybridTransport) and V2 (SSETransport+CCRClient):

type ReplBridgeTransport = {
  write(message: StdoutMessage): Promise<void>
  writeBatch(messages: StdoutMessage[]): Promise<void>
  close(): void
  isConnectedStatus(): boolean
  getStateLabel(): string
  setOnData(callback: (data: string) => void): void
  setOnClose(callback: (closeCode?: number) => void): void
  setOnConnect(callback: () => void): void
  connect(): void
  getLastSequenceNum(): number    // V1 always returns 0; V2 returns SSE seq
  readonly droppedBatchCount: number  // V1 only; V2 always 0
  reportState(state: SessionState): void      // V2 only; V1 no-op
  reportMetadata(metadata: Record<string, unknown>): void  // V2 only
  reportDelivery(eventId: string, status: 'processing' | 'processed'): void  // V2 only
  flush(): Promise<void>          // V2 only; V1 resolves immediately
}

createV1ReplTransport(hybrid: HybridTransport): ReplBridgeTransport

Thin no-op wrapper that delegates to HybridTransport. V1-specific behaviors:

  • getLastSequenceNum() always returns 0
  • reportState(), reportMetadata(), reportDelivery(), flush() are all no-ops

createV2ReplTransport(opts): Promise<ReplBridgeTransport>

Options:

{
  sessionUrl: string           // /v1/code/sessions/{id}
  ingressToken: string
  sessionId: string
  initialSequenceNum?: number  // SSE resume cursor
  epoch?: number               // If from /bridge, server already bumped epoch
  heartbeatIntervalMs?: number // Default: 20s
  heartbeatJitterFraction?: number
  outboundOnly?: boolean       // Skip SSE read stream (mirror mode)
  getAuthToken?: () => string | undefined  // Per-instance auth (multi-session safe)
}

Close Codes used internally:

  • 4090 — epoch superseded (epoch mismatch from CCRClient)
  • 4091 — CCR initialize() failure
  • 4092 — SSE reconnect-budget exhaustion (mapped from undefined)

Delivery ACK behavior: Both 'received' and 'processed' are fired immediately on SSE event receipt to prevent phantom prompt flooding on restarts. This is a fix for the issue where reconnectSession re-queues prompts that haven't been ACK'd as 'processed'.

HybridTransport (cli/transports/HybridTransport.ts)

Extends WebSocketTransport. WebSocket for reads, HTTP POST for writes.

Configuration:

BATCH_FLUSH_INTERVAL_MS = 100     // Accumulates stream_events before POST
POST_TIMEOUT_MS = 15_000           // Per-attempt timeout
CLOSE_GRACE_MS = 3000              // Grace period for queued writes on close

Write flow:

write(stream_event) ─┐
                     │ (100ms timer)
write(other) ──────► SerialBatchEventUploader.enqueue()
writeBatch() ───────┘     │
                          ▼ serial, batched, retries indefinitely
                     postOnce()  (single HTTP POST)
  • maxBatchSize: 500
  • maxQueueSize: 100,000
  • baseDelayMs: 500, maxDelayMs: 8000, jitterMs: 1000

Post URL: Converts WebSocket URL to HTTP(S) POST endpoint (convertWsUrlToPostUrl()).

WebSocketTransport (cli/transports/WebSocketTransport.ts)

Base WebSocket transport.

Configuration:

DEFAULT_MAX_BUFFER_SIZE = 1000
DEFAULT_BASE_RECONNECT_DELAY = 1000
DEFAULT_MAX_RECONNECT_DELAY = 30_000
DEFAULT_RECONNECT_GIVE_UP_MS = 600_000   // 10 minutes
DEFAULT_PING_INTERVAL = 10_000
DEFAULT_KEEPALIVE_INTERVAL = 300_000     // 5 minutes
SLEEP_DETECTION_THRESHOLD_MS = 60_000   // 2× max reconnect delay
KEEP_ALIVE_FRAME = '{"type":"keep_alive"}\n'

Permanent Close Codes (no retry):

  • 1002 — protocol error (session reaped)
  • 4001 — session expired/not found
  • 4003 — unauthorized

Sleep detection: If gap between reconnection attempts exceeds 60s, resets the reconnection budget and retries (machine likely slept).

States: 'idle' | 'connected' | 'reconnecting' | 'closing' | 'closed'

SSETransport (cli/transports/SSETransport.ts)

Server-Sent Events transport.

Configuration:

RECONNECT_BASE_DELAY_MS = 1000
RECONNECT_MAX_DELAY_MS = 30_000
RECONNECT_GIVE_UP_MS = 600_000    // 10 minutes
LIVENESS_TIMEOUT_MS = 45_000      // Server keepalives every 15s
PERMANENT_HTTP_CODES = {401, 403, 404}
POST_MAX_RETRIES = 10
POST_BASE_DELAY_MS = 500
POST_MAX_DELAY_MS = 8000

SSE Frame Parsing:

type SSEFrame = {
  event?: string
  id?: string       // Used as sequence number for Last-Event-ID
  data?: string
}

Frames are double-newline delimited. Leading space after : is stripped per SSE spec. Comments (:keepalive) are ignored. Sequence numbers are tracked via id field.

Exported for testing: parseSSEFrames(buffer: string): { frames: SSEFrame[]; remaining: string }

Sequence number carryover: On reconnect, sends Last-Event-ID or from_sequence_num query param so the server resumes from where the old stream left off.

SerialBatchEventUploader (cli/transports/SerialBatchEventUploader.ts)

type SerialBatchEventUploaderConfig<T> = {
  maxBatchSize: number         // Max items per POST
  maxBatchBytes?: number       // Max serialized bytes per POST
  maxQueueSize: number         // Max pending items before enqueue() blocks
  send: (batch: T[]) => Promise<void>  // The actual HTTP call
  baseDelayMs: number
  maxDelayMs: number
  jitterMs: number
  maxConsecutiveFailures?: number   // After N failures, drop batch and advance
  onBatchDropped?: (batchSize, failures) => void
}

class RetryableError extends Error: Throw from config.send() to override exponential backoff with server-supplied retryAfterMs (e.g., for 429 responses).

Backpressure: enqueue() blocks when maxQueueSize is reached.

WorkerStateUploader (cli/transports/WorkerStateUploader.ts)

Coalescing uploader for PUT /worker (session state + metadata).

  • At most 1 in-flight PUT + 1 pending patch (never grows beyond 2 slots).
  • Coalescing rules:
    • Top-level keys: last value wins.
    • external_metadata / internal_metadata: RFC 7396 merge (null values preserved for server-side delete).

CCRClient (cli/transports/ccrClient.ts)

The CCR v2 write-side client.

Configuration:

DEFAULT_HEARTBEAT_INTERVAL_MS = 20_000    // Server TTL: 60s
STREAM_EVENT_FLUSH_INTERVAL_MS = 100      // text_delta coalescing window
MAX_CONSECUTIVE_AUTH_FAILURES = 10        // ~200s at 20s heartbeat

class CCRInitError extends Error: Carries reason: CCRInitFailReason:

  • 'no_auth_headers'
  • 'missing_epoch'
  • 'worker_register_failed'

Epoch mismatch (409 response): Triggers onEpochMismatch() callback, which closes CCRClient and SSETransport and fires onCloseCb(4090).

text_delta coalescing: stream_event messages with content_block_delta / text_delta are accumulated in a per-message-ID buffer for 100ms. Each emitted event is a complete self-contained snapshot of the text so far.

Transport Selection (cli/transports/transportUtils.ts)

getTransportForUrl(url, headers, sessionId, refreshHeaders): Transport

Priority:

  1. SSETransport — when CLAUDE_CODE_USE_CCR_V2 env is truthy
  2. HybridTransport — when URL is ws(s):// AND CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2 env is truthy
  3. WebSocketTransport — default for ws(s)://
  4. Throws for unsupported protocols

10. Message Protocol (bridgeMessaging.ts)

File: bridge/bridgeMessaging.ts

Pure functions shared by both V1 (initBridgeCore) and V2 (initEnvLessBridgeCore).

Type Guards

isSDKMessage(value: unknown): value is SDKMessage

Checks for non-null object with string type field.

isSDKControlResponse(value: unknown): value is SDKControlResponse

Checks type === 'control_response' and has 'response' field.

isSDKControlRequest(value: unknown): value is SDKControlRequest

Checks type === 'control_request', has 'request_id' and 'request' fields.

isEligibleBridgeMessage(m: Message): boolean

Returns true for messages that should be forwarded to the bridge transport:

  • type === 'user' (non-virtual)
  • type === 'assistant' (non-virtual)
  • type === 'system' with subtype === 'local_command'

extractTitleText(m: Message): string | undefined

Extracts title-worthy text from a user message. Filters out:

  • Non-user messages
  • isMeta messages
  • Tool result messages
  • Compact summary messages
  • Non-human origins (origin.kind !== 'human')
  • Pure display-tag content (stripped via stripDisplayTagsAllowEmpty)

handleIngressMessage(data, recentPostedUUIDs, recentInboundUUIDs, onInboundMessage, onPermissionResponse?, onControlRequest?): void

Parses and routes an ingress WebSocket message:

  1. Parses JSON, normalizes control message keys.
  2. Checks for control_response → calls onPermissionResponse.
  3. Checks for control_request → calls onControlRequest.
  4. Validates it's an SDKMessage.
  5. Echo dedup: skips if UUID in recentPostedUUIDs.
  6. Re-delivery dedup: skips if UUID in recentInboundUUIDs.
  7. Only forwards type === 'user' messages to onInboundMessage.
  8. All other message types are logged and ignored.

Server Control Request Handling

handleServerControlRequest(request, handlers): void

Processes server-sent control_request messages. Must respond promptly (server kills WS after ~10-14s timeout).

Supported subtypes:

Subtype Behavior
initialize Responds with { commands: [], output_style: 'normal', available_output_styles: ['normal'], models: [], account: {}, pid: process.pid }
set_model Calls onSetModel(request.request.model), responds success
set_max_thinking_tokens Calls onSetMaxThinkingTokens(maxTokens), responds success
set_permission_mode Calls onSetPermissionMode(mode), responds success or error
interrupt Calls onInterrupt(), responds success
unknown Responds with error: "REPL bridge does not handle control_request subtype: ..."

Outbound-only mode: All mutable requests respond with error 'This session is outbound-only...'. initialize still responds success.

Response envelope:

{
  "type": "control_response",
  "response": {
    "subtype": "success" | "error",
    "request_id": "<id>",
    ...
  },
  "session_id": "<sessionId>"
}

makeResultMessage(sessionId: string): SDKResultSuccess

Builds a minimal result message for session archival:

{
  "type": "result",
  "subtype": "success",
  "duration_ms": 0,
  "duration_api_ms": 0,
  "is_error": false,
  "num_turns": 0,
  "result": "",
  "stop_reason": null,
  "total_cost_usd": 0,
  "usage": {...},
  "modelUsage": {},
  "permission_denials": [],
  "session_id": "<sessionId>",
  "uuid": "<randomUUID>"
}

class BoundedUUIDSet

FIFO-bounded ring buffer for UUID deduplication. O(capacity) memory.

class BoundedUUIDSet {
  constructor(capacity: number)
  add(uuid: string): void   // Evicts oldest when at capacity
  has(uuid: string): boolean
  clear(): void
}

Used for:

  • recentPostedUUIDs — echo suppression (messages we sent reflected back)
  • recentInboundUUIDs — re-delivery dedup (server replays history after transport swap)

11. JWT Authentication (jwtUtils.ts)

File: bridge/jwtUtils.ts

decodeJwtPayload(token: string): unknown | null

Decodes JWT payload segment without signature verification. Strips sk-ant-si- prefix if present. Returns parsed JSON or null on malformed input.

decodeJwtExpiry(token: string): number | null

Extracts the exp Unix seconds claim from a JWT without verifying the signature. Returns null if unparseable.

createTokenRefreshScheduler(opts): { schedule, scheduleFromExpiresIn, cancel, cancelAll }

Options:

{
  getAccessToken: () => string | undefined | Promise<string | undefined>
  onRefresh: (sessionId: string, oauthToken: string) => void
  label: string
  refreshBufferMs?: number   // Default: TOKEN_REFRESH_BUFFER_MS = 5 min
}

Constants:

TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000      // 5 minutes before expiry
FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes (fallback)
MAX_REFRESH_FAILURES = 3
REFRESH_RETRY_DELAY_MS = 60_000               // 1 minute

Methods:

schedule(sessionId, token): Decodes exp from JWT, schedules refresh (exp × 1000 - now - refreshBufferMs) ms from now. If token has no decodable exp (e.g., OAuth token), preserves existing timer.

scheduleFromExpiresIn(sessionId, expiresInSeconds): Schedules refresh using explicit TTL. Clamp to 30s floor: max(expiresInSeconds × 1000 - refreshBufferMs, 30_000).

cancel(sessionId): Clears timer, bumps generation to invalidate in-flight refreshes.

cancelAll(): Clears all timers and failure counters.

Generation tracking: Each session has a monotonic generation counter. doRefresh() checks that the generation hasn't changed before scheduling follow-up timers. This prevents orphaned timers when a session is cancelled while a refresh is in flight.

Follow-up refresh: After each successful refresh, schedules FALLBACK_REFRESH_INTERVAL_MS (30 min) follow-up to handle long-running sessions that outlast the first refresh window.


12. Session ID Compatibility (sessionIdCompat.ts)

File: bridge/sessionIdCompat.ts

Handles the V2 compat layer's cse_*session_* ID translation.

Problem: CCR V2 infra uses cse_* prefix internally; the compat gateway and client-facing API (/v1/sessions) expect session_*. Same UUID, different prefix.

Exports

setCseShimGate(gate: () => boolean): void

Registers the GrowthBook gate isCseShimEnabled. Called from bridge init code that already imports bridgeEnabled.ts. The SDK bundle never calls this, so the shim defaults to active.

toCompatSessionId(id: string): string

Re-tags cse_*session_* for compat API calls (/v1/sessions/{id}, /archive, /events). No-op for IDs that aren't cse_*. No-op when shim gate is off.

"cse_abc123" → "session_abc123"
"session_abc123" → "session_abc123"  (no-op)

toInfraSessionId(id: string): string

Inverse: re-tags session_*cse_* for infrastructure calls (/bridge/reconnect). No-op for IDs that aren't session_*.

"session_abc123" → "cse_abc123"
"cse_abc123" → "cse_abc123"  (no-op)

13. Work Secrets & CCR v2 Registration (workSecret.ts)

File: bridge/workSecret.ts

decodeWorkSecret(secret: string): WorkSecret

Decodes base64url-encoded work secret JSON. Validates:

  • Must be a version-1 secret.
  • session_ingress_token must be a non-empty string.
  • api_base_url must be a string.

buildSdkUrl(apiBaseUrl: string, sessionId: string): string

Builds V1 WebSocket URL:

  • Localhost: ws://host/v2/session_ingress/ws/{sessionId} (direct to session-ingress)
  • Production: wss://host/v1/session_ingress/ws/{sessionId} (Envoy rewrites /v1//v2/)

sameSessionId(a: string, b: string): boolean

Compares two session IDs regardless of prefix (cse_ vs session_). Compares the UUID body (everything after the last _). Requires body length ≥ 4 to avoid false matches on malformed IDs.

buildCCRv2SdkUrl(apiBaseUrl: string, sessionId: string): string

Builds V2 HTTP(S) session URL: {apiBaseUrl}/v1/code/sessions/{sessionId}

registerWorker(sessionUrl: string, accessToken: string): Promise<number>

POST {sessionUrl}/worker/register to register as the CCR worker. Returns worker_epoch. The epoch is serialized as int64 which may be returned as a string by protojson — handles both string and number forms.


14. Bridge Pointer: Crash Recovery (bridgePointer.ts)

File: bridge/bridgePointer.ts

Purpose

Crash-recovery pointer written after session creation, refreshed periodically, cleared on clean shutdown. On next startup, claude remote-control detects stale pointers and offers to resume.

Constants

BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000   // 4 hours (matches Redis BRIDGE_LAST_POLL_TTL)
MAX_WORKTREE_FANOUT = 50

BridgePointer Schema

type BridgePointer = {
  sessionId: string
  environmentId: string
  source: 'standalone' | 'repl'
}

File location: {projectsDir}/{sanitizedDir}/bridge-pointer.json

Exports

getBridgePointerPath(dir: string): string

Returns the absolute path to the bridge pointer file for a given working directory.

writeBridgePointer(dir, pointer): Promise<void>

Writes pointer atomically. Also used to refresh mtime (same-content writes). Best-effort — logs and swallows errors.

readBridgePointer(dir): Promise<(BridgePointer & { ageMs: number }) | null>

Reads the pointer. Returns null if:

  • File does not exist
  • JSON is malformed
  • Schema validation fails
  • mtime is more than 4 hours ago (stale)

Stale/invalid pointers are automatically deleted.

readBridgePointerAcrossWorktrees(dir): Promise<{ pointer, dir } | null>

Worktree-aware read for --continue. Fast-path checks the given dir first. If not found, fans out to git worktree siblings (via getWorktreePathsPortable()) in parallel (capped at 50). Returns the freshest pointer and its directory.

clearBridgePointer(dir): Promise<void>

Deletes the pointer file. Idempotent (ENOENT is expected on clean shutdown).


15. Permission Callbacks (bridgePermissionCallbacks.ts)

File: bridge/bridgePermissionCallbacks.ts

Types

type BridgePermissionResponse = {
  behavior: 'allow' | 'deny'
  updatedInput?: Record<string, unknown>
  updatedPermissions?: PermissionUpdate[]
  message?: string
}

type BridgePermissionCallbacks = {
  sendRequest(
    requestId, toolName, input, toolUseId, description,
    permissionSuggestions?, blockedPath?
  ): void
  sendResponse(requestId, response: BridgePermissionResponse): void
  cancelRequest(requestId): void
  onResponse(
    requestId,
    handler: (response: BridgePermissionResponse) => void
  ): () => void  // returns unsubscribe function
}

isBridgePermissionResponse(value: unknown): value is BridgePermissionResponse

Type predicate. Checks that value.behavior === 'allow' || value.behavior === 'deny'.


16. Inbound Messages & Attachments

inboundAttachments.ts

Resolves file_uuid attachments from inbound bridge user messages.

Flow:

  1. Web composer uploads via /api/{org}/upload (cookie-auth).
  2. Bridge receives file_attachments: [{ file_uuid, file_name }] on the user message.
  3. resolveInboundAttachments() fetches each file via GET /api/oauth/files/{uuid}/content.
  4. Downloads to ~/.claude/uploads/{sessionId}/{uuid-prefix}-{sanitizedName}.
  5. Returns @"path" prefix string to prepend to message content.

DOWNLOAD_TIMEOUT_MS = 30_000

File path sanitization: sanitizeFileName() strips path components and replaces non-alphanumeric chars except ._- with _.

Prefix format: 8 chars from file_uuid (or random UUID), e.g. @"abc12345-filename.pdf" .

Exports:

extractInboundAttachments(msg: unknown): InboundAttachment[]
resolveInboundAttachments(attachments: InboundAttachment[]): Promise<string>
prependPathRefs(
  content: string | Array<ContentBlockParam>,
  prefix: string,
): string | Array<ContentBlockParam>
resolveAndPrepend(
  msg: unknown,
  content: string | Array<ContentBlockParam>,
): Promise<string | Array<ContentBlockParam>>

prependPathRefs() targets the last text block in a content array (because processUserInputBase reads the last block).

inboundMessages.ts

extractInboundMessageFields(msg: SDKMessage): { content, uuid } | undefined

Extracts content and UUID from a user message. Normalizes image blocks:

  • Converts camelCase mediaTypemedia_type (mobile app compatibility fix for mobile-apps#5825)
  • Detects missing media_type via detectImageFormatFromBase64()

normalizeImageBlocks(blocks: ContentBlockParam[]): ContentBlockParam[]

Fast-path: returns original reference if no malformed blocks. Only allocates on the fix-needed path.


17. Session Runner (sessionRunner.ts)

File: bridge/sessionRunner.ts

Spawns child Claude CLI processes for bridge sessions.

Constants

MAX_ACTIVITIES = 10      // Ring buffer size for activity history
MAX_STDERR_LINES = 10    // Ring buffer size for stderr

safeFilenameId(id: string): string

Sanitizes session IDs for use in file names. Replaces non-alphanumeric chars (except _-) with underscores.

PermissionRequest

Message emitted by child CLI on stdout when it needs permission:

type PermissionRequest = {
  type: 'control_request'
  request_id: string
  request: {
    subtype: 'can_use_tool'
    tool_name: string
    input: Record<string, unknown>
    tool_use_id: string
  }
}

SessionSpawnerDeps

type SessionSpawnerDeps = {
  execPath: string
  scriptArgs: string[]        // Empty for compiled binaries; [process.argv[1]] for npm
  env: NodeJS.ProcessEnv
  verbose: boolean
  sandbox: boolean
  debugFile?: string
  permissionMode?: string
  onDebug: (msg: string) => void
  onActivity?: (sessionId, activity) => void
  onPermissionRequest?: (sessionId, request, accessToken) => void
}

Tool Activity Verbs

const TOOL_VERBS = {
  Read: 'Reading', Write: 'Writing', Edit: 'Editing', MultiEdit: 'Editing',
  Bash: 'Running', Glob: 'Searching', Grep: 'Searching',
  WebFetch: 'Fetching', WebSearch: 'Searching', Task: 'Running task',
  FileReadTool: 'Reading', FileWriteTool: 'Writing', FileEditTool: 'Editing',
  GlobTool: 'Searching', GrepTool: 'Searching', BashTool: 'Running',
  NotebookEditTool: 'Editing notebook', LSP: 'LSP',
}

18. Bridge Debug & Fault Injection (bridgeDebug.ts)

File: bridge/bridgeDebug.ts

Ant-only fault injection for testing bridge recovery paths. Zero overhead in external builds.

BridgeFault

type BridgeFault = {
  method: 'pollForWork' | 'registerBridgeEnvironment' | 'reconnectSession' | 'heartbeatWork'
  kind: 'fatal' | 'transient'
  status: number
  errorType?: string
  count: number          // Decremented on consume; removed at 0
}
  • fatal: Throws BridgeFatalError — triggers environment teardown.
  • transient: Throws a plain Error (mimics 5xx/network) — triggers retry/backoff.

BridgeDebugHandle

type BridgeDebugHandle = {
  fireClose: (code: number) => void       // Invoke transport permanent-close handler
  forceReconnect: () => void              // Call reconnectEnvironmentWithSession()
  injectFault: (fault: BridgeFault) => void
  wakePollLoop: () => void                // Abort at-capacity sleep immediately
  describe: () => string                  // "envId=... sessionId=..."
}

Exports

registerBridgeDebugHandle(h: BridgeDebugHandle): void
clearBridgeDebugHandle(): void
getBridgeDebugHandle(): BridgeDebugHandle | null
injectBridgeFault(fault: BridgeFault): void
wrapApiForFaultInjection(api: BridgeApiClient): BridgeApiClient

wrapApiForFaultInjection() wraps pollForWork, registerBridgeEnvironment, reconnectSession, and heartbeatWork with fault queue checks. All other methods pass through unchanged.


19. Bridge Utilities

debugUtils.ts

redactSecrets(s: string): string

Redacts sensitive field values matching: session_ingress_token, environment_secret, access_token, secret, token. Values shorter than 16 chars → [REDACTED]. Longer: first8chars...last4chars.

debugTruncate(s: string): string          // 2000 char limit, collapses newlines
debugBody(data: unknown): string          // Serialize + redact + truncate
describeAxiosError(err: unknown): string  // Extracts server message from axios errors
extractHttpStatus(err: unknown): number | undefined
extractErrorDetail(data: unknown): string | undefined  // Checks data.message, data.error.message
logBridgeSkip(reason, debugMsg?, v2?): void  // Logs analytics + debug for bridge skip

bridgeStatusUtil.ts

type StatusState = 'idle' | 'attached' | 'titled' | 'reconnecting' | 'failed'
TOOL_DISPLAY_EXPIRY_MS = 30_000   // How long a tool activity stays visible
SHIMMER_INTERVAL_MS = 150         // Shimmer animation tick

timestamp(): string                // "HH:MM:SS"
formatDuration(ms): string         // re-exported from utils/format.ts
truncatePrompt(s, width): string   // re-exported
abbreviateActivity(summary): string  // Truncates to 30 chars

buildBridgeConnectUrl(environmentId, ingressUrl?): string
  // → "{baseUrl}/code?bridge={environmentId}"

buildBridgeSessionUrl(sessionId, environmentId, ingressUrl?): string
  // → "{remoteSessionUrl}?bridge={environmentId}"

computeGlimmerIndex(tick, messageWidth): number
computeShimmerSegments(text, glimmerIndex): { before, shimmer, after }

getBridgeStatus({ error, connected, sessionActive, reconnecting }): BridgeStatusInfo
  // Returns { label, color } for UI rendering

buildIdleFooterText(url): string
buildActiveFooterText(url): string
FAILED_FOOTER_TEXT = 'Something went wrong, please try again'

wrapWithOsc8Link(text, url): string   // OSC 8 terminal hyperlink

capacityWake.ts

type CapacitySignal = { signal: AbortSignal; cleanup: () => void }
type CapacityWake = {
  signal(): CapacitySignal  // Merged abort: outer loop OR capacity wake
  wake(): void              // Abort current sleep, arm fresh controller
}

createCapacityWake(outerSignal: AbortSignal): CapacityWake

Shared primitive for both replBridge.ts and bridgeMain.ts to sleep while at-capacity and wake early when a session ends or the outer signal aborts.

flushGate.ts

class FlushGate<T> {
  get active(): boolean
  get pendingCount(): number
  start(): void                  // Mark flush in-progress; enqueue() queues items
  end(): T[]                     // End flush, return queued items
  enqueue(...items: T[]): boolean  // Queue if active, else returns false
  drop(): number                 // Discard all (permanent close); returns count dropped
  deactivate(): void             // Clear active without dropping (transport replacement)
}

Used during initial history flush to queue new messages that arrive concurrently, preventing server-side interleaving.

pollConfig.ts / pollConfigDefaults.ts

getPollIntervalConfig(): PollIntervalConfig — reads from GrowthBook tengu_bridge_poll_interval_config with 5-minute refresh. Falls back to defaults on schema violation.

type PollIntervalConfig = {
  poll_interval_ms_not_at_capacity: number        // Default: 2000ms
  poll_interval_ms_at_capacity: number            // Default: 600,000ms (10 min)
  non_exclusive_heartbeat_interval_ms: number     // Default: 0 (disabled)
  multisession_poll_interval_ms_not_at_capacity: number  // Default: 2000ms
  multisession_poll_interval_ms_partial_capacity: number // Default: 2000ms
  multisession_poll_interval_ms_at_capacity: number      // Default: 600,000ms
  reclaim_older_than_ms: number                   // Default: 5000ms
  session_keepalive_interval_v2_ms: number        // Default: 120,000ms
}

Validation rules:

  • poll_interval_ms_* and multisession_* fields: min 100ms.
  • non_exclusive_heartbeat_interval_ms: min 0 (0 = disabled).
  • At-capacity intervals: 0 (disabled) or ≥100ms (1-99 rejected to prevent confusion with seconds).
  • Object-level: at least one at-capacity liveness mechanism must be enabled (heartbeat > 0 OR at-capacity poll > 0).

envLessBridgeConfig.ts

getEnvLessBridgeConfig(): Promise<EnvLessBridgeConfig> — reads from GrowthBook tengu_bridge_repl_v2_config. Defaults:

DEFAULT_ENV_LESS_BRIDGE_CONFIG = {
  init_retry_max_attempts: 3,
  init_retry_base_delay_ms: 500,
  init_retry_jitter_fraction: 0.25,
  init_retry_max_delay_ms: 4000,
  http_timeout_ms: 10_000,
  uuid_dedup_buffer_size: 2000,
  heartbeat_interval_ms: 20_000,
  heartbeat_jitter_fraction: 0.1,
  token_refresh_buffer_ms: 300_000,
  teardown_archive_timeout_ms: 1500,
  connect_timeout_ms: 15_000,
  min_version: '0.0.0',
  should_show_app_upgrade_message: false,
}

Validation ranges:

  • init_retry_max_attempts: 110
  • http_timeout_ms: min 2000ms
  • heartbeat_interval_ms: 500030,000ms
  • heartbeat_jitter_fraction: 00.5
  • token_refresh_buffer_ms: 30,0001,800,000ms
  • teardown_archive_timeout_ms: 5002000ms
  • connect_timeout_ms: 5,00060,000ms

trustedDevice.ts

Manages the trusted device token for ELEVATED security tier bridge sessions.

getTrustedDeviceToken(): string | undefined
clearTrustedDeviceTokenCache(): void
clearTrustedDeviceToken(): void
enrollTrustedDevice(): Promise<void>

Gate: tengu_sessions_elevated_auth_enforcement. When gate is off, getTrustedDeviceToken() returns undefined unconditionally.

Storage: macOS keychain via getSecureStorage() (memoized — keychain spawn is ~40ms).

Token precedence: CLAUDE_TRUSTED_DEVICE_TOKEN env var > keychain.

Enrollment: POST /api/auth/trusted_devices with display name "Claude Code on {hostname()} · {platform}". Must be called within 10 minutes of login. Best-effort — never throws. On success, persists to keychain and clears memo cache.

codeSessionApi.ts

Thin HTTP wrappers for CCR V2 code-session API.

createCodeSession(
  baseUrl, accessToken, title, timeoutMs, tags?
): Promise<string | null>
// POST /v1/code/sessions → session.id (cse_*)
// Body: { title, bridge: {}, tags?: [...] }

type RemoteCredentials = {
  worker_jwt: string
  api_base_url: string
  expires_in: number         // Seconds
  worker_epoch: number
}

fetchRemoteCredentials(
  sessionId, baseUrl, accessToken, timeoutMs, trustedDeviceToken?
): Promise<RemoteCredentials | null>
// POST /v1/code/sessions/{id}/bridge

worker_epoch is parsed defensively (protojson may return int64 as string).

sessionRunner.ts (full createSessionSpawner)

The createSessionSpawner() function (referenced from bridgeMain.ts) creates a SessionSpawner that:

  1. Calls spawn(child_process) with the Claude binary and appropriate flags.
  2. Parses child stdout as NDJSON, routing control_request messages to permission callbacks.
  3. Tracks SessionActivity in a ring buffer of size 10.
  4. Tracks last 10 stderr lines.
  5. Watches child exit to resolve handle.done with 'completed' (exit 0) or 'failed' (exit != 0) or 'interrupted' (SIGTERM/SIGKILL).

20. CLI Framework

cli/exit.ts

cliError(msg?: string): never   // stderr + process.exit(1)
cliOk(msg?: string): never      // stdout + process.exit(0)

Centralized CLI exit helpers. cliError uses console.error; cliOk uses process.stdout.write. The never return type allows TypeScript to narrow control flow at call sites.

cli/ndjsonSafeStringify.ts

ndjsonSafeStringify(value: unknown): string

JSON serializer that escapes U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) as \u2028/\u2029. These are valid line terminators in JavaScript (ECMA-262 §11.3) and would break line-splitting NDJSON receivers. The escaped form is still valid JSON.

cli/handlers/auth.ts

installOAuthTokens(tokens: OAuthTokens): Promise<void>
authLogin({ email?, sso?, console?, claudeai? }): Promise<void>
authStatus({ json?, text? }): Promise<void>
authLogout(): Promise<void>

installOAuthTokens() performs post-token acquisition:

  1. performLogout({ clearOnboarding: false }) — clears old state
  2. Fetches OAuth profile or falls back to tokenAccount
  3. Calls storeOAuthAccountInfo()
  4. saveOAuthTokensIfNeeded() + clearOAuthTokenCache()
  5. fetchAndStoreUserRoles() (best-effort)
  6. For claude.ai auth: fetchAndStoreClaudeCodeFirstTokenDate() (best-effort)
  7. For Console auth: createAndStoreApiKey() (required — throws if fails)
  8. clearAuthRelatedCaches()

authLogin() fast path: When CLAUDE_CODE_OAUTH_REFRESH_TOKEN env var is set, exchanges directly via refreshOAuthToken(), skipping browser OAuth flow. Requires CLAUDE_CODE_OAUTH_SCOPES env var (space-separated scopes).

authStatus() JSON output fields:

{
  "loggedIn": boolean,
  "authMethod": "none" | "claude.ai" | "api_key_helper" | "oauth_token" | "api_key" | "third_party",
  "apiProvider": string,
  "apiKeySource": string,       // Present when apiKey
  "email": string | null,       // Present when claude.ai
  "orgId": string | null,
  "orgName": string | null,
  "subscriptionType": string | null
}

cli/handlers/agents.ts

agentsHandler(): Promise<void>

Lists configured agents grouped by source. Output format: agentType · model · memory per agent. Shows shadowed agents (overridden by a higher-priority source) with (shadowed by {source}) prefix.

cli/handlers/autoMode.ts

autoModeDefaultsHandler(): void      // Dumps default auto mode rules as JSON
autoModeConfigHandler(): void        // Dumps effective config (user settings OR defaults)
autoModeCritiqueHandler({ model? }): Promise<void>  // AI critique of user rules

Auto mode rule categories: allow, soft_deny, environment.

Critique uses sideQuery() with a dedicated system prompt that asks Claude to evaluate rules for clarity, completeness, conflicts, and actionability.

cli/handlers/mcp.tsx (partial)

mcpServeHandler({ debug?, verbose? }): Promise<void>
mcpRemoveHandler(name, { scope? }): Promise<void>
// ... plus add, get, list, reset, import, desktop-import handlers

cli/handlers/plugins.ts (partial)

Handlers for claude plugin * and marketplace commands:

  • installPlugin, uninstallPlugin, enablePlugin, disablePlugin
  • listPlugins, marketplaceSearch, addMarketplace, removeMarketplace

cli/print.ts

The main SDK -p (print mode) handler. Orchestrates:

  • StructuredIO / RemoteIO for I/O
  • Tool pool assembly
  • Message queue management
  • Session state notifications
  • Optional bridge enablement via enableRemoteControl

cli/update.ts

update(): Promise<void>

Handles claude update. Checks current version, detects install type, selects updater (npm global, native binary, local). Shows warnings for multiple installations.


21. CLI Transports

See Section 9 — Transport Layer for the transport hierarchy.

cli/remoteIO.ts — RemoteIO class

class RemoteIO extends StructuredIO {
  constructor(streamUrl: string, initialPrompt?, replayUserMessages?)
}

Bidirectional streaming for SDK mode. Extends StructuredIO.

Constructor behavior:

  1. Creates PassThrough input stream.
  2. Reads CLAUDE_CODE_SESSION_ACCESS_TOKEN for initial auth headers.
  3. Reads CLAUDE_CODE_ENVIRONMENT_RUNNER_VERSION for x-environment-runner-version header.
  4. Creates refreshHeaders closure that re-reads the token dynamically on reconnects.
  5. Calls getTransportForUrl() to get transport (WS, Hybrid, or SSE).

Keep-alive: When session_keepalive_interval_v2_ms > 0 and transport is SSE/v2, sends silent {type:'keep_alive'} frames at that interval to prevent upstream proxy idle timeouts.

State/metadata listeners: Wires setSessionStateChangedListener and setSessionMetadataChangedListener to propagate SessionState changes to the CCRClient via reportState() and reportMetadata().

Command lifecycle: Wires setCommandLifecycleListener to fire reportDelivery('processing') on command start and reportDelivery('processed') on command end.

cli/structuredIO.ts — StructuredIO class

class StructuredIO {
  constructor(inputStream: Readable, replayUserMessages?)
  // Handles SDK control message parsing (control_request/control_response)
  // Permission handling via can_use_tool protocol
  // Elicitation dialog support
  // Hook system integration
}

const SANDBOX_NETWORK_ACCESS_TOOL_NAME = 'SandboxNetworkAccess'

StructuredIO is the base class providing:

  • NDJSON message deserialization from stdin
  • can_use_tool permission request handling
  • control_response dispatching
  • Elicitation dialog flow (SDKControlElicitationResponseSchema)
  • Hook execution before permission decisions

22. Remote Session System

remote/SessionsWebSocket.ts — SessionsWebSocket

WebSocket client for viewing sessions via /v1/sessions/ws/{id}/subscribe.

Connection Protocol:

  1. Connect to wss://api.anthropic.com/v1/sessions/ws/{sessionId}/subscribe?organization_uuid={orgUuid}
  2. Send auth message:
    { "type": "auth", "credential": { "type": "oauth", "token": "<accessToken>" } }
    
  3. Receive SessionsMessage stream.

Configuration:

RECONNECT_DELAY_MS = 2000
MAX_RECONNECT_ATTEMPTS = 5
PING_INTERVAL_MS = 30000
MAX_SESSION_NOT_FOUND_RETRIES = 3   // 4001 can be transient during compaction
PERMANENT_CLOSE_CODES = { 4003 }    // unauthorized — stops reconnecting

Note: 4001 (session not found) is handled separately with up to 3 limited retries because compaction can briefly cause false "not found" responses.

Callbacks:

type SessionsWebSocketCallbacks = {
  onMessage: (message: SessionsMessage) => void
  onClose?: () => void          // Permanent close only
  onError?: (error: Error) => void
  onConnected?: () => void
  onReconnecting?: () => void   // Transient drop with reconnect scheduled
}

Message types accepted: Any object with a string type field (open-ended to avoid dropping new server message types).

remote/RemoteSessionManager.ts — RemoteSessionManager

Coordinates WebSocket subscription + HTTP POST + permission flow.

type RemoteSessionConfig = {
  sessionId: string
  getAccessToken: () => string
  orgUuid: string
  hasInitialPrompt?: boolean
  viewerOnly?: boolean    // Pure viewer: no interrupt, no title update, no 60s reconnect timeout
}

type RemotePermissionResponse =
  | { behavior: 'allow'; updatedInput: Record<string, unknown> }
  | { behavior: 'deny'; message: string }

Callbacks:

type RemoteSessionCallbacks = {
  onMessage: (message: SDKMessage) => void
  onPermissionRequest: (request, requestId) => void
  onPermissionCancelled?: (requestId, toolUseId) => void
  onConnected?: () => void
  onDisconnected?: () => void
  onReconnecting?: () => void
  onError?: (error: Error) => void
}

remote/remotePermissionBridge.ts

createSyntheticAssistantMessage(
  request: SDKControlPermissionRequest,
  requestId: string,
): AssistantMessage

Creates a synthetic AssistantMessage wrapping a remote tool_use for the permission dialog. Uses a fake message ID remote-{requestId} and empty usage stats.

createToolStub(toolName: string): Tool

Creates a minimal Tool stub for tools unknown to the local CLI (e.g., MCP tools running on CCR). Routes to FallbackPermissionRequest. The stub's renderToolUseMessage() shows up to 3 input key-value pairs.

remote/sdkMessageAdapter.ts

Converts SDKMessage from CCR to REPL Message types.

type ConvertedMessage =
  | { type: 'message'; message: Message }
  | { type: 'stream_event'; event: StreamEvent }
  | { type: 'ignored' }

type ConvertOptions = {
  convertToolResults?: boolean          // For direct-connect mode
  convertUserTextMessages?: boolean     // For historical event conversion
}

convertSDKMessage(msg: SDKMessage, opts?: ConvertOptions): ConvertedMessage
isSessionEndMessage(msg: SDKMessage): boolean     // msg.type === 'result'
isSuccessResult(msg: SDKResultMessage): boolean   // msg.subtype === 'success'
getResultText(msg: SDKResultMessage): string | null

Conversion rules:

SDKMessage type Converted to
assistant AssistantMessage
user (tool_result, convertToolResults=true) UserMessage
user (text, convertUserTextMessages=true) UserMessage
user (other) ignored
stream_event StreamEvent
result (success) ignored
result (error) SystemMessage (warning)
system (init) SystemMessage (info): "Remote session initialized (model: ...)"
system (status: compacting) SystemMessage (info): "Compacting conversation…"
system (compact_boundary) SystemMessage (compact_boundary)
tool_progress SystemMessage (info): "Tool {name} running for {n}s…"
auth_status, tool_use_summary, rate_limit_event ignored
unknown ignored (logged)

23. replLauncher.tsx

File: src/replLauncher.tsx

type AppWrapperProps = {
  getFpsMetrics: () => FpsMetrics | undefined
  stats?: StatsStore
  initialState: AppState
}

launchRepl(
  root: Root,
  appProps: AppWrapperProps,
  replProps: REPLProps,
  renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
): Promise<void>

Lazy-loads App and REPL components and renders them wrapped in the React tree. Used by main.tsx to defer loading of the heavy UI component tree (App.js + REPL.js) until it's actually needed for interactive mode.


24. Configuration Defaults & GrowthBook Flags

GrowthBook Feature Flags (Bridge)

Flag Type Default Purpose
tengu_ccr_bridge boolean false Master gate: enables Remote Control
tengu_bridge_repl_v2 boolean false Enables env-less (V2) REPL bridge
tengu_bridge_repl_v2_cse_shim_enabled boolean true cse_*session_* retag shim
tengu_bridge_min_version DynamicConfig {minVersion} '0.0.0' Min CLI version for V1 bridge
tengu_bridge_repl_v2_config DynamicConfig (EnvLessBridgeConfig) See defaults V2 bridge timing config
tengu_bridge_poll_interval_config DynamicConfig (PollIntervalConfig) See defaults Poll intervals
tengu_ccr_bridge_multi_session boolean N/A Enables multi-session spawn modes
tengu_sessions_elevated_auth_enforcement boolean false Enables trusted device requirement
tengu_cobalt_harbor boolean false Auto-connect CCR on startup
tengu_ccr_mirror boolean false CCR mirror mode

Build Flags (bun:bundle features)

Flag Purpose
BRIDGE_MODE Enables bridge-related code paths
CCR_AUTO_CONNECT Enables getCcrAutoConnectDefault()
CCR_MIRROR Enables isCcrMirrorEnabled()
BASH_CLASSIFIER Enables bash classifier reason serialization
TRANSCRIPT_CLASSIFIER Enables transcript classifier reason serialization

Environment Variables (Bridge)

Variable Purpose
CLAUDE_BRIDGE_OAUTH_TOKEN Ant-only: override OAuth token for bridge
CLAUDE_BRIDGE_BASE_URL Ant-only: override API base URL
CLAUDE_TRUSTED_DEVICE_TOKEN Override trusted device token from env
CLAUDE_CODE_USE_CCR_V2 Use SSETransport + CCRClient for all sessions
CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2 Use HybridTransport (WS reads + POST writes)
CLAUDE_CODE_CCR_MIRROR Enable CCR mirror mode
CLAUDE_CODE_SESSION_ACCESS_TOKEN Session ingress auth token
CLAUDE_CODE_ENVIRONMENT_RUNNER_VERSION Sent as x-environment-runner-version header
CLAUDE_CODE_OAUTH_REFRESH_TOKEN Fast-path login via token exchange
CLAUDE_CODE_OAUTH_SCOPES Required when CLAUDE_CODE_OAUTH_REFRESH_TOKEN is set

25. Complete API Endpoint Reference

Environments API (/v1/environments/bridge)

All environment API calls require:

  • anthropic-version: 2023-06-01
  • anthropic-beta: environments-2025-11-01
  • x-environment-runner-version: <version>
  • Authorization: Bearer <token>
  • Optional: X-Trusted-Device-Token: <token>

POST /v1/environments/bridge

Register a bridge environment.

Request:

{
  "machine_name": "hostname",
  "directory": "/path/to/dir",
  "branch": "main",
  "git_repo_url": "https://github.com/owner/repo",
  "max_sessions": 4,
  "metadata": { "worker_type": "claude_code" },
  "environment_id": "<backend-issued-id>"   // Optional: for re-registration
}

Response:

{
  "environment_id": "env_abc123",
  "environment_secret": "secret_xyz"
}

Timeout: 15s

GET /v1/environments/{environmentId}/work/poll

Poll for new work. Auth: environmentSecret.

Query params:

  • reclaim_older_than_ms (optional): Reclaim unacknowledged work older than this

Response: WorkResponse | null (null = no work available)

Timeout: 10s

POST /v1/environments/{environmentId}/work/{workId}/ack

Acknowledge a work item. Auth: sessionToken (session ingress JWT).

Timeout: 10s

POST /v1/environments/{environmentId}/work/{workId}/heartbeat

Send heartbeat. Auth: sessionToken (session ingress JWT).

Response:

{
  "lease_extended": true,
  "state": "running",
  "last_heartbeat": "2025-03-31T...",
  "ttl_seconds": 300
}

Timeout: 10s

POST /v1/environments/{environmentId}/work/{workId}/stop

Stop a work item. Auth: OAuth token.

Request: { "force": true|false }

Timeout: 10s

DELETE /v1/environments/bridge/{environmentId}

Deregister environment. Auth: OAuth token.

Timeout: 10s

POST /v1/environments/{environmentId}/bridge/reconnect

Force re-dispatch a session. Auth: OAuth token.

Request: { "session_id": "cse_abc123" }

Timeout: 10s

Sessions API (/v1/sessions)

All calls require anthropic-beta: ccr-byoc-2025-07-29 and x-organization-uuid: <orgUuid>.

POST /v1/sessions

Create a session.

Request:

{
  "title": "My Session",
  "events": [{ "type": "event", "data": <SDKMessage> }],
  "session_context": {
    "sources": [{ "type": "git_repository", "url": "...", "revision": "main" }],
    "outcomes": [...],
    "model": "claude-opus-4"
  },
  "environment_id": "env_abc123",
  "source": "remote-control",
  "permission_mode": "auto"
}

Response: { "id": "session_abc123" }

GET /v1/sessions/{sessionId}

Fetch session metadata. Returns { environment_id?, title? }.

PATCH /v1/sessions/{sessionId}

Update session title. Request: { "title": "New Title" }

POST /v1/sessions/{sessionId}/archive

Archive a session. Returns 409 if already archived (idempotent).

POST /v1/sessions/{sessionId}/events

Send events to a session (used for permission responses). Auth: session ingress token.

Request:

{
  "events": [
    {
      "type": "control_response",
      "response": {
        "subtype": "success",
        "request_id": "req_abc",
        "response": { "behavior": "allow" }
      }
    }
  ]
}

CCR V2 Code Sessions API

POST /v1/code/sessions

Request:

{
  "title": "Bridge Session",
  "bridge": {},
  "tags": ["optional", "tags"]
}

Response: { "session": { "id": "cse_abc123" } }

POST /v1/code/sessions/{sessionId}/bridge

Register as bridge worker and get JWT.

Optional header: X-Trusted-Device-Token

Response:

{
  "worker_jwt": "sk-ant-si-...",
  "api_base_url": "https://...",
  "expires_in": 18000,
  "worker_epoch": "42"
}

Note: Each call bumps worker_epoch — this call IS the registration.

POST /v1/code/sessions/{sessionId}/worker/register

Register as CCR worker (V1 CCR v2 path). Returns { "worker_epoch": "42" }.

GET /v1/code/sessions/{sessionId}/worker/events/stream

SSE stream for receiving inbound events. Sends Last-Event-ID or from_sequence_num for resumption.

SSE frame format:

event: sdk_event
id: 42
data: {"event_id":"evt_abc","payload":{...}}

:keepalive

POST /v1/code/sessions/{sessionId}/worker/events

Post events to the session. Auth: worker JWT.

PUT /v1/code/sessions/{sessionId}/worker

Update worker state.

Request:

{
  "worker_status": "running" | "requires_action" | "completed",
  "external_metadata": { "key": "value" },
  "internal_metadata": { "key": "value" }
}

Metadata merge: RFC 7396 — keys added/overwritten, null values = server-side delete.

POST /v1/code/sessions/{sessionId}/worker/events/{eventId}/delivery

Report event delivery status. Request: { "status": "received" | "processing" | "processed" }

Auth / Trusted Device

POST /api/auth/trusted_devices

Enroll device for elevated security sessions.

Request:

{ "display_name": "Claude Code on hostname · darwin" }

Response:

{ "device_token": "...", "device_id": "..." }

Gate: Must be called within 10 minutes of login.

File Attachments

GET /api/oauth/files/{fileUuid}/content

Download attachment content. Auth: OAuth token. Returns binary file data.

Sessions WebSocket (Remote Viewer)

WS /v1/sessions/ws/{sessionId}/subscribe?organization_uuid={orgUuid}

Connect as viewer. After connect, send:

{ "type": "auth", "credential": { "type": "oauth", "token": "<accessToken>" } }

26. WebSocket & SSE Protocol Reference

Session-Ingress WebSocket Protocol

URL: wss://api.anthropic.com/v1/session_ingress/ws/{sessionId}

V1 read/write: Messages in both directions are NDJSON (StdoutMessage / StdinMessage).

Permanent close codes (client stops retrying):

  • 1002 — protocol error
  • 4001 — session expired/not found
  • 4003 — unauthorized

Keep-alive frame: {"type":"keep_alive"} — sent by client at DEFAULT_KEEPALIVE_INTERVAL (5 min).

Ping/pong: Client sends ping every 10s, expects pong within 10s. Connection recycled on pong timeout.

SSE Event Format

event: sdk_event
id: <sequence_number>
data: <json_payload>

:keepalive

  • id field: monotonic sequence number used for resume (Last-Event-ID)
  • event field: event type (sdk_event, keep_alive, etc.)
  • data field: JSON object with event_id and payload

Control Message Protocol

SDKControlRequest (server → client):

{
  "type": "control_request",
  "request_id": "req_abc",
  "request": {
    "subtype": "initialize" | "set_model" | "set_max_thinking_tokens" | "set_permission_mode" | "interrupt" | "can_use_tool",
    ...subtype-specific fields...
  }
}

SDKControlResponse (client → server):

{
  "type": "control_response",
  "session_id": "session_abc",
  "response": {
    "subtype": "success" | "error",
    "request_id": "req_abc",
    ...response payload...
  }
}

SDKControlCancelRequest (server → client):

{
  "type": "control_cancel_request",
  "request_id": "req_abc"
}

Permission Request Flow

  1. Child CLI emits control_request with subtype: 'can_use_tool' on stdout.
  2. Bridge receives it via onPermissionRequest callback.
  3. Bridge forwards to server via POST /v1/sessions/{id}/events (permission response event).
  4. Claude.ai displays approval dialog.
  5. User approves/denies.
  6. Server sends control_response back through the WebSocket.
  7. Bridge calls onPermissionResponse callback.
  8. Child CLI receives the decision on stdin and proceeds.

Permission response payload:

{
  "behavior": "allow" | "deny",
  "updatedInput": {...},          // Optional: modified tool input
  "updatedPermissions": [...],    // Optional: new permission rules
  "message": "..."                // Optional: deny message
}

NDJSON Safety

All NDJSON-format messages (used in child process stdio) must escape U+2028 and U+2029 as \u2028/\u2029 (see ndjsonSafeStringify). These are valid JSON but are JavaScript line terminators that can break line-splitting receivers.


Document generated from source analysis of Claude Code codebase, 2026-03-31.