Article Detail

Claude Code'S Bridge Layer: How IDE Integration Works

2026-05-14MDX POCen

<>

Claude Code started as a terminal application. You type in a terminal, it responds in a terminal. But terminals aren’t where most developers spend their day anymore. The real action is in VS Code, JetBrains, and the browser.

The bridge layer is what connects Claude Code to those environments. It’s about 31 files in src/bridge/, guarded by a feature flag that defaults to off. When it’s enabled, Claude Code stops being just a terminal REPL and becomes a service that IDEs and the web UI can talk to.

Architecture Overview

The bridge connects three parties: the CLI (running Claude Code), a server-side session-ingress layer, and a client (IDE extension or web UI).

IDE / claude.ai  ──WebSocket/SSE──→  Session-Ingress  ──→  CLI (replBridge)
   ←── POST / CCRClient writes ────  Session-Ingress  ←──  CLI

There are two transport generations, which is a sign of how quickly this system evolved:

Version Read Path Write Path
v1 (env-based) WebSocket to Session-Ingress HTTP POST to Session-Ingress
v2 (env-less) SSE stream via SSETransport CCRClient to /worker/* endpoints

Both are wrapped behind a ReplBridgeTransport interface, so the rest of the system doesn’t care which one is active.

Transport Protocols

<>

The original design. Claude Code registers an environment with the session-ingress service, gets a WebSocket URL, and polls for work. Flow:

  1. Register environment → get a WorkSecret (base64url-encoded token with session config)
  2. Poll for work → acknowledge → spawn session
  3. Open WebSocket for inbound messages
  4. POST responses back via HTTP

The WorkSecret is the key payload. It bundles the session-ingress token, API base URL, git sources, and auth tokens into a single base64-encoded blob. Decoding it gives the bridge everything it needs to establish a session.

<>

The newer design, simpler setup. No environment registration needed:

  1. Create a session directly via API
  2. POST to /bridge endpoint to get a JWT
  3. Open SSE stream for inbound messages
  4. Write outbound via CCRClient

The v2 path reduces setup complexity significantly. No polling, no environment registration — just a direct connection.

Authentication

The bridge has a layered auth model with several token types:

  1. OAuth tokens — The user must be a claude.ai subscriber (isClaudeAISubscriber())
  2. JWT — Session-ingress tokens with sk-ant-si- prefix and exp claims. Proactive refresh before expiry.
  3. Trusted Device tokenX-Trusted-Device-Token header for elevated security sessions
  4. Environment secret — Base64url-encoded WorkSecret bundling all session config

There are dev overrides too: CLAUDE_BRIDGE_OAUTH_TOKEN and CLAUDE_BRIDGE_BASE_URL let engineers test against staging environments.

Message Flow

Inbound (Server → CLI)

Messages flow from the IDE/web UI, through session-ingress, to the CLI:

  • user messages — prompts typed in the web UI, enqueued into the REPL input queue
  • control_request — lifecycle commands: initialize, set_model, interrupt, set_permission_mode, set_max_thinking_tokens
  • control_response — permission decisions coming back from the IDE dialog

Outbound (CLI → Server)

Messages flow from the CLI back to the client:

  • assistant messages — Claude’s responses
  • user messages — echoed for sync
  • result messages — turn completion notifications
  • System events, tool starts, activities

Deduplication is handled by a BoundedUUIDSet (capacity 2000) that tracks recent message UUIDs to reject echoes and re-deliveries. This prevents the inevitable bugs that come with distributed message passing.

Lifecycle

A bridge session goes through a well-defined lifecycle:

  1. Entitlement check: isBridgeEnabled() checks a GrowthBook feature gate (tengu_ccr_bridge) plus the OAuth subscriber status
  2. Session creation: createBridgeSession() POSTs to the API
  3. Transport init: v1 HybridTransport or v2 SSETransport + CCRClient
  4. Message pump: Read inbound via transport, write outbound via batch
  5. Token refresh: createTokenRefreshScheduler() proactively refreshes JWTs before expiry
  6. Teardown: teardown() flushes pending messages, closes transport, archives session

Spawn Modes

When running claude remote-control, the bridge supports three spawn modes:

Mode Behavior
single-session One session in cwd, bridge tears down when it ends
worktree Persistent server, each session gets an isolated git worktree
same-dir Persistent server, sessions share cwd

The worktree mode is the most interesting for multi-session setups. Each session operates in its own git worktree, so multiple users or IDE sessions can work on different branches simultaneously without conflicts.

<>

When the IDE sends a tool execution request, permissions flow through a different path than in the terminal REPL:

IDE sends can_use_tool control request


CLI processes permission check chain

    ├── Auto-mode → CLI automatically decides allow/deny

    └── Interactive-mode:


        CLI sends control_request to IDE → IDE shows permission dialog

            User clicks Allow / Deny / Always Allow


        IDE returns control_response { behavior: 'allow' | 'deny', updatedInput? }


        CLI executes (or skips) the tool based on response

In auto mode, the CLI decides silently. In interactive mode, the permission dialog appears in the IDE — a native dialog, not a terminal prompt. This is a fundamentally different UX from the terminal, where the user is already looking at a terminal. In the IDE, the permission request needs to be a notification or dialog that doesn’t break the editing flow.

Feature Gate Architecture

The entire bridge is behind feature('BRIDGE_MODE'), which defaults to false. This is a compile-time feature flag using bun:bundle:

import { feature } from 'bun:bundle'
// feature('BRIDGE_MODE') returns false by default

The gate is applied at every entry point:

Location Guard
CLI entrypoint feature('BRIDGE_MODE') && args[0] === 'remote-control'
REPL bridge hooks All useAppState calls gated by ternary
UI components Early return null if bridge is off
Config settings Spread only when bridge is enabled

This is well-engineered dead code elimination. When BRIDGE_MODE is false, the bundler strips entire branches at build time. The bridge code can reference OAuth libraries, WebSocket handlers, and SSE transports without impacting the terminal-only binary size.

Why This Matters

Bridge imports reach across the codebase. Files that reference bridge types or functions exist in hooks, components, settings, tools, and commands. If the feature gate were leaky, a missing import or runtime error could crash the terminal REPL. The safety comes from three design choices:

  1. All bridge files exist — imports resolve even when bridge is off
  2. No side effects at import time — bridge modules define functions but don’t execute bridge logic on import
  3. Runtime guardsisBridgeEnabled() returns false, getReplBridgeHandle() returns null, useReplBridge short-circuits

There’s also a stub file (src/bridge/stub.ts) providing isBridgeAvailable()false and silent no-op handles, which gives any future code a safe fallback without touching the feature flag.

Chrome Extension Integration

Separate from the main bridge, there are two Chrome-specific entry points:

--claude-in-chrome-mcp

Starts an MCP server that bridges Claude Code with a Chrome extension. Uses StdioServerTransport (MCP over stdin/stdout) and supports both native socket and WebSocket bridge (wss://bridge.claudeusercontent.com). Gated by a separate GrowthBook flag (tengu_copper_bridge).

--chrome-native-host

Implements Chrome’s native messaging protocol — 4-byte length prefix + JSON over stdin/stdout. Creates a Unix domain socket server that proxies MCP messages between the Chrome extension and local Claude Code instances.

Both Chrome paths use dynamic imports, so they have zero startup cost unless explicitly invoked. They’re completely independent of the BRIDGE_MODE feature flag.

Key Types

Understanding the bridge means understanding a handful of core types:

  • BridgeConfig — Full bridge configuration (directory, auth, URLs, spawn mode, timeouts)
  • WorkSecret — Decoded work payload (token, API URL, git sources, MCP config)
  • SessionHandle — Running session handle (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'

Activating the Bridge

The bridge isn’t something you flip on in config. It requires:

  1. Environment variable: export CLAUDE_CODE_BRIDGE_MODE=true
  2. Auth: Logged in to claude.ai with an active subscription
  3. GrowthBook gate: tengu_ccr_bridge enabled for your org
  4. IDE extension: VS Code or JetBrains extension installed
  5. Network: WebSocket/SSE connectivity to session-ingress

Then you run claude remote-control and a QR code appears in the terminal — scan it to connect your IDE.

What I Find Interesting

The bridge is one of those systems that’s invisible when it works and deeply confusing when it doesn’t. The dual-transport design (v1 vs v2) suggests the team iterated quickly on the protocol, and the v2 simplification (no environment registration) is exactly the kind of learning you only get from running something in production.

The feature gate discipline is worth studying. Every bridge feature — every hook, every component, every tool — is wrapped in feature('BRIDGE_MODE') or an equivalent runtime guard. There’s no half-state where the bridge is partially active. This is how you build features that can be developed in a shared codebase without risking the core product.

The Chrome integration is a nice touch. It’s a completely separate subsystem with its own entry points, its own auth, and its own transport. The fact that it coexists with the main bridge without conflicting is a testament to the modularity of the architecture.