2.6 多入口架构:CLI / SDK / IDE / MCP 的统一路由

源码文件entrypoints/cli.tsx(启动路由器)、entrypoints/init.ts(初始化中枢)、entrypoints/agentSdkTypes.ts(SDK 公共 API)、entrypoints/mcp.ts(MCP 服务器入口)、entrypoints/sandboxTypes.ts(沙箱类型)、entrypoints/sdk/controlSchemas.ts(SDK 控制协议)、main.tsx(入口点检测与客户端类型)

核心概念:四层启动路由器、多入口点检测、CLAUDE_CODE_ENTRYPOINT 环境变量、SDK 控制协议、MCP 服务器模式、构建时 Feature Flag


导语:一个二进制,七种入口

原书第 2 章开篇即抛出一个核心挑战——模式爆炸

Claude Code 从最初的交互式 REPL 已经扩展出十余种运行模式——--version 快速查询、MCP 协议服务器、SDK 子进程集成、Bridge 远程控制、后台守护进程、模板脚手架……用户每次只使用其中一种,但所有模式共享同一个入口二进制文件。

这不是简单的"一个程序多种功能",而是一个二进制、七种入口的架构挑战:

入口类型 触发方式 核心文件 CLAUDE_CODE_ENTRYPOINT 值
CLI 交互式 claude(无参数) cli.tsxmain.tsx cli
SDK CLI 模式 claude -p "query" cli.tsxmain.tsx sdk-cli
TypeScript SDK TS SDK 调用 agentSdkTypes.ts sdk-ts
Python SDK Python SDK 调用 agentSdkTypes.ts sdk-py
VSCode 集成 VSCode 扩展启动 main.tsx claude-vscode
Desktop 集成 Claude Desktop main.tsx claude-desktop
MCP 服务器 claude mcp serve entrypoints/mcp.ts mcp

此外还有 github-actionlocal-agentremote 等特殊入口类型。这些入口共享同一套核心 Agent 运行时,但各有不同的初始化路径、安全边界和通信协议。

核心问题:如何在单一二进制中,让每种入口只加载必需的模块,同时保持架构的一致性和可维护性?

Claude Code 的答案是四层启动路由器 + 环境变量入口标记 + 统一初始化中枢。让我们从源码中逐层拆解。


一、L0-L3 四层启动路由器(entrypoints/cli.tsx

1.1 cli.tsx:真正的入口点

cli.tsx 是 Claude Code 的真正入口点——当用户在终端输入 claude 时,第一个执行的 TypeScript 文件就是它。原书将其称为"启动路由器(Bootstrap Router)",它实现了一个精巧的四层分流架构:

用户输入: $ claude [args]
         ↓
    cli.tsx (启动路由器)
         ↓
┌─────────────────────────────────────┐
│ L0: 环境预处理                        │
│   corepack 修复 / CCR 堆内存 / 消融基线  │
├─────────────────────────────────────┤
│ L1: 零依赖快速路径                     │
│   --version → 直接输出,不加载任何模块   │
├─────────────────────────────────────┤
│ L2: 功能分流(Feature Routes)          │
│   MCP / Bridge / Daemon / BG / ...    │
│   每个分支按需动态导入,互不干扰         │
├─────────────────────────────────────┤
│ L3: 完整 CLI 启动                     │
│   动态导入 main.tsx → cliMain()       │
└─────────────────────────────────────┘

1.2 L0:环境预处理(顶层副作用)

cli.tsx 的前 26 行是顶层副作用代码——在任何异步逻辑之前执行,零依赖、零成本:

// cli.tsx 第1-26行 —— L0 环境预处理

// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
process.env.COREPACK_ENABLE_AUTO_PIN = '0';

// Set max heap size for child processes in CCR environments (containers have 16GB)
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
  const existing = process.env.NODE_OPTIONS || '';
  process.env.NODE_OPTIONS = existing 
    ? `${existing} --max-old-space-size=8192` 
    : '--max-old-space-size=8192';
}

// Harness-science L0 ablation baseline
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
  for (const k of ['CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_THINKING', 
    'DISABLE_INTERLEAVED_THINKING', 'DISABLE_COMPACT', ...]) {
    process.env[k] ??= '1';
  }
}

关键设计:L0 的消融基线配置故意放在 cli.tsx 而非 init.ts 中,源码注释解释了原因:

“BashTool/AgentTool/PowerShellTool capture DISABLE_BACKGROUND_TASKS into module-level consts at import time — init() runs too late.”

这意味着某些模块在导入时就会读取环境变量到模块级常量。如果等 init() 执行时再设置,已经来不及了——模块已经用默认值完成了初始化。这是一个值得注意的时序陷阱

1.3 L1:零依赖快速路径

// cli.tsx 第36-42行 —— L1 零依赖快速路径
async function main(): Promise<void> {
  const args = process.argv.slice(2);
  
  // Fast-path for --version/-v: zero module loading needed
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }
  // ...继续到 L2/L3
}

--version 是最极端的快速路径:零模块加载。版本号通过 MACRO.VERSION 在构建时内联,不需要读取 package.json,不需要加载任何模块。原书提到预期耗时约 5 毫秒——这在源码中得到了完美验证。

1.4 L2:功能分流——10 条 Feature Route

L2 是 cli.tsx 的核心——根据命令行参数将请求路由到不同的功能模块。每条路由都通过 await import() 动态加载,互不干扰。源码中包含以下 10 条快速路径:

路由 触发参数 加载的模块 Feature Flag
dump-system-prompt --dump-system-prompt constants/prompts.js DUMP_SYSTEM_PROMPT(内部)
Chrome MCP --claude-in-chrome-mcp claudeInChrome/mcpServer.js 无(始终可用)
Chrome Native Host --chrome-native-host claudeInChrome/chromeNativeHost.js
Computer Use MCP --computer-use-mcp computerUse/mcpServer.js CHICAGO_MCP
Daemon Worker --daemon-worker daemon/workerRegistry.js DAEMON
Bridge/Remote Control remote-control/rc/remote/sync/bridge bridge/bridgeMain.js BRIDGE_MODE
Daemon daemon daemon/main.js DAEMON
Background Sessions ps/logs/attach/kill/--bg cli/bg.js BG_SESSIONS
Template Jobs new/list/reply cli/handlers/templateJobs.js TEMPLATES
Environment Runner environment-runner environment-runner/main.js BYOC_ENVIRONMENT_RUNNER
Self-Hosted Runner self-hosted-runner self-hosted-runner/main.js SELF_HOSTED_RUNNER

Bridge 路由为例,它展示了一条完整的 L2 快速路径:

// cli.tsx 第112-162行 —— Bridge/Remote Control 路由
if (feature('BRIDGE_MODE') && (args[0] === 'remote-control' || args[0] === 'rc' || 
    args[0] === 'remote' || args[0] === 'sync' || args[0] === 'bridge')) {
  profileCheckpoint('cli_bridge_path');
  
  // 1. 启用配置系统
  const { enableConfigs } = await import('../utils/config.js');
  enableConfigs();
  
  // 2. 验证 OAuth 认证(必须在 GrowthBook 门控之前)
  const { getClaudeAIOAuthTokens } = await import('../utils/auth.js');
  if (!getClaudeAIOAuthTokens()?.accessToken) {
    exitWithError(BRIDGE_LOGIN_ERROR);
  }
  
  // 3. 检查 GrowthBook 功能门控
  const disabledReason = await getBridgeDisabledReason();
  if (disabledReason) exitWithError(`Error: ${disabledReason}`);
  
  // 4. 检查版本兼容性
  const versionError = checkBridgeMinVersion();
  if (versionError) exitWithError(versionError);
  
  // 5. 检查策略限制
  await waitForPolicyLimitsToLoad();
  if (!isPolicyAllowed('allow_remote_control')) {
    exitWithError("Error: Remote Control is disabled by your organization's policy.");
  }
  
  // 6. 启动 Bridge 主循环
  await bridgeMain(args.slice(1));
  return;
}

注意时序:OAuth 认证必须在 GrowthBook 门控之前——源码注释解释了原因:

“Auth check must come before the GrowthBook gate check — without auth, GrowthBook has no user context and would return a stale/default false.”

1.5 L3:完整 CLI 启动

当所有快速路径都不匹配时,cli.tsx 执行 L3——动态导入 main.tsx

// cli.tsx 第287-299行 —— L3 完整 CLI 启动
// No special flags detected, load and run the full CLI
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js');
startCapturingEarlyInput();
profileCheckpoint('cli_before_main_import');
const { main: cliMain } = await import('../main.js');
profileCheckpoint('cli_after_main_import');
await cliMain();
profileCheckpoint('cli_after_main_complete');

值得注意的是 startCapturingEarlyInput() ——在加载 main.tsx 之前就开始捕获输入。这是因为 main.tsx 的加载需要 100-200ms,在此期间用户的键击不应丢失。原书称此为"渐进式启动"的一部分。

1.6 构建时 Feature Flag:死代码消除

原书第 2.2.3 节讨论的构建时 Feature Flag 在源码中大量使用。feature() 函数来自 bun:bundle,在构建时被内联为布尔常量:

import { feature } from 'bun:bundle';

// 内部构建:feature('DAEMON') → true,代码保留
// 外部构建:feature('DAEMON') → false,编译器完全消除此代码块
if (feature('DAEMON') && args[0] === 'daemon') {
  const { daemonMain } = await import('../daemon/main.js');
  await daemonMain(args.slice(1));
  return;
}

Trade-off 分析(原书表格的源码验证):

维度 优势 代价
二进制大小 外部构建不包含内部功能代码
运行时性能 零开销,编译器完全消除
可维护性 同一份源码在不同构建中行为不同,测试矩阵变大
心智模型 Feature Flag(代码是否存在)与环境变量(运行时行为)混用

二、入口点检测:initializeEntrypoint()CLAUDE_CODE_ENTRYPOINT

2.1 入口点标记机制

当 L3 路径将控制权交给 main.tsx 后,main.tsx 的第一个关键操作是标记入口点类型。这通过 initializeEntrypoint() 函数完成:

// main.tsx 第517-540行 —— 入口点初始化
function initializeEntrypoint(isNonInteractive: boolean): void {
  // Skip if already set (e.g., by SDK or other entrypoints)
  if (process.env.CLAUDE_CODE_ENTRYPOINT) {
    return;  // ← SDK/IDE 等外部调用者可能已预设此值
  }
  const cliArgs = process.argv.slice(2);

  // Check for MCP serve command
  const mcpIndex = cliArgs.indexOf('mcp');
  if (mcpIndex !== -1 && cliArgs[mcpIndex + 1] === 'serve') {
    process.env.CLAUDE_CODE_ENTRYPOINT = 'mcp';
    return;
  }
  
  // GitHub Actions 环境
  if (isEnvTruthy(process.env.CLAUDE_CODE_ACTION)) {
    process.env.CLAUDE_CODE_ENTRYPOINT = 'claude-code-github-action';
    return;
  }

  // local-agent 由启动器预设环境变量(通过上面的 early return 处理)
  
  // 根据 TTY 状态判断 CLI vs SDK-CLI
  process.env.CLAUDE_CODE_ENTRYPOINT = isNonInteractive ? 'sdk-cli' : 'cli';
}

关键设计:函数首先检查 CLAUDE_CODE_ENTRYPOINT 是否已被预设。这是一个优先级机制——SDK 和 IDE 等外部调用者可以在启动 CLI 进程时通过环境变量预设入口类型,从而绕过自动检测。

2.2 七种客户端类型

入口点确定后,main.tsx 将其映射为客户端类型(clientType),这个值会影响后续的认证策略、遥测标记和 UI 适配:

// main.tsx 第818-833行 —— 客户端类型检测
const clientType = (() => {
  // 1. GitHub Actions 环境
  if (isEnvTruthy(process.env.GITHUB_ACTIONS)) return 'github-action';
  
  // 2. SDK 模式(TypeScript / Python / CLI)
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-ts') return 'sdk-typescript';
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-py') return 'sdk-python';
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-cli') return 'sdk-cli';
  
  // 3. IDE 集成
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-vscode') return 'claude-vscode';
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'local-agent') return 'local-agent';
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop') return 'claude-desktop';
  
  // 4. 远程会话
  const hasSessionIngressToken = 
    process.env.CLAUDE_CODE_SESSION_ACCESS_TOKEN || 
    process.env.CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR;
  if (process.env.CLAUDE_CODE_ENTRYPOINT === 'remote' || hasSessionIngressToken) {
    return 'remote';
  }
  
  // 5. 默认:标准 CLI
  return 'cli';
})();
setClientType(clientType);

入口类型与客户端类型的映射关系

CLAUDE_CODE_ENTRYPOINT    →  clientType
─────────────────────────────────────────
(undefined + TTY)         →  cli          (交互式 REPL)
(undefined + no TTY)      →  sdk-cli      (管道模式)
sdk-ts                    →  sdk-typescript
sdk-py                    →  sdk-python
claude-vscode             →  claude-vscode
local-agent               →  local-agent
claude-desktop            →  claude-desktop
mcp                       →  (不走 main.tsx,直接到 mcp.ts)
(remote token present)    →  remote

2.3 客户端类型的影响

客户端类型不只是标签——它直接影响运行时行为。例如,state.ts 中有一处关键的非交互判断:

// bootstrap/state.ts 第1236行
return getIsNonInteractiveSession() && STATE.clientType !== 'claude-vscode'

这意味着 VSCode 扩展即使运行在非交互模式下,也会被排除在某些非交互行为之外——因为 VSCode 有自己的 UI 来展示配置错误,不需要 CLI 的 stderr 输出。

同样,main.tsx 中对 --question-preview-format 的设置也依赖于客户端类型:

// main.tsx 第835-843行
if (!clientType.startsWith('sdk-') &&
    clientType !== 'claude-desktop' && 
    clientType !== 'local-agent' && 
    clientType !== 'remote') {
  setQuestionPreviewFormat('markdown');
}

SDK 模式和桌面应用通过自己的机制传递预览格式,不会被默认值覆盖。


三、SDK 入口:agentSdkTypes.ts 与控制协议

3.1 SDK 公共 API 表面

entrypoints/agentSdkTypes.ts 是 Agent SDK 的公共 API 入口点。它定义了外部开发者可以调用的所有函数和类型:

// agentSdkTypes.ts —— SDK 公共 API

// V1 API: 单次查询
export function query(_params: {
  prompt: string | AsyncIterable<SDKUserMessage>
  options?: Options
}): Query

// V2 API (alpha): 持久会话
export function unstable_v2_createSession(_options: SDKSessionOptions): SDKSession
export function unstable_v2_resumeSession(_sessionId: string, _options: SDKSessionOptions): SDKSession
export async function unstable_v2_prompt(_message: string, _options: SDKSessionOptions): Promise<SDKResultMessage>

// 会话管理
export async function getSessionMessages(_sessionId: string, _options?): Promise<SessionMessage[]>
export async function listSessions(_options?): Promise<SDKSessionInfo[]>
export async function getSessionInfo(_sessionId: string, _options?): Promise<SDKSessionInfo | undefined>
export async function renameSession(_sessionId: string, _title: string, _options?): Promise<void>
export async function tagSession(_sessionId: string, _tag: string | null, _options?): Promise<void>
export async function forkSession(_sessionId: string, _options?): Promise<ForkSessionResult>

// 工具定义
export function tool<Schema extends AnyZodRawShape>(
  _name: string, _description: string, _inputSchema: Schema,
  _handler: (args, extra) => Promise<CallToolResult>,
  _extras?: { annotations?: ToolAnnotations; searchHint?: string; alwaysLoad?: boolean }
): SdkMcpToolDefinition<Schema>

// MCP 服务器创建
export function createSdkMcpServer(_options: CreateSdkMcpServerOptions): McpSdkServerConfigWithInstance

注意:所有函数体都是 throw new Error('not implemented')。这不是 bug——这是 SDK 的接口外壳模式。实际实现在编译时被替换为真正的实现代码。SourceMap 还原版本丢失了实际实现,但接口定义完整保留。

3.2 SDK 的双层 API 架构

SDK 提供了两代 API:

V1 API(稳定)

  • query() —— 单次查询,执行后退出
  • 适合 CI/CD 管道、脚本自动化

V2 API(alpha)

  • unstable_v2_createSession() —— 创建持久会话
  • unstable_v2_resumeSession() —— 恢复会话
  • unstable_v2_prompt() —— 一次性便捷查询
  • 适合多轮对话、交互式应用

V2 API 的 unstable_ 前缀表明其尚未稳定,但已提供了完整的会话管理能力。

3.3 SDK 类型体系

agentSdkTypes.ts 重新导出了三个层级的类型:

// 1. 核心类型(可序列化)—— messages, configs
export * from './sdk/coreTypes.js'

// 2. 运行时类型(不可序列化)—— callbacks, interfaces
export * from './sdk/runtimeTypes.js'

// 3. 控制协议类型(SDK builders 用)
export type { SDKControlRequest, SDKControlResponse } from './sdk/controlTypes.js'

// 4. 设置类型(从 JSON Schema 生成)
export type { Settings } from './sdk/settingsTypes.generated.js'

// 5. 工具类型(@internal)
export * from './sdk/toolTypes.js'

coreTypes.ts 中定义了两个关键的常量数组,它们是整个系统的运行时契约:

// coreTypes.ts —— Hook 事件清单
export const HOOK_EVENTS = [
  'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
  'Notification', 'UserPromptSubmit',
  'SessionStart', 'SessionEnd',
  'Stop', 'StopFailure',
  'SubagentStart', 'SubagentStop',
  'PreCompact', 'PostCompact',
  'PermissionRequest', 'PermissionDenied',
  'Setup', 'TeammateIdle',
  'TaskCreated', 'TaskCompleted',
  'Elicitation', 'ElicitationResult',
  'ConfigChange',
  'WorktreeCreate', 'WorktreeRemove',
  'InstructionsLoaded', 'CwdChanged', 'FileChanged',
] as const  // 27 个 Hook 事件

export const EXIT_REASONS = [
  'clear', 'resume', 'logout',
  'prompt_input_exit', 'other',
  'bypass_permissions_disabled',
] as const

3.4 SDK 控制协议(sdk/controlSchemas.ts

SDK 与 CLI 进程之间通过控制协议通信。controlSchemas.ts 定义了 Zod schema 来验证所有控制请求和响应:

// controlSchemas.ts —— 控制协议请求类型

// 1. 初始化请求:配置 hooks、MCP 服务器、agent 定义
SDKControlInitializeRequestSchema: {
  subtype: 'initialize',
  hooks?: Record<HookEvent, SDKHookCallbackMatcher[]>,
  sdkMcpServers?: string[],
  systemPrompt?: string,
  appendSystemPrompt?: string,
  agents?: Record<string, AgentDefinition>,
  promptSuggestions?: boolean,
  agentProgressSummaries?: boolean,
}

// 2. 中断请求:中断当前对话轮
SDKControlInterruptRequestSchema: {
  subtype: 'interrupt',
}

// 3. 权限请求:请求使用工具的权限
SDKControlPermissionRequestSchema: {
  subtype: 'can_use_tool',
  tool_name: string,
  input: Record<string, unknown>,
  permission_suggestions?: PermissionUpdate[],
  tool_use_id: string,
}

// 4. 设置权限模式
SDKControlSetPermissionModeRequestSchema: {
  subtype: 'set_permission_mode',
  mode: PermissionMode,
}

// 5. 设置模型
SDKControlSetModelRequestSchema: {
  subtype: 'set_model',
  model?: string,
}

// 6. MCP 状态查询
SDKControlMcpStatusRequestSchema: {
  subtype: 'mcp_status',
}

// 7. 上下文使用量查询
SDKControlGetContextUsageRequestSchema: {
  subtype: 'get_context_usage',
}

这个控制协议是 SDK 集成的核心——SDK 进程通过 stdin/stdout 发送和接收 JSON-RPC 消息,控制 CLI 子进程的行为。

3.5 SDK 的后台任务原语

agentSdkTypes.ts 还导出了助手守护进程原语(Assistant Daemon Primitives),这些是 @internal 标记的,供 Claude.ai 远程控制使用:

// Cron 任务定义
export type CronTask = {
  id: string
  cron: string
  prompt: string
  createdAt: number
  recurring?: boolean
}

// 监听定时任务
export function watchScheduledTasks(_opts: {
  dir: string
  signal: AbortSignal
  getJitterConfig?: () => CronJitterConfig
}): ScheduledTasksHandle

// 连接远程控制
export async function connectRemoteControl(_opts: ConnectRemoteControlOptions): Promise<RemoteControlHandle | null>

connectRemoteControl 是一个特别重要的函数——它允许守护进程持有 WebSocket 连接,即使 Agent 子进程崩溃也能保持会话不中断。源码注释解释了这一设计:

“The daemon owns the WebSocket in the PARENT process — if the agent subprocess (spawned via query()) crashes, the daemon respawns it while claude.ai keeps the same session.”


四、MCP 服务器入口:entrypoints/mcp.ts

4.1 MCP 服务器模式

当用户执行 claude mcp serve 时,Claude Code 作为一个 MCP 协议服务器运行。mcp.ts 是这个模式的完整入口:

// mcp.ts —— MCP 服务器启动
export async function startMCPServer(
  cwd: string, debug: boolean, verbose: boolean
): Promise<void> {
  setCwd(cwd);
  
  // 创建 MCP Server 实例
  const server = new Server(
    { name: 'claude/tengu', version: MACRO.VERSION },
    { capabilities: { tools: {} } }
  );

  // 注册 ListTools 请求处理器
  server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
    const toolPermissionContext = getEmptyToolPermissionContext()
    const tools = getTools(toolPermissionContext)
    return {
      tools: await Promise.all(tools.map(async tool => ({
        ...tool,
        description: await tool.prompt({...}),
        inputSchema: zodToJsonSchema(tool.inputSchema),
        outputSchema,
      }))),
    }
  });

  // 注册 CallTool 请求处理器
  server.setRequestHandler(CallToolRequestSchema, 
    async ({ params: { name, arguments: args } }): Promise<CallToolResult> => {
      const tool = findToolByName(tools, name)
      // ... 权限检查、输入验证、工具执行
    }
  );

  // 使用 StdioServerTransport 启动
  const transport = new StdioServerTransport()
  await server.connect(transport)
}

4.2 MCP 服务器的设计特点

精简的 ToolUseContext:MCP 服务器模式创建了一个最小化的 ToolUseContext,许多字段被设为空值或空函数:

const toolUseContext: ToolUseContext = {
  abortController: createAbortController(),
  options: {
    commands: MCP_COMMANDS,           // 仅包含 review 命令
    tools,
    mainLoopModel: getMainLoopModel(),
    thinkingConfig: { type: 'disabled' },  // 禁用 thinking
    mcpClients: [],                   // MCP 服务器本身不嵌套 MCP
    mcpResources: {},
    isNonInteractiveSession: true,    // 非交互式
    agentDefinitions: { activeAgents: [], allAgents: [] },  // 无子代理
  },
  getAppState: () => getDefaultAppState(),  // 默认状态
  setAppState: () => {},              // 状态设置是空操作
  messages: [],                       // 无消息历史
  // ...
}

LRU 文件状态缓存:为防止内存无限增长,MCP 服务器使用有大小限制的 LRU 缓存:

const READ_FILE_STATE_CACHE_SIZE = 100  // 100 个文件
const readFileStateCache = createFileStateCacheWithSizeLimit(
  READ_FILE_STATE_CACHE_SIZE,
)

MCP 服务器是"无状态"的——每次工具调用都创建新的 toolPermissionContexttoolUseContext,不保留跨调用的状态。这使得 MCP 服务器天然支持并发调用。


五、沙箱类型:entrypoints/sandboxTypes.ts

sandboxTypes.ts 是沙箱配置的唯一真相来源,被 SDK 和设置验证共同使用。它定义了三个层级的 Zod schema:

5.1 网络配置

SandboxNetworkConfigSchema = z.object({
  allowedDomains: z.array(z.string()).optional(),
  allowManagedDomainsOnly: z.boolean().optional()
    .describe('When true, only allowedDomains from managed settings are respected'),
  allowUnixSockets: z.array(z.string()).optional()
    .describe('macOS only: Unix socket paths to allow'),
  allowAllUnixSockets: z.boolean().optional(),
  allowLocalBinding: z.boolean().optional(),
  httpProxyPort: z.number().optional(),
  socksProxyPort: z.number().optional(),
})

5.2 文件系统配置

SandboxFilesystemConfigSchema = z.object({
  allowWrite: z.array(z.string()).optional(),
  denyWrite: z.array(z.string()).optional(),
  denyRead: z.array(z.string()).optional(),
  allowRead: z.array(z.string()).optional()
    .describe('Paths to re-allow reading within denyRead regions'),
  allowManagedReadPathsOnly: z.boolean().optional()
    .describe('When true, only allowRead paths from policySettings are used'),
})

5.3 沙箱设置

SandboxSettingsSchema = z.object({
  enabled: z.boolean().optional(),
  failIfUnavailable: z.boolean().optional()
    .describe('Exit with error at startup if sandbox cannot start'),
  autoAllowBashIfSandboxed: z.boolean().optional(),
  allowUnsandboxedCommands: z.boolean().optional()
    .describe('Default: true'),
  network: SandboxNetworkConfigSchema(),
  filesystem: SandboxFilesystemConfigSchema(),
  ignoreViolations: z.record(z.string(), z.array(z.string())).optional(),
  enableWeakerNestedSandbox: z.boolean().optional(),
  enableWeakerNetworkIsolation: z.boolean().optional()
    .describe('macOS only: Allow access to com.apple.trustd.agent'),
  excludedCommands: z.array(z.string()).optional(),
  ripgrep: z.object({ command: z.string(), args: z.array(z.string()).optional() }).optional(),
}).passthrough()

关键设计failIfUnavailable 允许企业部署将沙箱作为硬性门控——如果沙箱无法启动,整个进程直接退出。enableWeakerNetworkIsolation.describe() 注释明确标注了安全风险——“Reduces security — opens a potential data exfiltration vector through the trustd service”。这种在 schema 中嵌入安全文档的做法值得学习。


六、初始化中枢:entrypoints/init.ts 的多入口适配

init.ts 是所有入口共享的初始化中枢。原书第 2.4 节详细讨论了它的 memoize 单例模式和信任分层设计。这里我们关注它如何适配不同入口。

6.1 memoize 单例:跨入口的幂等保证

// init.ts 第57行
export const init = memoize(async (): Promise<void> => {
  // ...完整初始化流程
})

memoize 确保 init() 无论被调用多少次(从 CLI、SDK、IDE 等不同入口路径),都只执行一次。这对于有副作用的初始化(配置文件写入、网络连接、遥测启动)至关重要。

6.2 信任分层的入口适配

init.ts 的初始化分为信任前信任后两个阶段。不同入口在这两个阶段的行为不同:

信任前阶段(所有入口共享):

// 1. 配置验证
enableConfigs()
// 2. 安全环境变量(仅来自全局配置)
applySafeConfigEnvironmentVariables()
// 3. CA 证书(在任何 TLS 连接之前)
applyExtraCACertsFromConfig()
// 4. 优雅关闭注册
setupGracefulShutdown()

信任前阶段的 fire-and-forget 异步(不阻塞主路径):

// 1P 事件日志(fire-and-forget)
void Promise.all([
  import('../services/analytics/firstPartyEventLogger.js'),
  import('../services/analytics/growthbook.js'),
]).then(([fp, gb]) => {
  fp.initialize1PEventLogging()
  gb.onGrowthBookRefresh(() => {
    void fp.reinitialize1PEventLoggingIfConfigChanged()
  })
})

// OAuth 账户信息填充(fire-and-forget)
void populateOAuthAccountInfoIfNeeded()

// JetBrains IDE 检测(fire-and-forget,结果缓存供后续同步访问)
void initJetBrainsDetection()

// 仓库检测(fire-and-forget,用于 gitDiff PR 链接)
void detectCurrentRepository()

6.3 非交互式入口的错误处理

init.ts 的错误处理区分了交互式和非交互式入口:

} catch (error) {
  if (error instanceof ConfigParseError) {
    // 非交互式入口:直接输出到 stderr 并退出
    if (getIsNonInteractiveSession()) {
      process.stderr.write(
        `Configuration error in ${error.filePath}: ${error.message}\n`
      )
      gracefulShutdownSync(1)
      return
    }
    
    // 交互式入口:显示配置错误对话框
    return import('../components/InvalidConfigDialog.js').then(m =>
      m.showInvalidConfigDialog({ error })
    )
  } else {
    throw error
  }
}

注意getIsNonInteractiveSession() 的实现排除了 claude-vscode——VSCode 扩展即使在非交互模式下也会显示对话框,因为它有自己的 UI 来展示错误。

6.4 遥测初始化的入口适配

遥测初始化被设计为信任后操作,由 initializeTelemetryAfterTrust() 单独触发:

export function initializeTelemetryAfterTrust(): void {
  if (isEligibleForRemoteManagedSettings()) {
    // 有远程设置资格的入口:等待远程设置加载后再初始化遥测
    // 例外:非交互式入口 + beta tracing → 立即初始化
    if (getIsNonInteractiveSession() && isBetaTracingEnabled()) {
      void doInitializeTelemetry().catch(...)
    }
    void waitForRemoteManagedSettingsToLoad()
      .then(async () => {
        applyConfigEnvironmentVariables()
        await doInitializeTelemetry()
      })
      .catch(...)
  } else {
    // 无远程设置资格的入口:立即初始化
    void doInitializeTelemetry().catch(...)
  }
}

七、SDK 模式的特殊路径

7.1 SDK URL 自动配置

当 SDK 调用 CLI 时,会传递 --sdk-url 参数。main.tsx 检测到此参数后会自动配置多个选项:

// main.tsx 第1235-1252行
if (sdkUrl) {
  // 自动使用 stream-json 格式
  if (!inputFormat) inputFormat = 'stream-json';
  if (!outputFormat) outputFormat = 'stream-json';
  
  // 自动启用 verbose 模式
  if (options.verbose === undefined) verbose = true;
  
  // 自动启用 print 模式
  if (!options.print) print = true;
}

这意味着 SDK 调用者不需要手动设置这些参数——--sdk-url 隐含了非交互式 + 流式 JSON 的语义。

7.2 Hook 事件的 SDK 控制

// main.tsx 第1228-1233行
if (includeHookEvents || isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) {
  setAllHookEventsEnabled(true);
}

默认情况下,只有 SessionStartSetup 事件会被发出。SDK 可以通过 includeHookEvents 选项启用全部 27 个 Hook 事件。CCR(Claude Code Remote)环境也会自动启用全部事件。


八、入口点的内存清理注册

init.ts 中注册了多种清理函数,确保进程退出时资源被正确释放:

// LSP 服务器清理
registerCleanup(shutdownLspServerManager)

// Swarm 团队清理(防止子代理创建的团队泄漏)
registerCleanup(async () => {
  const { cleanupSessionTeams } = await import('../utils/swarm/teamHelpers.js')
  await cleanupSessionTeams()
})

// 暂存目录确保
if (isScratchpadEnabled()) {
  await ensureScratchpadDir()
}

这些清理函数对所有入口生效,确保无论是 CLI、SDK 还是 IDE,进程退出时都不会留下垃圾资源。


九、设计模式提炼

模式 1:四层启动路由器(L0-L3 Layered Bootstrap Router)

问题:一个二进制支持十余种运行模式,但用户每次只使用一种,如何避免不必要的模块加载?

方案:将启动流程分为四层——L0 环境预处理(零依赖副作用)、L1 零依赖快速路径(--version)、L2 功能分流(10+ 条 Feature Route,每条动态导入)、L3 完整 CLI 启动(加载 main.tsx)。

实现要点

  • 每层通过 return 短路,不继续到下一层
  • L2 每条路由通过 await import() 动态加载
  • Feature Flag(feature())在构建时消除不需要的代码
  • profileCheckpoint() 标记每条路径的执行时间

Trade-off:手动路由牺牲了 IDE 跳转能力和代码可读性,换取每个分支独立定制前置逻辑的灵活性。

模式 2:环境变量入口标记(Entrypoint Environment Variable Tagging)

问题:不同入口(CLI/SDK/IDE/MCP/Remote)需要不同的初始化行为,但共享同一套初始化代码。

方案:通过 CLAUDE_CODE_ENTRYPOINT 环境变量标记入口类型。外部调用者(SDK、IDE)可以在启动 CLI 进程前预设此值;CLI 自身通过 initializeEntrypoint() 在自动检测时设置。

实现要点

  • initializeEntrypoint() 首先检查是否已被预设(if (process.env.CLAUDE_CODE_ENTRYPOINT) return
  • 自动检测的优先级:MCP serve > GitHub Actions > TTY 状态
  • 入口类型映射为客户端类型,影响认证、遥测和 UI 适配

模式 3:接口外壳模式(Interface Shell Pattern)

问题:SDK 的公共 API 需要稳定的接口定义,但实际实现在编译时注入。

方案agentSdkTypes.ts 导出所有公共函数,但函数体都是 throw new Error('not implemented')。实际实现在构建时被替换。

优势

  • 接口定义与实现分离,便于版本管理
  • SourceMap 还原版本可以保留完整的接口定义
  • 开发者可以只看类型文件就理解 SDK 的全部能力

模式 4:控制协议分层(Control Protocol Layering)

问题:SDK 进程需要控制 CLI 子进程的行为(中断、设置模型、查询权限),但两者运行在不同进程中。

方案:通过 JSON-RPC 控制协议通信。sdk/controlSchemas.ts 使用 Zod schema 定义所有控制请求和响应,确保类型安全。

控制请求类型

  • initialize —— 配置 hooks、MCP 服务器、agent 定义
  • interrupt —— 中断当前对话
  • can_use_tool —— 请求工具权限
  • set_permission_mode —— 设置权限模式
  • set_model —— 切换模型
  • mcp_status —— 查询 MCP 状态
  • get_context_usage —— 查询上下文使用量

模式 5:构建时 Feature Flag 死代码消除

问题:存在多个构建变体(内部/外部),如何在不增加运行时开销的情况下控制功能集?

方案:使用 bun:bundlefeature() 函数,在构建时将 Feature Flag 内联为布尔常量。false 分支的代码被编译器完全消除,不占用二进制空间。

适用条件

  • 多版本构建场景(社区版/企业版、内部版/外部版)
  • 功能开关决定代码是否存在(而非运行时行为)
  • 需要零运行时开销

Trade-off:同一份源码在不同构建中行为不同,测试矩阵变大。


十、验证清单

以下是基于源码分析对原书结论的验证:

  • “四层启动路由器”cli.tsx 确实实现了 L0-L3 四层分流,每层通过 return 短路
  • --version 零模块加载”:L1 路径通过 MACRO.VERSION 构建时内联,不加载任何模块
  • “L2 每个分支独立动态导入”:10+ 条 Feature Route 全部使用 await import()
  • “构建时 Feature Flag”feature() 函数在 7 条路由中使用,内部构建变体代码被消除
  • “memoize 单例初始化”init.ts 使用 lodash-es/memoize 包装,保证幂等性
  • “信任分层”applySafeConfigEnvironmentVariables() 在信任前,applyConfigEnvironmentVariables() 在信任后
  • “fire-and-forget 并行初始化”:OAuth 填充、IDE 检测、仓库检测使用 void fire-and-forget
  • “API 预连接”preconnectAnthropicApi() 在网络配置完成后、用户输入前调用
  • “DAG 叶子约束”bootstrap/state.tsagentSdkTypes.ts 直接导入(import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'),保持类型依赖
  • “SDK 双层 API”:V1 query() + V2 unstable_v2_createSession()/unstable_v2_prompt()

十一、入口架构全景图

                         ┌─────────────┐
                         │  cli.tsx    │  ← 真正的入口二进制
                         │ (L0-L3 路由) │
                         └──────┬──────┘
                                │
           ┌────────────────────┼────────────────────┐
           │                    │                    │
    L1 快速路径            L2 功能分流           L3 完整CLI
    --version              ┌────┴────┐          ┌────┴────┐
    (零模块)               │         │          │         │
                     MCP Server  Bridge    main.tsx   Daemon
                     (mcp.ts)  (bridgeMain)  │      (daemonMain)
                                              │
                                    ┌─────────┴─────────┐
                                    │  initializeEntrypoint()
                                    │  标记 CLAUDE_CODE_ENTRYPOINT
                                    └─────────┬─────────┘
                                              │
                    ┌────────────┬────────────┼────────────┬────────────┐
                    │            │            │            │            │
                 cli          sdk-cli      sdk-ts/py   claude-vscode  remote
                 (交互式)     (管道模式)    (SDK调用)    (IDE集成)     (远程)
                    │            │            │            │            │
                    └────────────┴────────────┴────────────┴────────────┘
                                              │
                                     ┌────────┴────────┐
                                     │   init.ts       │  ← 共享初始化
                                     │ (memoize 单例)   │
                                     │ 信任前 → 信任后  │
                                     └────────┬────────┘
                                              │
                                     ┌────────┴────────┐
                                     │ bootstrap/       │  ← 全局状态锚点
                                     │ state.ts         │
                                     │ (DAG 叶子节点)   │
                                     └─────────────────┘

十二、总结:多入口架构的设计哲学

Claude Code 的多入口架构体现了三个层次的工程智慧:

  1. 分层路由的按需加载:L0-L3 四层设计将"加载开销与代码总量成正比"变成了"与所用模式代码量成正比"。这是渐进式启动的核心——每次只加载必需的模块,从 5ms 的 --version 到 200ms 的完整 REPL,性能开销与功能需求精确匹配。

  2. 环境变量入口标记:通过 CLAUDE_CODE_ENTRYPOINT 环境变量,外部调用者(SDK、IDE)可以声明自己的身份,CLI 内部也能自动检测。这种软标记 + 硬检测的双重机制既保证了灵活性(外部可预设),又保证了安全性(内部自动检测作为后备)。

  3. 控制协议的进程间通信:SDK 通过 JSON-RPC 控制协议与 CLI 子进程通信,Zod schema 确保类型安全。这种设计使得 SDK 可以在任何编程语言中实现——Python SDK 只需要发送和接收 JSON-RPC 消息,不需要理解 TypeScript 内部结构。

原书将这些设计概括为"入口路由器"设计模式。从源码来看,这个模式的关键不在于路由本身(任何 if/else 都能做路由),而在于每个路由分支的前置条件独立定制——MCP 需要策略检查,Bridge 需要 OAuth 验证,Daemon 需要配置系统,BG 需要会话管理——这些差异使得声明式的命令注册无法胜任,手动路由成为必然选择。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐