第二章:启动流程——从敲下命令到界面就绪

当用户在终端敲下 claude 并回车,到看到交互式提示符,中间经历了什么?
本章拆解 Claude Code 的完整启动链路,揭示其中蕴含的性能优化工程设计思想。


目录


2.1 启动全景:8 个阶段

整个启动过程可以划分为 8 个阶段。下图展示了从进程创建到用户可交互的完整链路:

flowchart TD Start(("用户敲下 claude")) subgraph Phase0["Phase 0: 快速路径分发"] P0["cli.tsx:main()"] P0Check{"简单请求?
--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
2
3
4
5
6
7
// 防止 corepack 自动 pin 版本
process.env.COREPACK_ENABLE_AUTO_PIN = '0'

// 远程环境下调整 NODE_OPTIONS
if (process.env.CLAUDE_CODE_REMOTE) {
process.env.NODE_OPTIONS = ...
}

设计意图:环境变量必须在任何 import 之前设置,否则被 import 的模块可能读到错误的值。这就是为什么它们放在文件最顶部,而不是放在 main() 函数里。

快速路径矩阵

flowchart LR Entry["process.argv"] Entry --> V{"--version?"} V -- "是" --> Print1["打印 MACRO.VERSION
直接 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
2
3
4
5
6
// cli.tsx:288-298 —— 正常路径
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js')
startCapturingEarlyInput() // 在 UI 就绪前缓存用户按键

const { main: cliMain } = await import('../main.js')
await cliMain()

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
2
3
4
5
6
7
8
9
10
11
12
// main.tsx:12-20 —— 文件顶部,所有 import 之前
profileCheckpoint('main_tsx_entry')

startMdmRawRead() // 启动子进程读取 MDM(企业管理)配置
startKeychainPrefetch() // 启动子进程读取 macOS Keychain 密钥

// --- 接下来是 200+ 行的 import 语句,耗时 ~135ms ---
import { Commander } from 'commander'
import { createStore } from './state/store.js'
// ... 200+ imports ...

profileCheckpoint('main_tsx_imports_loaded') // 标记 import 完成

时间重叠图

1
2
3
4
5
6
7
8
9
10
11
时间 ──────────────────────────────────────────────────────────→

main.tsx 文件开始加载:
├─ startMdmRawRead() ──────────────────┐ (子进程 I/O ~65ms)
├─ startKeychainPrefetch() ────────────────┐│ (子进程 I/O ~65ms)
│ ││
└─ 200+ import 语句加载 ─────────────────┘┘ (CPU ~135ms)

三者同时完成
净耗时 = max(65, 65, 135) ≈ 135ms
而非串行: 65 + 65 + 135 = 265ms

工程思想 #2:并行预取(Parallel Prefetch)

I/O 操作(读文件、网络请求、子进程通信)的等待时间可以与 CPU 计算(模块解析、代码编译)重叠。通过在"必须做的慢操作"(import)开始之前,先发起不依赖 import 结果的 I/O,实现了时间折叠

这与 CPU 流水线中的"指令预取"思想完全一致:不等上一步完成就开始下一步的准备工作


2.4 Phase 2:主函数与交互模式检测

文件: main.tsxmain() 函数(第 585 行起)

import 完成后,真正的 main() 函数开始执行。

步骤一:环境与参数预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.tsx:585-650
profileCheckpoint('main_function_start')

// Windows 安全设置
process.env.NoDefaultCurrentDirectoryInExePath = '1'

// 捕获 Node.js 内部警告
initializeWarningHandler()

// 处理特殊格式的参数
handleCcProtocolUrls(argv) // cc:// 协议深链接
handleSshModeArgs(argv) // SSH 模式
handleAssistantArgs(argv) // --assistant 参数

步骤二:交互模式检测

这是一个关键分叉点,决定了后续走交互式(REPL)还是非交互式(Headless)路径:

1
2
3
4
5
6
7
8
9
10
11
// main.tsx:660-680
const hasPrintFlag = argv.includes('-p') || argv.includes('--print')
const hasInitOnlyFlag = argv.includes('--init-only')
const hasSdkUrl = /* 检查 SDK URL */

const isNonInteractive = hasPrintFlag
|| hasInitOnlyFlag
|| hasSdkUrl
|| !process.stdout.isTTY // ← 管道模式自动判定

setIsInteractive(!isNonInteractive)
flowchart TD Check{"检测交互模式"} Check --> TTY{"stdout.isTTY?"} Check --> Print{"有 -p / --print?"} Check --> SDK{"有 SDK URL?"} Check --> Init{"有 --init-only?"} TTY -- "false" --> NonInteractive["非交互模式
(Headless)"] Print -- "true" --> NonInteractive SDK -- "true" --> NonInteractive Init -- "true" --> NonInteractive TTY -- "true" --> Interactive["交互模式
(REPL)"] Print -- "false" --> Interactive

步骤三:提前加载设置

1
2
3
4
// main.tsx:815-854
initializeEntrypoint(isNonInteractive) // 标记入口类型
eagerLoadSettings() // 提前解析 --settings 和 --setting-sources
await run() // 进入 Commander.js 流程

eagerLoadSettings() 在 Commander.js 还没解析完参数时就手动扫描 argv,提前加载设置文件。为什么?因为后续的 init() 需要用到配置,如果等 Commander 解析完再加载,会晚几十毫秒。

工程思想 #3:急切加载(Eager Loading)

对于明确会用到的数据,不必等到"正式需要"时才去读取。通过手动提前扫描参数,打破了"参数解析 → 读配置"的串行依赖。


2.5 Phase 3:Commander 解析与 preAction 钩子

文件: main.tsxrun() 函数(第 884 行起)

Commander.js 程序定义

Claude Code 使用 Commander.js 做命令行参数解析。run() 函数的前半段定义了 30+ 个 CLI 选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.tsx:884-905 —— 简化示意
const program = new CommanderCommand()
.configureHelp(createSortedHelpConfig())
.enablePositionalOptions()
.option('-p, --print', '非交互模式')
.option('--output-format <format>', '输出格式')
.option('--model <model>', '模型选择')
.option('--permission-mode <mode>', '权限模式')
.option('--mcp-config <path>', 'MCP 配置文件')
.option('--system-prompt <prompt>', '自定义系统提示词')
.option('--verbose', '详细输出')
.option('--resume <id>', '恢复会话')
.option('--max-turns <n>', '最大轮次')
.option('--plugin-dir <path>', '插件目录')
// ... 30+ 个选项

preAction 钩子:串行依赖的汇合点

Commander.js 的 preAction 钩子在参数解析完成、Action Handler 执行之前触发。这是收集前面所有并行操作结果的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// main.tsx:907-967
program.hook('preAction', async () => {
// 1. 等待前面并行发起的 I/O 操作完成
await Promise.all([
ensureMdmSettingsLoaded(), // 等 MDM 配置(Phase 1 发起)
ensureKeychainPrefetchCompleted() // 等 Keychain 密钥(Phase 1 发起)
])

// 2. 执行核心初始化(memoized,只执行一次)
await init()

// 3. 初始化日志 sink
const { initSinks } = await import('./utils/sinks.js')
initSinks()

// 4. 运行数据迁移
runMigrations()

// 5. Fire-and-forget:开始加载远程配置(不等待)
void loadRemoteManagedSettings()
void loadPolicyLimits()
})
flowchart TD subgraph Parallel["Phase 1 并行发起"] MDM["startMdmRawRead()"] Key["startKeychainPrefetch()"] Imports["200+ import"] end subgraph PreAction["preAction 钩子(汇合点)"] Wait["Promise.all([
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.tsinit() 函数(第 57 行)

init() 是被 memoize() 包装的异步函数,无论调用多少次都只执行一次。它负责建立整个应用运行所需的基础设施。

初始化清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// init.ts:57-214 —— 简化后的执行序列
export const init = memoize(async () => {
// ① 配置系统
enableConfigs() // 验证并激活配置
applySafeConfigEnvironmentVariables() // 应用安全的环境变量
applyExtraCACertsFromConfig() // 注入自定义 CA 证书

// ② 生命周期
setupGracefulShutdown() // 注册 SIGINT/SIGTERM 处理

// ③ 分析与实验
await initialize1PEventLogging() // 第一方事件日志
await initGrowthBook() // Feature Flag 系统

// ④ Fire-and-forget(异步启动,不等待)
void populateOAuthAccountInfoIfNeeded() // OAuth 信息
void initJetBrainsDetection() // IDE 检测
void detectCurrentRepository() // Git 仓库检测

// ⑤ 网络层
configureGlobalMTLS() // 双向 TLS
configureGlobalAgents() // HTTP Proxy Agent
preconnectAnthropicApi() // TCP+TLS 握手预热 ← 重要!

// ⑥ 平台适配
setShellIfWindows() // Windows 下设置 git-bash

// ⑦ 清理注册
registerLspManagerCleanup() // LSP 退出清理
registerTeamCleanup() // 团队资源清理
})

preconnectAnthropicApi():TCP+TLS 预热

这是一个值得特别关注的优化。普通 HTTP 请求的流程是:

1
2
用户提交问题 → 建立 TCP 连接 → TLS 握手 → 发送请求 → 等待响应
←——— 约 100-200ms ———→

Claude Code 在初始化阶段就提前完成了 TCP+TLS 握手:

1
2
3
4
5
init() 阶段:    preconnectAnthropicApi() → TCP 连接 → TLS 握手 → 连接放入池中
←—— 和其他初始化并行执行 ——→

用户提交问题: 从连接池取出已建立的连接 → 直接发送请求 → 等待响应
←—— 省掉了 100-200ms ——→

工程思想 #5:连接预热(Connection Warming)

对于明确要访问的远程服务,在用户真正需要之前就完成网络握手。这和浏览器中 <link rel="preconnect"> 是同一个思想。

memoize 模式

init()memoize() 包装的原因是:多个代码路径都可能需要调用 init()(比如快速路径中的 --dump-system-prompt 也需要初始化配置),但初始化逻辑只应执行一次。

1
2
3
4
// memoize 的效果
init() // 第一次:执行全部逻辑,缓存结果
init() // 第二次:直接返回缓存的 Promise,零开销
init() // 第 N 次:同上

工程思想 #6:幂等初始化(Idempotent Init)

初始化函数应该是幂等的——调用一次和调用多次效果相同。memoize() 是实现幂等初始化的最简模式,比维护一个 initialized 布尔标志更安全(因为它还能正确处理并发调用的情况)。


2.7 Phase 5:信任边界与安全屏障

文件: interactiveHelpers.tsxshowSetupScreens() 函数

这是启动流程中最重要的安全节点。在信任对话框完成之前:

  • 不执行任何工具
  • 不启用遥测
  • 不发送任何数据

信任屏幕序列

flowchart TD Start["showSetupScreens()"] Start --> FirstTime{"首次使用?"} FirstTime -- "是" --> Onboard["显示引导页面
介绍功能和限制"] 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
2
3
4
5
6
7
8
9
10
11
// init.ts:247
export async function initializeTelemetryAfterTrust() {
// 对于远程配置用户:等待远程设置加载完成
if (isRemoteSettingsEligible()) {
await ensureRemoteManagedSettingsLoaded()
reapplyConfigEnvironmentVariables() // 重新应用(远程可能覆盖)
}

// 延迟加载 ~400KB 的 OpenTelemetry + Protobuf 库
await doInitializeTelemetry()
}

注意 OpenTelemetry 库有 ~400KB,如果在启动早期就加载,会拖慢整个启动速度。把它推迟到信任对话框之后加载,不仅是安全设计,也是性能优化。

工程思想 #7:信任前零泄漏(Zero Leakage Before Trust)

在用户显式授权之前,不发送任何数据(包括遥测)。这不仅是隐私合规要求,更是建立用户信任的基础。信任对话框是一个安全闸门——闸门前后是完全不同的权限域。


2.8 Phase 6:状态树构建与工具注册

文件: main.tsx(Action Handler),state/tools.ts

通过信任屏障后,开始构建应用运行所需的核心状态。

AppState 状态树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// state/AppStateStore.ts:89 —— 简化后的 AppState 类型
type AppState = DeepImmutable<{
// UI 状态
verbose: boolean
briefMode: boolean
statusLine: StatusLineConfig

// 模型配置
mainLoopModel: ModelSetting
effort: EffortLevel

// 权限
toolPermissionContext: ToolPermissionContext
denialTracking: Map<string, number>

// MCP
mcp: {
clients: McpClient[]
tools: McpTool[]
commands: McpCommand[]
resources: McpResource[]
}

// 插件
plugins: {
enabled: Plugin[]
disabled: Plugin[]
commands: Command[]
}

// 任务
tasks: Record<string, TaskState>

// 会话
fileHistory: Set<string>
settings: SettingsJson
}>

状态创建流程

flowchart TD CLI["CLI 参数
(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// tools.ts:193 —— getAllBaseTools()
function getAllBaseTools(): Tools {
return [
AgentTool, // 子 Agent
TaskOutputTool, // 任务输出
BashTool, // Shell 执行
FileReadTool, // 读文件
FileEditTool, // 编辑文件
FileWriteTool, // 写文件
GlobTool, // 文件名搜索
GrepTool, // 内容搜索
WebFetchTool, // HTTP 请求
WebSearchTool, // Web 搜索
SkillTool, // 技能执行
// ... 40+ 工具
]
}

// tools.ts:271 —— getTools() 过滤逻辑
function getTools(permissionContext): Tools {
if (process.env.CLAUDE_CODE_SIMPLE) {
// 极简模式:只保留 3 个核心工具
return [BashTool, FileReadTool, FileEditTool]
}

return getAllBaseTools()
.filter(tool => !getDenyRuleForTool(tool, permissionContext)) // 排除被拒绝的
.filter(tool => tool.isEnabled?.() !== false) // 排除被禁用的
}

// tools.ts:345 —— assembleToolPool() 合并内置 + MCP 工具
function assembleToolPool(permissionContext, mcpTools): Tools {
const builtinTools = getTools(permissionContext)
return [...builtinTools, ...mcpTools] // 内置工具在前,MCP 工具在后
// 固定顺序 → 利于 LLM 的 prompt cache
}

工程思想 #8:固定工具顺序促进缓存(Stable Order for Cache)

工具列表的顺序刻意保持固定。因为工具描述会序列化到系统提示词中,发送给 LLM API。如果顺序不变,API 端可以复用提示词缓存(prompt cache),显著降低延迟和成本。这是一个容易忽略但影响巨大的优化。

MCP 服务器发现

1
2
3
4
5
6
7
8
9
10
// services/mcp/config.ts —— MCP 配置来源
function getClaudeCodeMcpConfigs(): McpConfig[] {
return merge(
globalConfig.mcpServers, // ~/.claude.json
projectConfig.mcpServers, // .claude/config.json
settings.mcpServers, // settings.json 系列
getPluginMcpServers(), // 插件提供的
fetchClaudeAIMcpConfigsIfEligible() // claude.ai 远程配置
)
}

MCP 服务器的实际连接不在这个阶段完成。配置只是被收集起来,真正的连接在 REPL 挂载后,由 useManageMCPConnections() Hook 异步建立。


2.9 Phase 7:React 组件树挂载与 REPL 启动

文件: ink.tsreplLauncher.tsxcomponents/App.tsxscreens/REPL.tsx

这是启动链路的最后一步——把一切"连接"起来,渲染到终端。

组件树结构

flowchart TD Root["Ink Root
(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 第一步:创建 Ink 终端 React 根
// ink.ts:25
const root = await createRoot(options)

// 第二步:挂载组件树
// replLauncher.tsx:12
async function launchRepl(root, appProps, replProps, renderAndRun) {
const { App } = await import('./components/App.js') // 延迟加载
const { REPL } = await import('./screens/REPL.js') // 延迟加载

await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>)
}

// 第三步:渲染并等待退出
// interactiveHelpers.tsx:~20
async function renderAndRun(root, element) {
root.render(element) // 渲染到终端
startDeferredPrefetches() // 启动延迟预取(UI 已就绪后)
await root.waitUntilExit() // 阻塞直到用户退出
await gracefulShutdown(0) // 优雅退出
}

延迟预取(Deferred Prefetches)

startDeferredPrefetches() 在 UI 渲染完成后才执行。这些操作不影响首屏渲染,但能加速后续的第一次用户交互:

1
2
3
4
5
6
7
8
9
// 延迟预取的内容
startDeferredPrefetches() → {
initUser() // 初始化用户身份
getUserContext() // 加载用户上下文
getSystemContext() // 加载系统上下文
countFilesRoundedRg() // 统计项目文件数
watchSettingsChanges() // 监听设置文件变化
watchSkillChanges() // 监听技能文件变化
}

工程思想 #9:分级加载(Tiered Loading)

将初始化工作分为三个优先级:

  1. 立即执行:快速路径判断、环境变量设置
  2. UI 就绪前:配置、认证、权限、网络——用户交互所必需
  3. UI 就绪后:上下文预取、文件统计、变化监听——提升后续体验

只有优先级 1 和 2 的工作影响启动速度,优先级 3 的工作在后台静默完成。


2.10 非交互模式:Headless 分支

当检测到非交互模式(-p 标志、非 TTY 等),启动流程在 Phase 6 之后走完全不同的分支:

flowchart TD Phase6["Phase 6: 状态与工具就绪"] Phase6 --> Check{"交互模式?"} Check -- "交互式" --> REPL["Phase 7: 挂载 React
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
3
4
# 管道模式示例
echo "解释这段代码" | claude -p
cat error.log | claude -p "分析这个错误日志"
claude -p --output-format json "列出 src/ 下所有 TODO"

2.11 配置系统:多源合并的层级模型

Claude Code 的配置来自7 个不同来源,按优先级从高到低合并:

flowchart TD subgraph Priority["优先级(从高到低)"] direction TB P1["① 组织策略
(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 的"项目记忆"机制。它在启动过程中被发现、加载、注入到系统提示词。

文件发现规则

flowchart TD subgraph Discovery["CLAUDE.md 发现顺序(优先级从低到高)"] direction TB L1["① /etc/claude-code/CLAUDE.md
(Managed 全局指令)"] L2["② ~/.claude/CLAUDE.md
~/.claude/rules/*.md
(User 个人指令)"] L3["③ /CLAUDE.md
/.claude/CLAUDE.md
/.claude/rules/*.md
(Project 项目指令)"] L4["④ /CLAUDE.md
/.claude/CLAUDE.md
/CLAUDE.local.md
(Local 本地指令)"] L1 ~~~ L2 ~~~ L3 ~~~ L4 end Note["后加载的文件在 prompt 中
位置更靠后 → 模型注意力更强"] L4 --> Note

加载机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// utils/claudemd.ts:790
export const getMemoryFiles = memoize(async (forceIncludeExternal) => {
const files: MemoryFile[] = []

// 1. Managed 层(/etc/claude-code/CLAUDE.md)
files.push(...loadManagedMemory())

// 2. User 层(~/.claude/)
files.push(...loadUserMemory())

// 3. 从 CWD 向上遍历到根目录
const dirs = walkUpFromCwd()
for (const dir of dirs.reverse()) { // 从根向 CWD 遍历(保证 CWD 最后加载)
files.push(...loadProjectMemory(dir))
}

// 4. 处理 @include 指令(递归展开引用)
await resolveIncludes(files)

return files
})

@include 指令允许 CLAUDE.md 引用其他文件:

1
2
3
4
5
6
<!-- CLAUDE.md 中的 @include 示例 -->
请遵循以下编码规范:
@./coding-standards.md

API 文档参见:
@~/shared-docs/api-reference.md

注入到系统提示词

加载的 CLAUDE.md 内容最终被包装在 <system-reminder> 标签中,注入到每次 API 调用的系统提示词:

1
2
3
4
5
6
7
8
9
10
11
系统提示词结构:
├── 核心系统提示词(角色、工具说明、行为规则)
├── <system-reminder>
│ ├── # claudeMd
│ │ ├── CLAUDE.md (Managed)
│ │ ├── CLAUDE.md (User)
│ │ ├── CLAUDE.md (Project)
│ │ └── CLAUDE.md (Local) ← 最后加载 = 最高注意力
│ ├── # currentDate
│ └── # gitStatus
└── 用户消息

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
时间  0ms                  135ms              300ms           500ms+
│ │ │ │
▼ ▼ ▼ ▼
cli.tsx main() init() REPL
快速路径判断 进入 初始化 渲染
│ │ │ │
├─ --version? → 立即返回 ├─ 交互检测 ├─ 配置激活 ├─ createRoot
├─ --mcp? → MCP 模式 ├─ eagerLoad ├─ mTLS+Proxy ├─ App + REPL
└─ 正常 → import main ├─ Commander ├─ API 预连接 ├─ 延迟预取
├─ MDM 预取 ───────────┤ │ │
├─ Keychain 预取 ──────┤ │ │
└─ 模块加载 ───────────┘ │ │
preAction ────────→│ │
Promise.all 汇合 信任对话框 ────→│

用户可交互

下一步

  • 第三章:Agent 循环深度解析——query.ts 的 1729 行如何驱动整个 Agent
  • 第四章:斜杠命令系统——/resume、/clear、/compact 的完整实现剖析

本文档基于 Claude Code 源码分析生成,源码路径:claude-code/src/