跳转至

阶段 1 | 入口与启动链

目标:搞清楚用户输入 claude(或 claude code)回车后,进程怎么走完"启动 → 初始化 → 打开 REPL"。 时长:1~2 小时 前端类比:理解 React 项目的 index.htmlmain.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-typingschalklodash-esreact - 内部核心:./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.jsscreens/Doctor.tsx
Resume(恢复会话) resume 子命令 commands/resume/index.jsscreens/ResumeConversation.tsx
REPL(默认) 无参数 / -c / -p replLauncher.tsxscreens/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.tsxuseEffect(() => { ... }, []) 那段。

1.5 REPL 启动:src/replLauncher.tsx

定位main.tsx 决定要走 REPL 之后的最后一跳

launchRepl() 函数从 main.tsx:24 import 得到。它的职责:

  1. 准备 Ink 的 render 调用(<Root><REPL /></Root>
  2. 处理 stdin/stdout 模式(TTY 交互 vs pipe pipe)
  3. 处理 --output-format stream-json 等 SDK 模式
  4. 把控制权交给 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 哲学

startMdmRawReadstartKeychainPrefetchprefetchPassesEligibilityprefetchOfficialMcpUrlsprefetchAwsCredentialsAndBedRockInfoIfSafeprefetchGcpCredentialsIfSafe —— 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 时执行副作用。但启动优化是例外(profileCheckpointstartMdmRawRead 等都加了 eslint-disable 注释)。这告诉我们:这个项目对"启动顺序"高度敏感,改动 import 顺序可能改变行为。


1.8 阅读清单(按顺序)

  1. src/entrypoints/cli.tsx(前 100 行)—— fast-path 完整流程
  2. src/main.tsx(前 50 行)—— 头部 prefetch 设计
  3. 🔍 src/main.tsx(行 585 的 main())—— 主入口逻辑(只读不读细节,关注调用链)
  4. src/entrypoints/init.ts(通读)—— 全局初始化
  5. src/replLauncher.tsx(通读)—— REPL 启动器
  6. 📌 src/screens/Doctor.tsx + src/screens/ResumeConversation.tsx(快速浏览)—— 对比看 REPL 的特殊性

1.9 练习任务

  1. 手画一张"启动流程图",把 cli.tsx 的 fast-path 和 main.tsx 的慢路径都画出来
  2. 找出所有 start*Prefetch / prefetch* 函数(grep -nE "prefetch|startKeychain|startMdm"),列出来理解并行策略
  3. grep -nE "^export (async )?function" src/main.tsx 找到所有顶层函数,理解每个的职责
  4. 思考:如果让你加一个新命令 claude logs(只看会话日志、不进 REPL),你应该加在 cli.tsx 还是 commands/?为什么?

1.10 下一步

进入 阶段 2:REPL 主循环 —— 这是 Claude Code 的"主战场",895KB 的 REPL.tsx 是怎么把 React 状态/渲染范式平移到 TTY 的。