深入 Claude Code 源码(一):启动层——从 `claude` 回车到光标出现,经历了什么?

随着 AI 编程助手的普及,越来越多的开发者开始关注一个问题:这些工具的启动速度。一个需要等待 3 秒才能响应第一个字符的助手,和一个 300ms 内就绪的助手,用起来的感觉是天壤之别。Claude Code 作为一个运行在终端里的 AI 代理,在启动这件事上做了大量精心的工程设计------不是靠减少功能,而是靠更聪明的初始化策略。

本文是「深入 Claude Code 源码」系列的第一篇,带大家拆解 Claude Code 的启动层(Bootstrap Layer)。我们会从进程启动的第一行代码,一路追踪到终端光标出现,把中间经历的每一个关键环节都说清楚。


一、守门员:dev-entry.ts

Claude Code 的入口文件是 src/dev-entry.ts,但它并不是第一个被执行的业务逻辑------它是一个守门员

大家可能会思考,一个正常的 Node.js/Bun 项目,直接 import 入口文件不就行了,为什么要多一个守门员?

这里需要说明一下背景:Claude Code 的源码是从 npm 发布包的 sourcemap 重建出来的。整个 src/ 目录有几百个模块,任何一个 import 路径写错,或者某个文件在重建过程中有缺失,程序运行到一半就会崩溃,而且错误信息往往藏在很深的调用栈里,极难定位。

dev-entry.ts 的做法是------在真正启动业务逻辑之前,先扫描所有相对 import 路径,验证每一个被引用的模块文件确实存在、可以被 Bun 的解析器找到。如果有任何一个缺失,它会打印出明确的诊断信息然后干净地退出,而不是让程序在某个角落里悄悄崩溃。

这就类似于航天器发射前的「发射前检查清单」------所有子系统依次确认 OK,才按下点火键。多一道检查的代价极低,但能拦住的问题却很真实。


二、最讲究顺序的文件:main.tsx

通过 dev-entry.ts 的校验之后,程序来到真正的核心启动文件:src/main.tsx

这个文件的开头有一段注释,值得大家仔细读:

typescript 复制代码
// These side-effects must run before all other imports:
// 1. profileCheckpoint marks entry before heavy module evaluation begins
// 2. startMdmRawRead fires MDM subprocesses (plutil/reg query) so they run
//    in parallel with the remaining ~135ms of imports below
// 3. startKeychainPrefetch fires both macOS keychain reads (OAuth + legacy
//    API key) in parallel
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();

这三行 import 加副作用调用,顺序严格不能乱。原因如下:

macOS 上读取 Keychain(钥匙串,用于存储 OAuth token 和 API key)需要调用系统 API,这个操作大约耗时 65ms 。企业 MDM 策略的读取也需要启动子进程(plutilreg query),同样需要数十毫秒。

如果等到所有模块 import 完成之后再做这两件事,它们会在关键路径上串行执行,白白增加启动延迟。工程师的思路是------在模块 import 还没执行完的时候,就把这两个慢操作的异步任务先发射出去。等后续业务逻辑真正需要这些数据时,它们早就在后台跑完了,取结果几乎不需要等待。

这就像一家餐厅,服务员在顾客点菜的同时就去厨房通知「备好常用食材」,不必等菜单确认了才开始备料。等顾客菜单出来,厨房已经准备好大半了。


三、功能开关:bun:bundle 的编译时黑魔法

继续往下看 main.tsx,大家会发现一个贯穿全文件的模式:

typescript 复制代码
const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null;

const assistantModule = feature('KAIROS')
  ? require('./assistant/index.js')
  : null;

这里的 feature('COORDINATOR_MODE') 来自 Bun 的 bun:bundle 功能------它是编译时的死代码消除(Dead Code Elimination),不是运行时判断。

这里需要说明一个重要的区别:如果是运行时判断,代码会在 bundle 里,只是不执行;而编译时消除意味着当 feature('COORDINATOR_MODE')false 时,那整个 require('./coordinator/coordinatorMode.js') 分支从最终产物里根本不存在------不是「存在但跳过执行」,而是「从未编译进去」。

Claude Code 有很多处于不同开发阶段的实验性功能:多智能体协调(COORDINATOR_MODE)、KAIROS 助手模式、历史记录 Snip(HISTORY_SNIP)、响应式压缩(REACTIVE_COMPACT)...... 这些功能可能对某些部署场景不可用,或者还在内测中。用 require() 而非顶层 import,配合 feature() 判断,Bun 就能在构建时把对应分支彻底从产物中剔除。

CLAUDE.md 里特别提醒大家保留这个模式:

Conditional imports via bun:bundle feature flags use require() to avoid circular dependencies --- preserve this pattern.


四、完整启动流程

main.tsx 的初始化过程梳理下来,整个流水线如图所示:
#mermaid-svg-lnTkn1Lv1Q6Hsvz1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .error-icon{fill:#552222;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .marker.cross{stroke:#333333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 p{margin:0;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster-label text{fill:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster-label span{color:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster-label span p{background-color:transparent;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .label text,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 span{fill:#333;color:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node rect,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node circle,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node ellipse,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node polygon,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .rough-node .label text,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node .label text,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .image-shape .label,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .rough-node .label,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node .label,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .image-shape .label,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .icon-shape .label{text-align:center;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node.clickable{cursor:pointer;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .arrowheadPath{fill:#333333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster text{fill:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .cluster span{color:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .icon-shape,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .icon-shape p,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .icon-shape .label rect,#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lnTkn1Lv1Q6Hsvz1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 交互模式 (无 -p)
print 模式 (-p)
SDK 模式 (库调用)
dev-entry.ts

模块完整性校验
main.tsx 入口

三个并行预热任务启动
Commander.js 命令解析

注册所有子命令和参数
认证与策略初始化

信任对话框 / MDM 策略 / GrowthBook
工具与服务装载

getTools() / getMcpTools() / getCommands()
运行模式判断
launchRepl()

React/Ink 终端 UI
ask()

直接调用 QueryEngine
QueryEngine 类

完全绕过 main.tsx

各阶段的关键细节:

Phase 1 --- 并行预热(import 阶段)

  • profileCheckpoint('main_tsx_entry') 打上性能计时起点
  • startMdmRawRead() 异步读取 MDM 企业策略(macOS 的 plutil,Windows 的注册表)
  • startKeychainPrefetch() 并行发起两个钥匙串读取(OAuth token + 旧版 API key)

Phase 2 --- Commander.js 命令解析

注册 claude 的所有子命令:claude loginclaude configclaude mcpclaude --resumeclaude -p "..." 等,并解析当前调用的命令行参数。

Phase 3 --- 认证与权限初始化

  • checkHasTrustDialogAccepted() 判断是否需要弹出信任确认对话框
  • loadPolicyLimits() 加载企业策略限制(某些企业会禁用特定功能)
  • initializeGrowthBook() 初始化功能开关服务,决定哪些实验性功能对当前用户开放

Phase 4 --- 工具与服务装载

  • getTools() 收集所有 53 个工具的定义
  • getMcpToolsCommandsAndResources() 连接并枚举所有已配置的 MCP 服务
  • getCommands() 收集所有 87 个 slash 命令

Phase 5 --- UI 或执行路径选择

根据命令行参数决定走哪条路:-p 参数走 print 模式,无参数走 REPL 交互模式,作为 SDK 被调用时完全绕过这里。


五、三条执行路径的差异

大家可能还有疑问:SDK 模式和 print 模式具体有什么区别?

总体而言,三条路径的差异可以用下面这个表格来比较:

运行模式 入口 是否需要终端 UI 典型场景
交互模式(REPL) main.tsxlaunchRepl() 是,React/Ink 日常开发,直接输入指令
print 模式(-p main.tsxask() 否,stdout 输出 CI/CD、脚本集成
SDK 模式 直接 new QueryEngine(...) 否,调用方决定 嵌入式使用,如 Claude Desktop

三条路径都依赖同一个底层引擎------QueryEngine,这也是为什么 Claude Code 能同时服务这么多不同场景,核心逻辑不需要重写。


六、启动延迟的工程取舍

整个启动过程,从 claude 回车到光标出现,在现代 Mac 上大约 300~500ms 。其中绝大部分时间花在 TypeScript/Bun 模块解析上(约 135ms 的 import 评估,代码注释里明确标注了这个数字),业务逻辑本身非常轻。

这里有一个值得关注的工程权衡:print 模式(-p)里,recordTranscript() 这个写磁盘的操作被设计成 fire-and-forget(发射后不等待):

typescript 复制代码
// Bare/print mode: fire-and-forget. Scripted calls don't --resume after
// kill-mid-request. The await is ~4ms on SSD, ~30ms under disk contention

在 SSD 上只需要 4ms ,但在磁盘压力大时可能到 30ms 。对于 print 模式的脚本调用来说,这 30ms 意义不大,所以不阻塞。但交互模式下,为了保证 --resume 可靠,这里会等待写入完成。

这种「根据场景决定同步/异步」的细粒度取舍,在 Claude Code 的代码里随处可见。


本文带大家拆解了 Claude Code 启动层的核心设计,学习了:

  1. dev-entry.ts 是进程启动前的完整性检查,确保所有模块路径可解析
  2. main.tsx 的 import 顺序严格有序,通过并行预热把慢操作(Keychain、MDM)挪出关键路径
  3. feature() + require() 是 Bun 的编译时死代码消除,让实验性功能从未出现在产物里
  4. 启动最终分叉为交互、print、SDK 三条路径,都共享同一个底层 QueryEngine

接下来,我们将进入第二篇------查询引擎 ,也就是真正驱动 Claude 工作的心脏:query.tsQueryEngine.ts