随着 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 策略的读取也需要启动子进程(plutil 或 reg 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:bundlefeature flags userequire()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 login、claude config、claude mcp、claude --resume、claude -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.tsx → launchRepl() |
是,React/Ink | 日常开发,直接输入指令 |
print 模式(-p) |
main.tsx → ask() |
否,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 启动层的核心设计,学习了:
dev-entry.ts是进程启动前的完整性检查,确保所有模块路径可解析main.tsx的 import 顺序严格有序,通过并行预热把慢操作(Keychain、MDM)挪出关键路径feature()+require()是 Bun 的编译时死代码消除,让实验性功能从未出现在产物里- 启动最终分叉为交互、print、SDK 三条路径,都共享同一个底层
QueryEngine
接下来,我们将进入第二篇------查询引擎 ,也就是真正驱动 Claude 工作的心脏:query.ts 和 QueryEngine.ts。