The bridge (src/bridge/, ~31 files) connects Claude Code CLI sessions to
remote IDE extensions (VS Code, JetBrains) and the claude.ai web UI. It is
gated behind feature('BRIDGE_MODE') which defaults to false.
The bridge uses two transport generations:
| Version | Read Path | Write Path | Negotiation |
|---|---|---|---|
| v1 (env-based) | WebSocket to Session-Ingress (ws(s)://.../v1/session_ingress/ws/{sessionId}) |
HTTP POST to Session-Ingress | Environments API poll/ack/dispatch |
| v2 (env-less) | SSE stream via SSETransport |
CCRClient → /worker/* endpoints |
Direct POST /v1/code/sessions/{id}/bridge → worker JWT |
Both wrapped behind ReplBridgeTransport interface (replBridgeTransport.ts).
The v1 path: register environment → poll for work → acknowledge → spawn session.
The v2 path: create session → POST /bridge for JWT → SSE + CCRClient directly.
- OAuth tokens — claude.ai subscription required (
isClaudeAISubscriber()) - JWT — Session-Ingress tokens (
sk-ant-si-prefixed) withexpclaims.jwtUtils.tsdecodes and schedules proactive refresh before expiry. - Trusted Device token —
X-Trusted-Device-Tokenheader for elevated security tier sessions. Enrolled viatrustedDevice.ts. - Environment secret — base64url-encoded
WorkSecretcontainingsession_ingress_token,api_base_url, git sources, auth tokens.
Dev override: CLAUDE_BRIDGE_OAUTH_TOKEN and CLAUDE_BRIDGE_BASE_URL
(ant-only, process.env.USER_TYPE === 'ant').
IDE / claude.ai ──WebSocket/SSE──→ Session-Ingress ──→ CLI (replBridge)
←── POST / CCRClient writes ──── Session-Ingress ←── CLI
Inbound (server → CLI):
usermessages (prompts from web UI) →handleIngressMessage()→ enqueued to REPLcontrol_request(initialize, set_model, interrupt, set_permission_mode, set_max_thinking_tokens)control_response(permission decisions from IDE)
Outbound (CLI → server):
assistantmessages (Claude's responses)usermessages (echoed for sync)resultmessages (turn completion)- System events, tool starts, activities
Dedup: BoundedUUIDSet tracks recent posted/inbound UUIDs to reject echoes
and re-deliveries.
- Entitlement check:
isBridgeEnabled()/isBridgeEnabledBlocking()→ GrowthBook gatetengu_ccr_bridge+ OAuth subscriber check - Session creation:
createBridgeSession()→ POST to API - Transport init: v1
HybridTransportor v2SSETransport+CCRClient - Message pump: Read inbound via transport, write outbound via batch
- Token refresh: Proactive JWT refresh via
createTokenRefreshScheduler() - Teardown:
teardown()→ flush pending → close transport → archive session
Spawn modes for claude remote-control:
single-session: One session in cwd, bridge tears down when it endsworktree: Persistent server, each session gets an isolated git worktreesame-dir: Persistent server, sessions share cwd
BridgeConfig— Full bridge configuration (dir, auth, URLs, spawn mode, timeouts)WorkSecret— Decoded work payload (token, API URL, git sources, MCP config)SessionHandle— Running session (kill, activities, stdin, token update)ReplBridgeHandle— REPL bridge API (write messages, control requests, teardown)BridgeState—'ready' | 'connected' | 'reconnecting' | 'failed'SpawnMode—'single-session' | 'worktree' | 'same-dir'
The feature('BRIDGE_MODE') gate in src/shims/bun-bundle.ts defaults to
false (reads CLAUDE_CODE_BRIDGE_MODE env var). All critical code paths
are properly guarded:
| Location | Guard |
|---|---|
src/entrypoints/cli.tsx:112 |
feature('BRIDGE_MODE') && args[0] === 'remote-control' |
src/main.tsx:2246 |
feature('BRIDGE_MODE') && remoteControlOption !== undefined |
src/main.tsx:3866 |
if (feature('BRIDGE_MODE')) (Commander subcommand) |
src/hooks/useReplBridge.tsx:79-88 |
All useAppState calls gated by feature('BRIDGE_MODE') ternary |
src/hooks/useReplBridge.tsx:99 |
useEffect body gated by feature('BRIDGE_MODE') |
src/components/PromptInput/PromptInputFooter.tsx:160 |
if (!feature('BRIDGE_MODE')) return null |
src/components/Settings/Config.tsx:930 |
feature('BRIDGE_MODE') && isBridgeEnabled() spread |
src/tools/BriefTool/upload.ts:99 |
if (feature('BRIDGE_MODE')) |
src/tools/ConfigTool/supportedSettings.ts:153 |
feature('BRIDGE_MODE') spread |
All of the following are behind the feature gate and inactive:
runBridgeLoop()— Full bridge orchestration inbridgeMain.tsinitReplBridge()— REPL bridge initializationinitBridgeCore()/initEnvLessBridgeCore()— Transport negotiationcreateBridgeApiClient()— Environments API callsBridgeUI— Bridge status display and QR codes- Token refresh scheduling
- Multi-session management (worktree mode)
- Permission delegation to IDE
Static imports of bridge modules from outside src/bridge/ do NOT crash because:
- All bridge files exist — they're in the repo, so imports resolve.
- No side effects at import time — bridge modules define functions/types but don't execute bridge logic on import.
- Runtime guards — Functions like
isBridgeEnabled()returnfalsewhenfeature('BRIDGE_MODE')is false.getReplBridgeHandle()returnsnull.useReplBridgeshort-circuits via ternary operators.
Files with unguarded static imports (safe because files exist):
src/hooks/useReplBridge.tsx— imports types and utils from bridgesrc/components/Settings/Config.tsx— importsisBridgeEnabled(returns false)src/components/PromptInput/PromptInputFooter.tsx— early-returns nullsrc/tools/SendMessageTool/SendMessageTool.ts—getReplBridgeHandle()returns nullsrc/tools/BriefTool/upload.ts— guarded at call sitesrc/commands/logout/logout.tsx—clearTrustedDeviceTokenCacheis a no-op
Created src/bridge/stub.ts with:
isBridgeAvailable()→ always returnsfalsenoopBridgeHandle— silent no-opReplBridgeHandlenoopBridgeLogger— silent no-opBridgeLogger
Available for any future code that needs a safe fallback when bridge is off.
To enable the bridge:
export CLAUDE_CODE_BRIDGE_MODE=true- Must be logged in to claude.ai with an active subscription
(
isClaudeAISubscriber()must returntrue) - OAuth tokens obtained via
claude auth login(needsuser:profilescope) - GrowthBook gate
tengu_ccr_bridgemust be enabled for the user's org
- VS Code: Claude Code extension (connects via the bridge's Session-Ingress layer)
- JetBrains: Similar integration (same protocol)
- Web:
claude.ai/code?bridge={environmentId}URL
- Session-Ingress: WebSocket (
wss://) or SSE for reads; HTTPS POST for writes - API base: Production
api.claude.ai(configured via OAuth config) - Dev overrides:
CLAUDE_BRIDGE_BASE_URL, localhost usesws://and/v2/paths - QR code displayed in terminal links to
claude.ai/code?bridge={envId}
# Single session (tears down when session ends)
claude remote-control
# Named session
claude remote-control "my-project"
# With specific spawn mode (requires tengu_ccr_bridge_multi_session gate)
claude remote-control --spawn worktree
claude remote-control --spawn same-dir--remote-control [name]/--rc [name]— Start REPL with bridge pre-enabled--debug-file <path>— Write debug log to file--session-id <id>— Resume an existing session
Launches a Claude-in-Chrome MCP server via runClaudeInChromeMcpServer() from
src/utils/claudeInChrome/mcpServer.ts. This:
- Creates a
StdioServerTransport(MCP over stdin/stdout) - Uses
@ant/claude-for-chrome-mcppackage to create an MCP server - Bridges between Claude Code and the Chrome extension
- Supports both native socket (local) and WebSocket bridge (
wss://bridge.claudeusercontent.com) - Gated by
tengu_copper_bridgeGrowthBook flag (orUSER_TYPE=ant)
Not gated by feature('BRIDGE_MODE') — this is a separate subsystem. It only
runs when explicitly invoked with --claude-in-chrome-mcp flag.
Launches the Chrome Native Messaging Host via runChromeNativeHost() from
src/utils/claudeInChrome/chromeNativeHost.ts. This:
- Implements Chrome's native messaging protocol (4-byte length prefix + JSON over stdin/stdout)
- Creates a Unix domain socket server at a secure path
- Proxies MCP messages between Chrome extension and local Claude Code instances
- Has its own debug logging to
~/.claude/debug/chrome-native-host.txt(ant-only)
Not gated by feature('BRIDGE_MODE') — separate entry point. Only activated
when Chrome calls the registered native messaging host binary.
Both Chrome paths:
- Are dynamic imports — only loaded when the specific flag is passed
- Return immediately after their own
await— no side effects on normal CLI startup - Cannot crash normal operation because they're entirely separate code paths
- Have no dependency on the bridge feature flag
| Check | Status |
|---|---|
feature('BRIDGE_MODE') returns false by default |
✅ Verified in src/shims/bun-bundle.ts |
| Bridge code not executed when disabled | ✅ All call sites use feature() guard |
| No bridge-related errors on startup | ✅ Imports resolve (files exist), no side effects |
| CLI works in terminal-only mode | ✅ Bridge is purely additive |
| Chrome paths don't crash | ✅ Separate dynamic imports, only on explicit flags |
| Stub available for safety | ✅ Created src/bridge/stub.ts |