阶段 1 | 入口与启动链¶
目标:搞清楚用户输入
claude(或claude code)回车后,进程怎么走完"启动 → 初始化 → 打开 REPL"。 时长:1~2 小时 前端类比:理解 React 项目的index.html→main.tsx→<App />→ 路由 → 第一个页面的全链路
1.1 入口层级总览¶
Claude Code 的启动是双层入口模式(注意区分两个 main()):
bun run src/main.tsx ← bun 直接跑的是这个文件(package.json 缺失,无法验证;bin 入口推断)
↓
src/entrypoints/cli.tsx ← 二级 main(),fast-path 分流
↓
src/main.tsx 里的 main() ← 真正的"主 main()",处理大多数场景
↓
src/entrypoints/init.ts ← 全局初始化
↓
src/replLauncher.tsx ← 打开 REPL(如果是交互式场景)
↓
src/screens/REPL.tsx ← 终于开始渲染第一个 TUI 画面
💡 "双 main" 模式是性能优化:
cli.tsx里把所有常见 fast-path(--version、MCP server 模式、Chrome native host 等)用动态 import 隔离开,不进入主入口的 ~135ms 启动开销。Web 项目里类似"路由 lazy-load"的思想。
1.2 第一层:bin 入口 src/main.tsx¶
1.2.1 头部 18 行 —— 优先级最高的代码¶
// src/main.tsx 顶部
import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();
import { ensureKeychainPrefetchCompleted, startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();
这段代码的注释(行首 5 行)说明了三件大事:
1. profileCheckpoint —— 启动性能打点,每个关键节点打一个时间戳,最后汇总报告
2. startMdmRawRead —— 启动 macOS MDM 子进程(plutil),并发跑在剩余 135ms 导入时间里
3. startKeychainPrefetch —— 预取 macOS Keychain 里的 OAuth token 和 legacy API key,否则 applySafeConfigEnvironmentVariables() 会同步阻塞 65ms
前端类比:这就是 Web 项目的 <link rel="prefetch"> / <link rel="preload">。Claude Code 是 CLI 没法用 <link>,但思路一样:把能并行的 IO 提前到主流程之外。
💡 注意
eslint-disable-next-line custom-rules/no-top-level-side-effects—— 项目的代码规范是禁止顶层副作用(防止 import 时执行不可预期逻辑),但启动优化是特例,要显式禁用规则。
1.2.2 主体 import 块(第 18~50 行)¶
这一坨 import 暴露了整个项目的依赖关系:
- 第三方:bun:bundle、@commander-js/extra-typings、chalk、lodash-es、react
- 内部核心:./context.js、./entrypoints/init.js、./replLauncher.js、./Tool.js、./tools.js
- 工具函数 30+ 个:auth、config、effort、fastMode、messages、renderOptions …
💡 学习技巧:把这段 import 当目录读。所有"全局工具函数"都在
utils/目录(注意很多是utils/settings/*、utils/secureStorage/*这种二级子目录),所有"全局业务服务"在services/。命名规范 100% 稳定可推断。
1.2.3 两个导出的关键函数¶
// src/main.tsx:388
export function startDeferredPrefetches(): void { ... }
// src/main.tsx:585
export async function main() { ... }
startDeferredPrefetches(行 388):在 idle 时段延后预取,不阻塞启动。前端类比:React 18 的startTransition+ 后台 prefetch。main()(行 585):真正的入口。处理process.argv、决定走哪个屏幕。
1.2.4 main() 内部大致流程(基于 import 推断)¶
由于文件 803KB 完整读完不现实,我们从 import 列表倒推 main() 必做的步骤:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | init() from ./entrypoints/init.js |
加载 config、初始化 telemetry、加载 MCP servers |
| 2 | getGlobalConfig() |
读 ~/.claude/settings.json 等配置 |
| 3 | getSubscriptionType() / isClaudeAISubscriber() |
判断登录态(OAuth vs API key) |
| 4 | applyConfigEnvironmentVariables() |
把 config 里的 env vars 注入 process.env(给子进程用) |
| 5 | initializeGrowthBook() |
GrowthBook 特性开关初始化 |
| 6 | loadPolicyLimits() / loadRemoteManagedSettings() |
企业策略和远程管理设置 |
| 7 | 解析 process.argv |
决定走哪条路径 |
| 8 | launchRepl() from ./replLauncher.js |
启动 REPL(最常见路径) |
| 9 | 或 runClaudeInChromeMcpServer() |
MCP server 模式(--claude-in-chrome-mcp) |
| 10 | 或 startAsCliAgent() from ./entrypoints/sdk/ |
SDK 模式(被其他程序嵌入) |
1.3 第二层:fast-path 入口 src/entrypoints/cli.tsx¶
文件第 1~6 行注释明说:"Bootstrap entrypoint - checks for special flags before loading the full CLI. All imports are dynamic to minimize module evaluation for fast paths."
这是 Claude Code 启动优化的核心:所有"我知道我马上要 exit"的命令,绝不让 main.tsx 加载。
1.3.1 Fast-path 列表¶
| 命令 | 行为 | 性能优化点 |
|---|---|---|
--version / -v / -V |
输出 MACRO.VERSION 退出 |
0 个 module 加载(除本文件) |
--dump-system-prompt (Ant-only) |
输出当前 commit 的 system prompt | 动态 import config/model/prompts |
--claude-in-chrome-mcp |
跑 Chrome MCP server | 动态 import claudeInChrome/mcpServer.js |
--chrome-native-host |
跑 Chrome 原生消息 host | 动态 import chrome/nativeHost.js |
1.3.2 关键代码片段¶
// src/entrypoints/cli.tsx 第 1~30 行
import { feature } from 'bun:bundle';
// 修 corepack 自动 pin yarnpkg 的 bug
process.env.COREPACK_ENABLE_AUTO_PIN = '0';
// CCR 容器环境的 heap size 调整
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ' --max-old-space-size=8192';
}
// (Ant-only) Ablation baseline 实验旗标
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
for (const k of ['CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_THINKING', ...]) {
process.env[k] ??= '1';
}
}
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;
}
// ... 更多 fast-path
}
关键设计点:
- process.env 在 import 阶段就改——因为 BashTool/AgentTool/PowerShellTool 在 import 时就把某些 flag 捕获到 module-level const 了,init() 跑的时候就晚了。这条注释揭示了 Claude Code 启动顺序的隐藏依赖。
- MACRO.VERSION 是构建时内联(不是运行时读)—— Bun 构建工具替换。
💡 学习技巧:阅读
cli.tsx时,重点关注"为什么这里要立刻读process.env而不是等 init()" 的注释。这些注释是工程师留下的"决策考古"。
1.3.3 其他特殊模式¶
| 模式 | 触发条件 | 入口模块 |
|---|---|---|
| MCP Server | --claude-in-chrome-mcp |
utils/claudeInChrome/mcpServer.js |
| Chrome Native Host | --chrome-native-host |
utils/chrome/nativeHost.js |
| MCP server (通用) | mcp 子命令 |
commands/mcp/index.js |
| Doctor(诊断) | doctor 子命令 |
commands/doctor/index.js → screens/Doctor.tsx |
| Resume(恢复会话) | resume 子命令 |
commands/resume/index.js → screens/ResumeConversation.tsx |
| REPL(默认) | 无参数 / -c / -p |
replLauncher.tsx → screens/REPL.tsx |
1.4 初始化:src/entrypoints/init.ts¶
定位:所有"启动时一次性的副作用"集中地。
从 main.tsx 顶部 import 看到调用是 init() 和 initializeTelemetryAfterTrust()。推测内部做:
- 读取
~/.claude/settings.json(user settings) - 读取
.claude/settings.json(project settings) - 读取
.claude/settings.local.json(local-only) - 合并 settings、解析 include glob
- 加载 MCP server 配置
- 初始化 Sentry/telemetry
- 启动后台 housekeeping
前端类比:就是 Web 项目的 app.tsx 里 useEffect(() => { ... }, []) 那段。
1.5 REPL 启动:src/replLauncher.tsx¶
定位:
main.tsx决定要走 REPL 之后的最后一跳。
launchRepl() 函数从 main.tsx:24 import 得到。它的职责:
- 准备 Ink 的
render调用(<Root><REPL /></Root>) - 处理 stdin/stdout 模式(TTY 交互 vs pipe pipe)
- 处理
--output-format stream-json等 SDK 模式 - 把控制权交给
REPL.tsx组件
前端类比:React 里的 ReactDOM.createRoot(...).render(<App />)。
1.6 三个"非默认"屏幕的入口¶
| 命令 | 文件 | 屏幕组件 |
|---|---|---|
claude doctor |
src/commands/doctor/index.js |
src/screens/Doctor.tsx |
claude (无参 → 恢复) |
src/commands/resume/index.js |
src/screens/ResumeConversation.tsx |
claude (有 args) |
默认 | src/screens/REPL.tsx |
💡 你会发现
screens/目录只有 3 个文件 —— REPL 是绝对核心,Doctor 和 ResumeConversation 都是边缘场景。
1.7 关键洞察总结¶
1.7.1 "双 main" + fast-path 模式¶
cli.tsx 的存在是为了让短命的命令不付 135ms 启动成本。Web 项目里用 lazy route 解决,CLI 用 fast-path + 动态 import 解决。
1.7.2 启动期 prefetch 哲学¶
startMdmRawRead、startKeychainPrefetch、prefetchPassesEligibility、prefetchOfficialMcpUrls、prefetchAwsCredentialsAndBedRockInfoIfSafe、prefetchGcpCredentialsIfSafe —— 6 个 prefetch 函数集中在 main.tsx 头部。
前端类比:现代 SPA 的 <link rel="prefetch"> + requestIdleCallback prefetch API。Claude Code 的策略是启动时能并行的 IO 全并行,不放过任何能省的时间。
1.7.3 Bun feature('XXX') 的 build-time DCE¶
if (feature('ABLATION_BASELINE') && ...) { ... }
const REPLTool = process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
feature('X') —— Bun 自带的构建时门,外部构建不包含 X 特性,整个 if 分支会被消除
- process.env.USER_TYPE === 'ant' —— Bun 构建时把 process.env 替换为字面量,未命中的分支被消除
阅读时看到这两种写法,整个 if 分支可以直接跳过(对外部构建而言是死代码)。
1.7.4 Top-level side-effect 的合理性¶
项目有一条 ESLint 规则 custom-rules/no-top-level-side-effects,禁止 import 时执行副作用。但启动优化是例外(profileCheckpoint、startMdmRawRead 等都加了 eslint-disable 注释)。这告诉我们:这个项目对"启动顺序"高度敏感,改动 import 顺序可能改变行为。
1.8 阅读清单(按顺序)¶
- ✅
src/entrypoints/cli.tsx(前 100 行)—— fast-path 完整流程 - ✅
src/main.tsx(前 50 行)—— 头部 prefetch 设计 - 🔍
src/main.tsx(行 585 的main())—— 主入口逻辑(只读不读细节,关注调用链) - ✅
src/entrypoints/init.ts(通读)—— 全局初始化 - ✅
src/replLauncher.tsx(通读)—— REPL 启动器 - 📌
src/screens/Doctor.tsx+src/screens/ResumeConversation.tsx(快速浏览)—— 对比看 REPL 的特殊性
1.9 练习任务¶
- 手画一张"启动流程图",把
cli.tsx的 fast-path 和main.tsx的慢路径都画出来 - 找出所有
start*Prefetch/prefetch*函数(grep -nE "prefetch|startKeychain|startMdm"),列出来理解并行策略 - 用
grep -nE "^export (async )?function" src/main.tsx找到所有顶层函数,理解每个的职责 - 思考:如果让你加一个新命令
claude logs(只看会话日志、不进 REPL),你应该加在cli.tsx还是commands/?为什么?
1.10 下一步¶
进入 阶段 2:REPL 主循环 —— 这是 Claude Code 的"主战场",895KB 的 REPL.tsx 是怎么把 React 状态/渲染范式平移到 TTY 的。