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

2233 lines
74 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Code — Bridge Protocol, CLI Framework & Remote Systems
## Table of Contents
1. [Bridge System Overview](#1-bridge-system-overview)
2. [Bridge Types & Core Data Structures](#2-bridge-types--core-data-structures)
3. [Bridge API Client](#3-bridge-api-client)
4. [Bridge Configuration & Auth](#4-bridge-configuration--auth)
5. [Bridge Entitlement & Feature Gating](#5-bridge-entitlement--feature-gating)
6. [Session Lifecycle: Standalone Bridge (bridgeMain.ts)](#6-session-lifecycle-standalone-bridge-bridgemaints)
7. [REPL Bridge (replBridge.ts / initReplBridge.ts)](#7-repl-bridge-replbridgets--initreplbridgets)
8. [Env-Less Bridge Core (remoteBridgeCore.ts)](#8-env-less-bridge-core-remotebridgecorts)
9. [Transport Layer](#9-transport-layer)
10. [Message Protocol (bridgeMessaging.ts)](#10-message-protocol-bridgemessagingts)
11. [JWT Authentication (jwtUtils.ts)](#11-jwt-authentication-jwtutilsts)
12. [Session ID Compatibility (sessionIdCompat.ts)](#12-session-id-compatibility-sessionidcompatts)
13. [Work Secrets & CCR v2 Registration (workSecret.ts)](#13-work-secrets--ccr-v2-registration-worksecretsts)
14. [Bridge Pointer: Crash Recovery (bridgePointer.ts)](#14-bridge-pointer-crash-recovery-bridgepointerts)
15. [Permission Callbacks (bridgePermissionCallbacks.ts)](#15-permission-callbacks-bridgepermissioncallbacksts)
16. [Inbound Messages & Attachments](#16-inbound-messages--attachments)
17. [Session Runner (sessionRunner.ts)](#17-session-runner-sessionrunnerts)
18. [Bridge Debug & Fault Injection (bridgeDebug.ts)](#18-bridge-debug--fault-injection-bridgedebugets)
19. [Bridge Utilities](#19-bridge-utilities)
20. [CLI Framework](#20-cli-framework)
21. [CLI Transports](#21-cli-transports)
22. [Remote Session System](#22-remote-session-system)
23. [replLauncher.tsx](#23-repplaunchertsx)
24. [Configuration Defaults & GrowthBook Flags](#24-configuration-defaults--growthbook-flags)
25. [Complete API Endpoint Reference](#25-complete-api-endpoint-reference)
26. [WebSocket & SSE Protocol 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}/bridge``createV2ReplTransport()`.
- 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.ts``runBridgeLoop()`.
- Supports multi-session spawn modes: `single-session`, `worktree`, `same-dir`.
---
## 2. Bridge Types & Core Data Structures
**File:** `bridge/types.ts`
### Constants
```typescript
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
```typescript
type WorkData = {
type: 'session' | 'healthcheck'
id: string // session ID
}
```
### WorkResponse
The response from polling for work (`GET .../work/poll`):
```typescript
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):
```typescript
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:
```typescript
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
```typescript
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
```typescript
type BridgeWorkerType = 'claude_code' | 'claude_code_assistant'
```
### SessionActivity
```typescript
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()`:
```typescript
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
```typescript
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
```typescript
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
```typescript
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`):
```typescript
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:
```typescript
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
```typescript
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
```typescript
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`)
```typescript
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
```typescript
type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed'
```
### BridgeCoreParams
The explicit-parameter interface to `initBridgeCore()` (enabling daemon/non-REPL callers):
```typescript
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`)
```typescript
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:
```typescript
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
```typescript
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
```typescript
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):
```typescript
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:
```typescript
{
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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`)
```typescript
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:**
```typescript
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`)
```typescript
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:**
```json
{
"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:
```json
{
"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.
```typescript
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:**
```typescript
{
getAccessToken: () => string | undefined | Promise<string | undefined>
onRefresh: (sessionId: string, oauthToken: string) => void
label: string
refreshBufferMs?: number // Default: TOKEN_REFRESH_BUFFER_MS = 5 min
}
```
**Constants:**
```typescript
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
```typescript
BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000 // 4 hours (matches Redis BRIDGE_LAST_POLL_TTL)
MAX_WORKTREE_FANOUT = 50
```
### BridgePointer Schema
```typescript
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
```typescript
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:**
```typescript
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 `mediaType``media_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
```typescript
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:
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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`.
```typescript
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
```typescript
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
```typescript
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
```typescript
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.
```typescript
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:
```typescript
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.
```typescript
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.
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```json
{
"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
```typescript
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
```typescript
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)
```typescript
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
```typescript
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](#9-transport-layer) for the transport hierarchy.
### cli/remoteIO.ts — RemoteIO class
```typescript
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
```typescript
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:
```json
{ "type": "auth", "credential": { "type": "oauth", "token": "<accessToken>" } }
```
3. Receive `SessionsMessage` stream.
**Configuration:**
```typescript
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:**
```typescript
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.
```typescript
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:**
```typescript
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
```typescript
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.
```typescript
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.
```typescript
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`
```typescript
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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"events": [
{
"type": "control_response",
"response": {
"subtype": "success",
"request_id": "req_abc",
"response": { "behavior": "allow" }
}
}
]
}
```
### CCR V2 Code Sessions API
#### POST `/v1/code/sessions`
**Request:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{ "display_name": "Claude Code on hostname · darwin" }
```
**Response:**
```json
{ "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:
```json
{ "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):
```json
{
"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):
```json
{
"type": "control_response",
"session_id": "session_abc",
"response": {
"subtype": "success" | "error",
"request_id": "req_abc",
...response payload...
}
}
```
**SDKControlCancelRequest** (server → client):
```json
{
"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:**
```json
{
"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.*