Version 1.1.0-beta.1 · Node.js ≥ 20 · TypeScript
┌─────────────────┐ ┌──────────────────────────────┐ ┌──────────────────┐
│ Claude Code │ │ Bridge (Node.js) │ │ VS Code │
│ CLI / Desktop │ │ │ │ Extension │
│ │◄──WS──►│ McpTransport ExtensionClient│◄──WS──►│ │
│ (MCP client) │ HTTP │ (JSON-RPC 2.0) (custom RPC) │ │ (LSP, editor, │
│ │ stdio │ │ │ debug APIs) │
└─────────────────┘ └──────────────────────────────┘ └──────────────────┘
│
┌───────────┴───────────┐
│ HTTP endpoints │
│ /health /dashboard │
│ /mcp (Streamable HTTP)│
│ /oauth/* │
└───────────────────────┘
Two independent WebSocket connections run simultaneously. The Claude Code ↔ Bridge link carries MCP JSON-RPC 2.0: tool calls, resource reads, prompt invocations. The Extension ↔ Bridge link carries a separate custom request/response protocol (extension/<method>) over which the extension pushes live IDE state (diagnostics, selections, debug state) and responds to tool requests that need VS Code APIs. When the extension is disconnected, tools with extensionRequired: true return isError: true immediately rather than blocking.
| File | Purpose |
|---|---|
src/bridge.ts |
Top-level orchestrator: spawns server, extension client, automation hooks, orchestrator, plugins; manages AgentSession lifecycle and grace-period reconnection |
src/server.ts |
HTTP server; routes /health, /dashboard, /dashboard/data, /mcp, /oauth/*, /notify; WebSocket upgrade for Claude Code clients and extension |
src/config.ts |
CLI flag parsing, defaults, validation; produces Config object consumed by every subsystem |
src/version.ts |
BRIDGE_PROTOCOL_VERSION (wire compat) and PACKAGE_VERSION (npm) as separate constants |
| File | Purpose |
|---|---|
src/transport.ts |
McpTransport: per-session JSON-RPC 2.0 handler; AJV arg validation; rate limiting (200 req/min ring buffer); tool dispatch; cursor-paginated tools/list |
src/extensionClient.ts |
ExtensionClient: bridge↔extension WebSocket RPC; circuit breaker (3 timeouts/30s → open); tryRequest, validatedRequest helpers; pushes cached state (diagnostics, selection, active file) |
src/streamableHttp.ts |
StreamableHttpHandler: MCP over HTTP POST/GET/DELETE /mcp; session pool (max 5, oldest-idle eviction) |
src/sessionCheckpoint.ts |
Periodic session snapshots to ~/.claude/ide/checkpoint-<port>.json; restored on restart |
src/lockfile.ts |
LockFileManager: writes ~/.claude/ide/<port>.lock with O_EXCL (0o600); isBridge: true flag; PID reuse guard |
src/wsUtils.ts |
safeSend(), backpressure handling (1 MB threshold, 5s drain timeout); best-effort notification delivery |
| File/Directory | Purpose |
|---|---|
src/tools/index.ts |
registerAllTools(): builds all tool factories, applies SLIM_TOOL_NAMES filter in non-full mode, merges plugin tools |
src/tools/*.ts |
Individual tool factories; each exports createXxxTool(deps) returning { schema, handler } |
src/tools/lsp.ts |
14 LSP tools (goToDefinition, findReferences, getHover, getCallHierarchy, renameSymbol, etc.) |
src/tools/utils.ts |
resolveFilePath(): path-traversal prevention; null-byte rejection; symlink ancestor walk; workspace containment |
src/tools/headless/ |
lspClient.ts (Content-Length-framed JSON-RPC to typescript-language-server); lspFallback.ts (LSP fallback for goToDefinition, findReferences, getTypeSignature when extension absent) |
| File | Purpose |
|---|---|
src/automation.ts |
AutomationHooks: all 18 hook handlers; cooldown enforcement; loop guards; _enqueueAutomationTask(); AutomationPolicy interface |
src/claudeDriver.ts |
SubprocessDriver: spawns claude --verbose subprocess; streams JSONL output; abort/timeout; --effort, --model, --system-prompt forwarding |
src/claudeOrchestrator.ts |
ClaudeOrchestrator: task queue, status lifecycle (`pending→running→done |
| File | Purpose |
|---|---|
src/pluginLoader.ts |
Reads claude-ide-bridge-plugin.json manifest; imports index.mjs entrypoint; calls register(ctx); validates toolNamePrefix |
src/pluginWatcher.ts |
PluginWatcher: fs.watch + 300ms debounce; ESM cache-busting (?t=<timestamp>); atomic re-registration |
| File | Purpose |
|---|---|
src/oauth.ts |
OAuthServerImpl: RFC 8414/7591/9728 endpoints; PKCE S256; 24h opaque access tokens; timing-safe comparisons |
src/probe.ts |
probeAll(): detects available CLI tools (ctags, typescript-language-server, rg, git, gh); results drive getToolCapabilities |
src/activityLog.ts |
Bounded ring buffer (500 entries); percentiles; co-occurrence; JSONL persistence |
src/errors.ts |
ErrorCodes (JSON-RPC -32xxx) and ToolErrorCodes (string codes for isError: true content) |
src/dashboard.ts |
Inline HTML/CSS/JS served at /dashboard; /dashboard/data JSON: version, uptimeMs, sessions, extensionConnected |
- Arrive: JSON-RPC 2.0 message arrives over WebSocket (or HTTP POST
/mcpfor Streamable HTTP transport). - Auth check (
server.ts):x-claude-code-ide-authorizationheader compared withcrypto.timingSafeEqual; 401 on mismatch. Host header checked for DNS rebinding. - Rate limit (
transport.ts): O(1) ring-buffer check (200 req/min). Exceeding returns-32004 RATE_LIMIT_EXCEEDED. Failed AJV validation does not consume tokens. - Initialize guard:
tools/callandtools/listblocked until client has sentinitialize+ bridge sentinitializednotification. - AJV validation: tool
inputSchemavalidated at transport layer before handler is invoked. Invalid args return-32602 INVALID_PARAMS. - Dispatch (
transport.ts → tools/index.ts): tool name looked up in registered map. Unknown tool →isError: truecontent. - Extension gate: if
schema.extensionRequired === trueand extension is disconnected, handler is skipped; response isisError: truewith reconnect instructions. - Handler execution: factory-produced handler runs. Calls
extensionClient.tryRequest()/validatedRequest()for VS Code operations, or native Node.js APIs directly. - LSP fallback (select tools only): if extension disconnected and tool has
extensionFallback: true, handler retries viatypescript-language-serverstdio LSP. - Serialize: handler returns
{ content: [...] }. Tools withoutputSchemaalso populatestructuredContent. Results exceeding 50 KB inject_meta["anthropic/maxResultSizeChars"]for Claude Code 2.1.91+ persistence hint. - Return: JSON-RPC response sent via
safeSend()with backpressure check.
registerAllTools() in src/tools/index.ts constructs all tool factories with explicit dependency injection (workspace path, extensionClient, config, probes, activityLog, orchestrator, automationHooks, fileLock, transport). It returns a flat ToolSchema[] array.
Full mode (default since v2.43.0): the SLIM_TOOL_NAMES filter is bypassed; all 177 tools are registered. Full-only tools include git write operations, terminal, file tree, GitHub, HTTP client, Claude orchestration, and code quality tools.
Slim mode (--slim): SLIM_TOOL_NAMES is a Set<string> of 61 names. Only tools whose schema.name is in this set are registered. Slim mode exposes IDE-exclusive tools — LSP, debugger, editor state, decorations — that Claude Code cannot replicate natively.
Plugin tools: always bypass the slim filter regardless of mode. Registered after built-ins via ctx.registerTool(schema, handler) in the plugin's register(ctx) call.
Runtime introspection: getToolCapabilities reports which tools are available, which require the extension (extensionRequired), which have probe-based fallbacks (probe:<name>), and which have dual-path availability (extensionFallback).
A dedicated WebSocket connection (separate from the Claude Code connection) runs between the VS Code extension and the bridge. The extension initiates the connection; the bridge accepts it on the same port via path /extension.
Auth: extension sends x-claude-ide-extension: <bridgeToken> on upgrade; bridge validates with crypto.timingSafeEqual.
Method naming: requests use extension/<camelCase> (e.g. extension/getDiagnostics, extension/goToDefinition). Notifications from extension to bridge use the same scheme (e.g. extension/diagnosticsChanged, extension/selectionChanged).
Push state: extension pushes diagnostics, selections, active file, AI comments, and debug state as notifications. ExtensionClient caches these locally (capped: 500 files for diagnostics, 200 for AI comments, 32 KB for active file content). Tool handlers read from cache without making a round-trip to the extension.
Circuit breaker: ExtensionClient opens after ≥ 3 timeouts within a 30-second window. While open, all extension requests fast-fail. Exponential backoff with full jitter governs the half-open probe interval. When breaker opens, extensionRequired tools return isError: true; tools with LSP fallback reroute to typescript-language-server.
AutomationHooks (src/automation.ts) implements the hooks defined in the AutomationPolicy interface. The author-facing surface uses unified, discriminated hooks (v2.43.0+); the parser expands them into internal slots:
| Hook | Trigger |
|---|---|
onDiagnosticsStateChange |
New error/warning diagnostics (state: "error") or errors clear (state: "cleared") for a file |
onFileSave |
Matching file saved (minimatch glob) |
onFileChanged |
Matching file buffer changed (CC 2.1.83+) |
onRecipeSave |
Any .yaml/.yml file saved |
onCwdChanged |
Claude Code working directory changes (CC 2.1.83+) |
onCompaction |
Claude Code compacts context — phase: "pre" / "post" (CC 2.1.76+) |
onInstructionsLoaded |
Claude Code session starts (CC 2.1.76+) |
onTestRun |
runTests tool completes — filter: "any"|"failure"|"pass-after-fail" |
onGitCommit |
gitCommit tool succeeds |
onGitPush |
gitPush tool succeeds |
onGitPull |
gitPull tool succeeds |
onBranchCheckout |
gitCheckout tool succeeds |
onPullRequest |
githubCreatePR tool succeeds |
onTaskCreated |
Claude Code TaskCreated hook (CC 2.1.84+) |
onTaskSuccess |
Orchestrator task completes with status done |
onPermissionDenied |
Claude Code PermissionDenied hook (CC 2.1.89+) |
onDebugSession |
VS Code debug session — phase: "start" / "end" |
The pre-v2.43.0 split names (onDiagnosticsError/onDiagnosticsCleared, onPreCompact/onPostCompact, onTestPassAfterFailure, onDebugSessionStart/onDebugSessionEnd) are still accepted but emit a deprecation warning.
Event flow: lifecycle event fires (tool call completes or /notify POST received) → handle*() method checks policy enabled + per-file/event cooldown (min 5s) + loop guard (prevents re-entrant triggers) → _evaluateWhen() checks AutomationCondition (minDiagnosticCount, diagnosticsMinSeverity, testRunnerLastStatus) → _enqueueAutomationTask() → SubprocessDriver spawns claude --verbose subprocess with the resolved prompt. Rolling 60-minute rate limit window caps tasks per hour (default 20). Requires --driver subprocess.
Load path at bridge startup:
--plugin <path-or-npm-package>CLI flag (repeatable) passed toloadPlugins()insrc/pluginLoader.ts.pluginLoaderreadsclaude-ide-bridge-plugin.jsonmanifest (schemaVersion: 1). ValidatestoolNamePrefixmatches/^[a-zA-Z][a-zA-Z0-9_]{1,19}$/.- Imports
index.mjsentrypoint via dynamicimport(). - Calls
register(ctx)wherectxexposesworkspace,workspaceFolders,config,logger, andctx.registerTool(schema, handler). - Registered tools are merged into the registry after all built-ins; they always bypass the slim filter.
Hot reload (--plugin-watch): PluginWatcher (src/pluginWatcher.ts) watches the plugin directory. File changes trigger a 300ms debounced reload. ESM module cache is busted via ?t=<timestamp> query param. Tools are re-registered atomically under a reloadInFlight guard. Tool names must start with the declared toolNamePrefix.
WebSocket sessions (Claude Code clients): AgentSession per connection tracked in Bridge.sessions map. On WebSocket close, a grace timer starts (default 120s). A reconnecting client that sends X-Claude-Code-Session-Id matching an in-grace session is reattached to it — no new initialization, openedFiles set preserved. The stdio shim generates a stable per-process UUID automatically.
HTTP sessions (Streamable HTTP /mcp): max 5 concurrent. When at capacity, the oldest idle session (idle > 60s) is evicted. Sessions with active tool calls are not evicted. 10-minute idle TTL.
Lock file: ~/.claude/ide/<port>.lock created with O_EXCL (prevents symlink attacks), permissions 0o600. Schema: { pid, workspace, authToken, isBridge: true, startedAt }. isBridge: true distinguishes bridge-owned locks from IDE-owned locks. PID reuse guard: a lock whose PID resolves to a live process that started more than 24 hours ago is treated as stale and cleaned up.
Per-session stats: callCount and errorCount are not reset on detach(); they accumulate across reconnects and are read at final cleanupSession().
- Token validation: bridge auth token compared with
crypto.timingSafeEqualon every request; applies to WebSocket upgrade header, HTTP Bearer token, and extension handshake. - DNS rebinding defense: Host header parsed; non-loopback hostnames rejected on WebSocket upgrade.
- OAuth 2.0 (
--issuer-url): PKCE S256 mandatory; auth codes single-use with 5-min TTL; access tokens opaque base64url with 24h TTL; timing-safe comparisons throughout. - SSRF (
sendHttpRequest): three-layer defense — lexicalisPrivateHost()blocklist → DNS pre-resolution re-check → post-resolution IP pinning.::ffff:prefix recursively unwrapped. Host header override applied after user headers. - Command allowlist (
runCommand): only explicitly allowlisted commands execute. Interpreter commands (node, python, bash, etc.) permanently blocked from--allow-command. Argument splitting prevents--flag=valueinjection. - Path traversal (
resolveFilePathinsrc/tools/utils.ts): rejects null bytes; walks full symlink ancestor chain; rejects any path resolving outside workspace root. - Input validation: AJV validates all tool arguments at transport layer before handler invocation. Failed validation does not consume rate-limit tokens.
- Rate limiting: 200 requests/min (ring buffer, O(1)); 500 notifications/min; per-session tool token bucket (default 60/min, configurable via
--tool-rate-limit).
- Dual version numbers —
BRIDGE_PROTOCOL_VERSION(wire format, bumped rarely) vsPACKAGE_VERSION(npm, every release). Same pattern applies to the VS Code extension:EXTENSION_PROTOCOL_VERSION("1.1.0") vs npm package version. → ADR-0001 - Generation guards on reconnect — every WebSocket callback captures
genat attach time and checksgen !== this.generationbefore mutating state, preventing stale callbacks from corrupting a new connection. → ADR-0002 isBridgelock file flag — bridge-created locks carryisBridge: trueso tooling (VS Code extension lock scanner,bridgeDoctor) can distinguish them from IDE-owned locks on the same port range. → ADR-0003- Tool errors as content — tool execution errors return
{ isError: true }in the MCP content array, not JSON-RPC-32xxxerror objects. JSON-RPC errors are reserved for protocol-level failures (auth, parse, rate limit). → ADR-0004 - HTTP session eviction — oldest idle session (idle > 60s) evicted when the 5-session pool is full, rather than rejecting new connections. Active sessions are never evicted. → ADR-0005