2.6 多入口架构:CLI / SDK / IDE / MCP 的统一路由
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.tsx → main.tsx |
cli |
| SDK CLI 模式 | claude -p "query" |
cli.tsx → main.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-action、local-agent、remote 等特殊入口类型。这些入口共享同一套核心 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 服务器是"无状态"的——每次工具调用都创建新的 toolPermissionContext 和 toolUseContext,不保留跨调用的状态。这使得 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);
}
默认情况下,只有 SessionStart 和 Setup 事件会被发出。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:bundle 的 feature() 函数,在构建时将 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 检测、仓库检测使用
voidfire-and-forget - “API 预连接”:
preconnectAnthropicApi()在网络配置完成后、用户输入前调用 - “DAG 叶子约束”:
bootstrap/state.ts被agentSdkTypes.ts直接导入(import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'),保持类型依赖 - “SDK 双层 API”:V1
query()+ V2unstable_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 的多入口架构体现了三个层次的工程智慧:
-
分层路由的按需加载:L0-L3 四层设计将"加载开销与代码总量成正比"变成了"与所用模式代码量成正比"。这是渐进式启动的核心——每次只加载必需的模块,从 5ms 的
--version到 200ms 的完整 REPL,性能开销与功能需求精确匹配。 -
环境变量入口标记:通过
CLAUDE_CODE_ENTRYPOINT环境变量,外部调用者(SDK、IDE)可以声明自己的身份,CLI 内部也能自动检测。这种软标记 + 硬检测的双重机制既保证了灵活性(外部可预设),又保证了安全性(内部自动检测作为后备)。 -
控制协议的进程间通信:SDK 通过 JSON-RPC 控制协议与 CLI 子进程通信,Zod schema 确保类型安全。这种设计使得 SDK 可以在任何编程语言中实现——Python SDK 只需要发送和接收 JSON-RPC 消息,不需要理解 TypeScript 内部结构。
原书将这些设计概括为"入口路由器"设计模式。从源码来看,这个模式的关键不在于路由本身(任何 if/else 都能做路由),而在于每个路由分支的前置条件独立定制——MCP 需要策略检查,Bridge 需要 OAuth 验证,Daemon 需要配置系统,BG 需要会话管理——这些差异使得声明式的命令注册无法胜任,手动路由成为必然选择。
更多推荐



所有评论(0)