第二章:启动流程——从敲下命令到界面就绪
当用户在终端敲下
claude并回车,到看到交互式提示符,中间经历了什么?
本章拆解 Claude Code 的完整启动链路,揭示其中蕴含的性能优化和工程设计思想。
目录
- 2.1 启动全景:8 个阶段
- 2.2 Phase 0:快速路径分发(cli.tsx)
- 2.3 Phase 1:模块加载与并行预取(main.tsx 副作用)
- 2.4 Phase 2:主函数与交互模式检测
- 2.5 Phase 3:Commander 解析与 preAction 钩子
- 2.6 Phase 4:核心初始化(init.ts)
- 2.7 Phase 5:信任边界与安全屏障
- 2.8 Phase 6:状态树构建与工具注册
- 2.9 Phase 7:React 组件树挂载与 REPL 启动
- 2.10 非交互模式:Headless 分支
- 2.11 配置系统:多源合并的层级模型
- 2.12 CLAUDE.md 记忆系统的加载
- 2.13 关键工程思想总结
2.1 启动全景:8 个阶段
整个启动过程可以划分为 8 个阶段。下图展示了从进程创建到用户可交互的完整链路:
--version / --mcp / bridge"} P0Fast["零依赖返回
毫秒级响应"] end subgraph Phase1["Phase 1: 模块加载 + 并行预取"] P1A["startMdmRawRead()
MDM 配置子进程"] P1B["startKeychainPrefetch()
密钥读取子进程"] P1C["~135ms 静态 import
加载 200+ 模块"] end subgraph Phase2["Phase 2: 主函数入口"] P2A["argv 处理
协议链接/SSH/助手模式"] P2B["交互模式检测
TTY / -p / --init-only"] P2C["eagerLoadSettings()
提前加载设置"] end subgraph Phase3["Phase 3: Commander 解析"] P3A["Commander.js 定义
30+ 个 CLI 选项"] P3B["preAction 钩子
等待 MDM + Keychain"] end subgraph Phase4["Phase 4: 核心初始化"] P4A["enableConfigs()
配置系统激活"] P4B["mTLS + Proxy
网络层配置"] P4C["preconnectAnthropicApi()
TCP+TLS 预连接"] end subgraph Phase5["Phase 5: 信任边界"] P5A["showSetupScreens()
首次引导 / 信任对话框"] P5B["initializeTelemetryAfterTrust()
信任后才启用遥测"] end subgraph Phase6["Phase 6: 状态与工具"] P6A["createStore(initialState)
构建 AppState"] P6B["getTools() + assembleToolPool()
注册 40+ 工具"] P6C["getClaudeCodeMcpConfigs()
MCP 服务器发现"] end subgraph Phase7["Phase 7: UI 挂载"] P7A["createRoot()
Ink 终端 React 根"] P7B["launchRepl()
App + REPL 组件"] P7C["startDeferredPrefetches()
延迟预取"] end Ready(("REPL 就绪
用户可以输入")) Start --> P0 P0 --> P0Check P0Check -- "是" --> P0Fast P0Check -- "否" --> Phase1 P1A ~~~ P1B P1B ~~~ P1C Phase1 --> Phase2 P2A --> P2B --> P2C Phase2 --> Phase3 P3A --> P3B Phase3 --> Phase4 P4A --> P4B --> P4C Phase4 --> Phase5 P5A --> P5B Phase5 --> Phase6 P6A --> P6B --> P6C Phase6 --> Phase7 P7A --> P7B --> P7C Phase7 --> Ready
2.2 Phase 0:快速路径分发(cli.tsx)
文件: entrypoints/cli.tsx(~300 行)
这是整个应用的第一行代码。它的核心职责是:以最低成本判断请求类型,把简单请求拦截掉,不加载任何重模块。
模块级副作用(第 1-31 行)
在 main() 函数执行之前,文件顶层就做了两件事:
1 | // 防止 corepack 自动 pin 版本 |
设计意图:环境变量必须在任何
import之前设置,否则被 import 的模块可能读到错误的值。这就是为什么它们放在文件最顶部,而不是放在main()函数里。
快速路径矩阵
直接 return
零 import"] Entry --> D{"daemon?"} D -- "是" --> Import1["import daemon/cli.js"] Entry --> B{"bridge?"} B -- "是" --> Import2["import bridge/cli.js"] Entry --> M{"--mcp?"} M -- "是" --> Import3["import mcp.js
startMCPServer()"] Entry --> T{"template?"} T -- "是" --> Import4["import template handler"] Entry --> Dump{"--dump-system-prompt?"} Dump -- "是" --> Import5["enableConfigs()
getSystemPrompt()
打印并退出"] Entry --> Normal["正常路径 →
import main.js"]
正常路径的关键操作
1 | // cli.tsx:288-298 —— 正常路径 |
startCapturingEarlyInput() 是一个精巧的细节:从 import main.js 到 REPL 渲染完成有几百毫秒的时间,如果用户在这段时间内已经开始打字,这些按键会被缓存起来,等 UI 就绪后回放。用户不会感知到任何输入丢失。
工程思想 #1:快速路径分发(Fast Path Dispatch)
对于
--version这样的请求,加载 50 万行代码只为输出一个版本号是巨大的浪费。快速路径让简单请求在毫秒级返回,零模块加载。这是 CLI 工具性能优化的第一原则:区分轻量请求和重量请求,为轻量请求提供捷径。
2.3 Phase 1:模块加载与并行预取(main.tsx 副作用)
文件: main.tsx(~3800 行)
当 import('../main.js') 执行时,Node.js 开始加载 main.tsx 及其 200+ 个依赖模块。这需要约 135ms。
但 Claude Code 并没有干等这 135ms,而是在模块导入语句之前就发起了两个异步操作:
1 | // main.tsx:12-20 —— 文件顶部,所有 import 之前 |
时间重叠图
1 | 时间 ──────────────────────────────────────────────────────────→ |
工程思想 #2:并行预取(Parallel Prefetch)
I/O 操作(读文件、网络请求、子进程通信)的等待时间可以与 CPU 计算(模块解析、代码编译)重叠。通过在"必须做的慢操作"(import)开始之前,先发起不依赖 import 结果的 I/O,实现了时间折叠。
这与 CPU 流水线中的"指令预取"思想完全一致:不等上一步完成就开始下一步的准备工作。
2.4 Phase 2:主函数与交互模式检测
文件: main.tsx,main() 函数(第 585 行起)
import 完成后,真正的 main() 函数开始执行。
步骤一:环境与参数预处理
1 | // main.tsx:585-650 |
步骤二:交互模式检测
这是一个关键分叉点,决定了后续走交互式(REPL)还是非交互式(Headless)路径:
1 | // main.tsx:660-680 |
(Headless)"] Print -- "true" --> NonInteractive SDK -- "true" --> NonInteractive Init -- "true" --> NonInteractive TTY -- "true" --> Interactive["交互模式
(REPL)"] Print -- "false" --> Interactive
步骤三:提前加载设置
1 | // main.tsx:815-854 |
eagerLoadSettings() 在 Commander.js 还没解析完参数时就手动扫描 argv,提前加载设置文件。为什么?因为后续的 init() 需要用到配置,如果等 Commander 解析完再加载,会晚几十毫秒。
工程思想 #3:急切加载(Eager Loading)
对于明确会用到的数据,不必等到"正式需要"时才去读取。通过手动提前扫描参数,打破了"参数解析 → 读配置"的串行依赖。
2.5 Phase 3:Commander 解析与 preAction 钩子
文件: main.tsx,run() 函数(第 884 行起)
Commander.js 程序定义
Claude Code 使用 Commander.js 做命令行参数解析。run() 函数的前半段定义了 30+ 个 CLI 选项:
1 | // main.tsx:884-905 —— 简化示意 |
preAction 钩子:串行依赖的汇合点
Commander.js 的 preAction 钩子在参数解析完成、Action Handler 执行之前触发。这是收集前面所有并行操作结果的地方:
1 | // main.tsx:907-967 |
ensureMdmSettingsLoaded(),
ensureKeychainPrefetchCompleted()
])"] Init["await init()"] Sinks["initSinks()"] Migrate["runMigrations()"] FireForget["void loadRemoteManagedSettings()
void loadPolicyLimits()"] end MDM --> Wait Key --> Wait Imports --> PreAction Wait --> Init --> Sinks --> Migrate --> FireForget
注意 loadRemoteManagedSettings() 和 loadPolicyLimits() 前面的 void 关键字——它们是 fire-and-forget(发射即忘)操作。启动流程不会等待远程配置加载完成,因为本地配置已经足够启动。远程配置加载完成后会异步合并到当前配置中。
工程思想 #4:汇合点模式(Join Point)
并行操作需要一个明确的"汇合点"来收集结果。
preAction钩子恰好处于"参数解析完成"和"业务逻辑开始"之间,是天然的汇合位置。配合Promise.all确保所有前置条件就绪。
2.6 Phase 4:核心初始化(init.ts)
文件: entrypoints/init.ts,init() 函数(第 57 行)
init() 是被 memoize() 包装的异步函数,无论调用多少次都只执行一次。它负责建立整个应用运行所需的基础设施。
初始化清单
1 | // init.ts:57-214 —— 简化后的执行序列 |
preconnectAnthropicApi():TCP+TLS 预热
这是一个值得特别关注的优化。普通 HTTP 请求的流程是:
1 | 用户提交问题 → 建立 TCP 连接 → TLS 握手 → 发送请求 → 等待响应 |
Claude Code 在初始化阶段就提前完成了 TCP+TLS 握手:
1 | init() 阶段: preconnectAnthropicApi() → TCP 连接 → TLS 握手 → 连接放入池中 |
工程思想 #5:连接预热(Connection Warming)
对于明确要访问的远程服务,在用户真正需要之前就完成网络握手。这和浏览器中
<link rel="preconnect">是同一个思想。
memoize 模式
init() 用 memoize() 包装的原因是:多个代码路径都可能需要调用 init()(比如快速路径中的 --dump-system-prompt 也需要初始化配置),但初始化逻辑只应执行一次。
1 | // memoize 的效果 |
工程思想 #6:幂等初始化(Idempotent Init)
初始化函数应该是幂等的——调用一次和调用多次效果相同。
memoize()是实现幂等初始化的最简模式,比维护一个initialized布尔标志更安全(因为它还能正确处理并发调用的情况)。
2.7 Phase 5:信任边界与安全屏障
文件: interactiveHelpers.tsx,showSetupScreens() 函数
这是启动流程中最重要的安全节点。在信任对话框完成之前:
- 不执行任何工具
- 不启用遥测
- 不发送任何数据
信任屏幕序列
介绍功能和限制"] FirstTime -- "否" --> Trust Onboard --> Trust{"需要信任确认?"} Trust -- "是" --> TrustDialog["信任对话框
用户必须显式接受
才能继续"] Trust -- "否" --> API TrustDialog -- "接受" --> API{"需要 API Key?"} TrustDialog -- "拒绝" --> Exit["退出程序"] API -- "是" --> APIDialog["API Key 输入界面"] API -- "否" --> Bypass APIDialog --> Bypass{"使用了
--dangerously-skip-permissions?"} Bypass -- "是" --> BypassWarn["危险权限警告
用户二次确认"] Bypass -- "否" --> AutoCheck BypassWarn --> AutoCheck{"权限模式 = auto?"} AutoCheck -- "是" --> AutoOptIn["Auto 模式说明
用户同意"] AutoCheck -- "否" --> Telemetry AutoOptIn --> Telemetry["initializeTelemetryAfterTrust()
信任后才启用遥测"] Telemetry --> Done(("继续启动"))
信任后才初始化遥测
1 | // init.ts:247 |
注意 OpenTelemetry 库有 ~400KB,如果在启动早期就加载,会拖慢整个启动速度。把它推迟到信任对话框之后加载,不仅是安全设计,也是性能优化。
工程思想 #7:信任前零泄漏(Zero Leakage Before Trust)
在用户显式授权之前,不发送任何数据(包括遥测)。这不仅是隐私合规要求,更是建立用户信任的基础。信任对话框是一个安全闸门——闸门前后是完全不同的权限域。
2.8 Phase 6:状态树构建与工具注册
文件: main.tsx(Action Handler),state/,tools.ts
通过信任屏障后,开始构建应用运行所需的核心状态。
AppState 状态树
1 | // state/AppStateStore.ts:89 —— 简化后的 AppState 类型 |
状态创建流程
(Commander.js 解析)"] Settings["设置文件
(多层合并)"] Defaults["getDefaultAppState()
(默认值)"] CLI --> Merge["合并为 initialState"] Settings --> Merge Defaults --> Merge Merge --> Store["createStore(initialState, onChangeAppState)"] Store --> Provider["AppStateProvider
(React Context)"] Provider --> Children["子组件通过
useAppState() 读取"]
工具注册
1 | // tools.ts:193 —— getAllBaseTools() |
工程思想 #8:固定工具顺序促进缓存(Stable Order for Cache)
工具列表的顺序刻意保持固定。因为工具描述会序列化到系统提示词中,发送给 LLM API。如果顺序不变,API 端可以复用提示词缓存(prompt cache),显著降低延迟和成本。这是一个容易忽略但影响巨大的优化。
MCP 服务器发现
1 | // services/mcp/config.ts —— MCP 配置来源 |
MCP 服务器的实际连接不在这个阶段完成。配置只是被收集起来,真正的连接在 REPL 挂载后,由 useManageMCPConnections() Hook 异步建立。
2.9 Phase 7:React 组件树挂载与 REPL 启动
文件: ink.ts、replLauncher.tsx、components/App.tsx、screens/REPL.tsx
这是启动链路的最后一步——把一切"连接"起来,渲染到终端。
组件树结构
(createRoot)"] Theme["ThemeProvider"] App["App"] FPS["FpsMetricsProvider"] Stats["StatsProvider"] AppState["AppStateProvider
(创建 Store)"] Mailbox["MailboxProvider"] Voice["VoiceProvider"] REPL["REPL
(screens/REPL.tsx)"] MCP["MCPConnectionManager"] Prompt["PromptInput"] Messages["Messages"] Permission["PermissionRequest"] Status["StatusLine"] Root --> Theme --> App --> FPS --> Stats --> AppState AppState --> Mailbox --> Voice --> REPL REPL --> MCP REPL --> Prompt REPL --> Messages REPL --> Permission REPL --> Status
挂载三步曲
1 | // 第一步:创建 Ink 终端 React 根 |
延迟预取(Deferred Prefetches)
startDeferredPrefetches() 在 UI 渲染完成后才执行。这些操作不影响首屏渲染,但能加速后续的第一次用户交互:
1 | // 延迟预取的内容 |
工程思想 #9:分级加载(Tiered Loading)
将初始化工作分为三个优先级:
- 立即执行:快速路径判断、环境变量设置
- UI 就绪前:配置、认证、权限、网络——用户交互所必需
- UI 就绪后:上下文预取、文件统计、变化监听——提升后续体验
只有优先级 1 和 2 的工作影响启动速度,优先级 3 的工作在后台静默完成。
2.10 非交互模式:Headless 分支
当检测到非交互模式(-p 标志、非 TTY 等),启动流程在 Phase 6 之后走完全不同的分支:
REPL.tsx"] Check -- "非交互式" --> Headless["import cli/print.js"] Headless --> Run["runHeadless(prompt, options)"] Run --> Query["直接调用 query()
(无 UI 层)"] Query --> Format{"--output-format?"} Format -- "json" --> JSON["JSON 输出到 stdout"] Format -- "text" --> Text["纯文本输出到 stdout"] Format -- "stream-json" --> Stream["流式 JSON 输出"] JSON --> Exit["process.exit()"] Text --> Exit Stream --> Exit
非交互模式完全跳过 React/Ink,直接消费 query() 生成器的输出。这使得 Claude Code 可以被用作管道工具:
1 | # 管道模式示例 |
2.11 配置系统:多源合并的层级模型
Claude Code 的配置来自7 个不同来源,按优先级从高到低合并:
(policySettings)
企业管理员强制设定"] P2["② 命令行标志
(flagSettings)
本次运行指定"] P3["③ 本地配置
(.claude/settings.local.json)
不提交到 Git"] P4["④ 项目配置
(.claude/settings.json)
提交到 Git,团队共享"] P5["⑤ 用户配置
(~/.claude/settings.json)
个人全局偏好"] P6["⑥ 远程托管配置
(managed settings)
服务端下发"] P7["⑦ 默认值
(硬编码)
兜底"] P1 ~~~ P2 ~~~ P3 ~~~ P4 ~~~ P5 ~~~ P6 ~~~ P7 end Merge["getEffectiveSettings()
多层合并"] Result["最终生效的配置"] P1 --> Merge P2 --> Merge P3 --> Merge P4 --> Merge P5 --> Merge P6 --> Merge P7 --> Merge Merge --> Result
各层配置的加载时机
| 配置层 | 加载时机 | 加载方式 |
|---|---|---|
| 默认值 | 编译时 | 硬编码在源码中 |
| 用户配置 | Phase 2(eagerLoadSettings) | 同步读文件 |
| 项目配置 | Phase 2(eagerLoadSettings) | 同步读文件 |
| 本地配置 | Phase 2(eagerLoadSettings) | 同步读文件 |
| 命令行标志 | Phase 3(Commander 解析) | argv 解析 |
| 组织策略 | Phase 4(init) | 从 MDM 子进程获取 |
| 远程托管配置 | Phase 3(preAction fire-and-forget) | 异步 HTTP,延迟合并 |
工程思想 #10:多层配置与就近覆盖(Layered Config with Proximity Override)
每一层配置都有明确的"谁设置、何时设置、作用范围"语义。离用户越近的配置优先级越高(命令行 > 本地 > 项目 > 全局),但组织策略始终最高——这保证了企业管理员可以强制执行安全策略,即使用户试图覆盖。
2.12 CLAUDE.md 记忆系统的加载
CLAUDE.md 是 Claude Code 的"项目记忆"机制。它在启动过程中被发现、加载、注入到系统提示词。
文件发现规则
(Managed 全局指令)"] L2["② ~/.claude/CLAUDE.md
~/.claude/rules/*.md
(User 个人指令)"] L3["③
(Project 项目指令)"] L4["④
(Local 本地指令)"] L1 ~~~ L2 ~~~ L3 ~~~ L4 end Note["后加载的文件在 prompt 中
位置更靠后 → 模型注意力更强"] L4 --> Note
加载机制
1 | // utils/claudemd.ts:790 |
@include 指令允许 CLAUDE.md 引用其他文件:
1 | <!-- CLAUDE.md 中的 @include 示例 --> |
注入到系统提示词
加载的 CLAUDE.md 内容最终被包装在 <system-reminder> 标签中,注入到每次 API 调用的系统提示词:
1 | 系统提示词结构: |
2.13 关键工程思想总结
将启动流程中体现的工程思想汇总如下:
| # | 工程思想 | 体现位置 | 核心理念 |
|---|---|---|---|
| 1 | 快速路径分发 | cli.tsx 拦截 --version | 轻量请求走捷径,不加载重模块 |
| 2 | 并行预取 | main.tsx 顶部 MDM+Keychain | I/O 等待与 CPU 计算重叠执行 |
| 3 | 急切加载 | eagerLoadSettings() | 明确会用到的数据提前读取,打破串行依赖 |
| 4 | 汇合点模式 | preAction 中 Promise.all | 并行操作在明确节点收集结果 |
| 5 | 连接预热 | preconnectAnthropicApi() | 提前完成网络握手,加速首次请求 |
| 6 | 幂等初始化 | init() 用 memoize 包装 | 多次调用效果等同一次,安全且无副作用 |
| 7 | 信任前零泄漏 | 信任对话框前不启用遥测 | 安全闸门隔离不同权限域 |
| 8 | 固定顺序促缓存 | assembleToolPool() 顺序固定 | 稳定的提示词结构利于 API 端缓存 |
| 9 | 分级加载 | 立即/UI前/UI后三级 | 只让必要工作影响启动速度 |
| 10 | 多层配置就近覆盖 | 7 层配置合并 | 离用户越近优先级越高,组织策略始终最高 |
启动时间线总览
1 | 时间 0ms 135ms 300ms 500ms+ |
下一步
- 第三章:Agent 循环深度解析——
query.ts的 1729 行如何驱动整个 Agent - 第四章:斜杠命令系统——/resume、/clear、/compact 的完整实现剖析
本文档基于 Claude Code 源码分析生成,源码路径:
claude-code/src/
