第二章:启动、信任与宿主分流
拆开启动引导、session setup、trust 闸门和宿主模式分流,看 Claude Code 怎么真正活起来。
1. 为什么这一块必须单独拆
如果把 Claude Code 只当成一个 CLI,很容易下意识地把启动过程想成:
- 解析参数
- 初始化一下配置
- 进入 REPL 或直接跑一轮
但源码真正做的事情要复杂得多。
Claude Code 的启动层不是一个普通入口,而是一套:
带快路径、带安全闸门、带宿主分流、带会话成形步骤的 runtime bootstrap pipeline。
这块对你以后做 C# 版非常重要,因为它直接决定:
Program.cs要不要只做薄分发- 哪些初始化能在 trust 之前做,哪些绝不能做
- REPL、headless、remote、bridge、daemon 应该在哪一层分叉
SessionBootstrapper、SessionSetup、HostRouter、TrustGate怎么拆
相关源码锚点:
src/entrypoints/cli.tsxsrc/main.tsxsrc/entrypoints/init.tssrc/setup.tssrc/interactiveHelpers.tsx
2. 先说结论
我对这套设计的判断是:
Claude Code 的启动不是单阶段,而是“五段式”。
cli.tsx做极薄的快路径分发,尽量不加载整套 runtime。main.tsx顶层 做必须最早发生的 side effect 和 argv 改写。init.ts做“安全早初始化”,但故意不越过 trust 边界。setup.ts做会话落地,包括 cwd、worktree、socket、session memory、hook 快照。main.tsxaction handler 在 trust、resume、MCP、宿主模式这些条件齐备之后,决定进哪个运行壳。
这里最值钱的一点是:
Claude Code 明确把“初始化 runtime”与“允许执行本地不受信代码”分开了。
3. 总体结构图
flowchart TD
A["cli.tsx<br/>快路径入口"] --> B["main.tsx 顶层<br/>side effects + argv rewrite"]
B --> C["main()<br/>判定 interactive / non-interactive"]
C --> D["Commander preAction"]
D --> E["init.ts<br/>安全早初始化"]
E --> F["main.tsx action handler<br/>解析大部分业务参数"]
F --> G["setup.ts<br/>会话落地"]
G --> H{"interactive?"}
H -- "是" --> I["showSetupScreens()<br/>onboarding + trust gate"]
I --> J["LSP / settings dialog / startup prefetch / MCP connect"]
J --> K{"fresh / continue / resume / remote / ssh / assistant"}
K --> L["launchRepl()"]
H -- "否" --> M["apply full env<br/>headless AppState<br/>MCP connect"]
M --> N["runHeadless()"]
A --> O["bridge / daemon / bg / runner<br/>特种快路径直接返回"]
这张图里最关键的一点是:
真正的业务分流不是在 cli.tsx 一次完成,而是在不同阶段分层完成。
4. cli.tsx 是快路径分发器,不是完整入口
源码锚点:
src/entrypoints/cli.tsx
cli.tsx 做得非常克制。
它不会一上来就 import './main',而是先处理一批对启动时延特别敏感的路径:
--version--dump-system-prompt--claude-in-chrome-mcp--chrome-native-host--computer-use-mcp--daemon-workerremote-control/rcdaemonps / logs / attach / kill / --bgenvironment-runnerself-hosted-runner--worktree --tmux的早切换
这些路径的共同点是:
- 要么不需要整套主 runtime
- 要么特别追求启动速度
- 要么本身就是另一种宿主
所以它用了大量 dynamic import,把“完整 CLI 启动”变成最后的兜底路径。
4.1 这层最值钱的设计
我觉得这里最值得学的是:
把入口当成 dispatcher,而不是所有模式共用的统一巨入口。
如果以后你做 C# 版,这一层完全可以拆成:
FastPathRouterSpecialModeRouterDefaultCliEntry
而不是让 Main() 一把梭把所有模式都初始化一遍。
5. main.tsx 顶层先做“必须最早发生”的事
源码锚点:
src/main.tsx文件顶部
main.tsx 在 import 阶段就做了几件特别有代表性的事:
profileCheckpoint('main_tsx_entry')startMdmRawRead()startKeychainPrefetch()
注释写得很明确:
- MDM 读取要尽早并行跑
- macOS keychain 读取要尽早并行跑
- 这些都是为了压低后面真正初始化时的等待
这说明 Claude Code 在启动设计上很成熟:
它会把慢 I/O 尽可能前移并行,但只前移那些“安全且确定会需要”的部分。
5.1 这一层还做 argv rewrite
在真正进入 Commander 之前,main.tsx 还会提前改写参数,典型例子有:
cc:///cc+unix://--handle-uriassistant [sessionId]ssh <host> [dir]
这些逻辑不是简单语法糖,而是为了把不同来源的启动方式统一导向同一套主会话路径。
比如:
- 交互模式的
cc://会被改写成主命令路径,拿到完整 TUI -p模式的cc://会被改写成内部open子命令
这说明它在做的是:
输入归一化。
6. main() 很早就先决定“这是不是交互会话”
源码锚点:
src/main.tsx:797左右
Claude Code 很早就根据这些条件判定是不是 non-interactive:
-p / --print--init-only--sdk-urlstdout不是 TTY
然后立刻做几件事:
- 停止 early input capture
setIsInteractive(...)initializeEntrypoint(...)- 推导
clientType
这里很值钱,因为它说明:
interactive / non-interactive 不是 UI 层概念,而是启动期的全局分叉信号。
后面很多行为都跟这个位直接绑定:
- 要不要显示 trust dialog
- 要不要跳过 subcommand 注册
- 要不要把 telemetry / env / hooks 提前做完
- REPL 还是
runHeadless()
7. init.ts 是“安全早初始化”,不是完整 setup
源码锚点:
src/entrypoints/init.ts
init() 做的事情很多,但非常克制。
它主要负责:
enableConfigs()applySafeConfigEnvironmentVariables()applyExtraCACertsFromConfig()setupGracefulShutdown()- 初始化 1P event logging
- 预填 OAuth account info
- JetBrains 检测
- repository detection
- 提前创建 remote managed settings / policy limits loading promise
configureGlobalMTLS()configureGlobalAgents()preconnectAnthropicApi()- remote 模式下启动 upstream proxy
- Windows shell 处理
- 注册 LSP cleanup
- 初始化 scratchpad 目录
7.1 它为什么值钱
因为它明确只做两类事:
- 全局性、基础设施性、早做收益大的事情
- 不会越过 trust 边界的事情
比如它只应用 safe env vars,不会在这个阶段就把所有危险环境变量都吃进去。
7.2 telemetry 也被拆成 trust 前后两段
initializeTelemetryAfterTrust() 不是在 init() 里直接做,而是等 trust 通过之后再跑。
而且如果有 remote managed settings,还会等它们先落下来,再重新 apply env,再初始化 telemetry。
这说明 Claude Code 对启动顺序是有严格语义的,不是“能早就早”。
8. preAction hook 把 Commander 变成真正的 bootstrap 入口
源码锚点:
src/main.tsx里的program.hook('preAction', ...)
Claude Code 没把 init() 写在 main() 一开头,而是挂到 Commander 的 preAction 上。
这个设计很值钱。
因为它意味着:
- 只有真正执行命令时才初始化
- 单纯
--help不会把整套系统跑起来 - 所有默认命令和子命令都能复用同一套前置初始化逻辑
preAction 里做的核心事情包括:
- 等 MDM / keychain 预取完成
- 调
init() - 初始化 logging sinks
- 处理
--plugin-dir - 跑 migrations
- 非阻塞启动 remote managed settings / policy limits
- 非阻塞启动 settings sync
也就是说:
Claude Code 把“CLI 参数框架”和“真正的 bootstrap 生命周期”结合得很紧。
9. setup.ts 负责把“一个进程”变成“一个会话”
源码锚点:
src/setup.ts
setup() 不是初始化全局环境,而是把当前这次运行真正落成一个 session。
它做的事情包括:
- 校验 Node 版本
- 切 session ID
- 启动 UDS messaging server
- 捕获 teammate mode snapshot
- 恢复 iTerm / Terminal backup
setCwd(cwd)- 捕获 hook config snapshot
- 初始化 file-changed watcher
- 处理
--worktree - 初始化 session memory
- 初始化 context collapse
- 预热插件命令 / plugin hooks
- 安装 attribution / session-file-access / team memory 之类后台钩子
- attach sinks
- 发
tengu_started - 做 bypassPermissions 的安全校验
9.1 这里最重要的设计分工
setup() 最大的价值是把这些事情单独收口:
- 会话 cwd
- 会话 worktree
- 会话 sidecar 基础设施
- 会话级后台 watcher / socket / hook snapshot
这使得 init() 和 setup() 的职责非常清楚:
init()管“进程准备好没有”setup()管“这个 session 成形没有”
9.2 --worktree 说明它不是浅层参数
setup() 里对 --worktree 的处理非常重:
- 可能切到 canonical git root
- 可能创建新 worktree
- 可能创建 tmux session
- 可能修改 cwd / projectRoot / originalCwd
- 会保存 worktree state
- 会刷新 hooks snapshot 和 memory file cache
这说明 --worktree 不是 UI 偏好,而是:
会影响整个 session 语义的启动模式。
10. trust 不是权限模式的附属物,而是独立启动闸门
源码锚点:
src/interactiveHelpers.tsx:104src/main.tsx:2239
showSetupScreens() 里写得非常清楚:
- trust dialog 在 interactive session 里总是要看
- 它和
bypassPermissions不是一回事 - non-interactive session 根本不会走这里
它在 trust 通过之后才会做这些事:
setSessionTrustAccepted(true)- 重置并重新初始化 GrowthBook
getSystemContext()- 处理
.mcp.json审批 - 检查
CLAUDE.md外部 include applyConfigEnvironmentVariables()initializeTelemetryAfterTrust()
这个分层非常值钱,因为它清楚地区分了:
- workspace trust
- tool permission mode
- org policy
三种完全不同的治理层。
11. main.tsx 的 action handler 才是真正的 orchestration hub
源码锚点:
src/main.tsx默认 action handler
真正复杂的事情,其实都在 action handler 里。
这一段会统一处理:
- 系统 prompt 读取
--resume/--continue/--fork-session--remote/--teleport--sdk-url--chrome- 动态 MCP config
- channels
--tools/ permissions--agent- model / effort / advisor
inputPrompt- hooks
- MCP configs
- REPL 与 headless 分流
也就是说,Claude Code 的核心启动编排层不是 Commander,也不是 init(),而是这里。
12. 它的模式分流不是一刀切,而是分层分叉
我把这份源码里的启动分叉,大致归成下面几层。
12.1 第 1 层:cli.tsx 快路径分流
处理:
- version
- bridge server
- daemon
- bg sessions
- runners
特点:
- 追求快
- 追求少 import
- 不进入默认主会话路径
12.2 第 2 层:main.tsx argv 改写分流
处理:
cc://assistantssh- deep link
特点:
- 统一输入形态
- 为后面拿完整 TUI / 主会话语义做准备
12.3 第 3 层:interactive / non-interactive 分流
这是启动期最关键的一刀。
interactive
走:
showSetupScreens()- trust gate
launchRepl()
non-interactive
走:
- full env 直接应用
- headless AppState
- MCP 预连接
runHeadless()
而且在 non-interactive 下,Claude Code 还会专门优化:
-p模式跳过 subcommand 注册- 提前 kick
getSystemContext()/getUserContext() - 把 MCP 连接尽量在单轮前做完
12.4 第 4 层:interactive 内部再按宿主分流
这层主要有:
- fresh local REPL
--continue--resume- direct connect
- SSH remote
- assistant viewer
- remote session TUI
- teleport resume
这些路径最后大多会收敛到:
launchRepl()
但它们的 session config、initial messages、remote session config、resume data 都不同。
这就是一个很典型的:
同一宿主壳,不同 session backend。
13. -p/--print 不是“小模式”,而是另一种宿主
源码锚点:
src/main.tsx:2584左右
很多人看到 --print 会以为这只是“别开 REPL,打印一下”。
但 Claude Code 明显不是这样设计的。
在 --print 路径里,它会:
- 应用 full env
- 初始化 telemetry
- 单独处理 startup hooks
- 构造 headless
AppState - 建
headlessStore - 连接 MCP
- 启动 deferred prefetch
- 动态 import
print.ts - 调
runHeadless(...)
也就是说:
headless 是正式宿主,不是 REPL 的简化版。
14. 为什么它要跳过 print 模式下的 subcommand 注册
源码锚点:
src/main.tsx:3875左右
这里的注释非常有代表性:
-p/--print 下,52 个 subcommands 根本不会被调度到,注册它们只是额外成本。
所以 Claude Code 在 print 模式下直接跳过整段 subcommand 注册。
这说明作者非常在意:
- 启动链里哪些工作是真需要的
- 哪些只是“框架上看着统一”,但实际上纯浪费
这个思路对 C# 版也很值钱:
别因为代码结构统一,就让所有宿主都付同样的启动成本。
15. resume / remote / ssh / assistant 说明它在做“backend polymorphism”
从 main.tsx 后半段能看出一个很清晰的设计方向:
--continue/--resume走本地 transcript 恢复cc:///open走 direct connectssh走远程 CLI + 本地代理assistant走 viewer-only remote session--remote/--teleport走 CCR session
这些模式都不是单独实现一套 UI,而是尽量把差异收敛在:
- session creation
- auth / transport
- initial messages
- session config
最后依旧交给 REPL 或 headless 宿主壳。
所以 Claude Code 真正在做的是:
多 backend、少宿主。
16. 这一层对 C# 版最值钱的抽象
如果要迁到 C#,我建议直接把这套启动逻辑拆成下面几层:
16.1 FastPathEntryRouter
负责:
--version- daemon / worker
- bridge server
- bg session tools
- 其他不需要主 runtime 的路径
16.2 BootstrapPrelude
负责:
- 最早 side effect
- keychain / policy / MDM 之类预取
- argv rewrite / deep link 归一化
16.3 SafeInitializer
对应 init.ts,负责:
- 配置系统
- safe env
- network agent / proxy / mTLS
- graceful shutdown
16.4 SessionSetup
对应 setup.ts,负责:
- cwd / projectRoot / worktree
- session socket / session memory / hook snapshot
- session-level background infra
16.5 TrustGate
把下面这些事收在一起:
- interactive trust dialog
- full env apply
- remote settings / telemetry after trust
- trust 通过后的审批和 warning
16.6 HostRouter
负责最终把 session 分发到:
ReplHostHeadlessHostRemoteViewerHostDirectConnectHostSshHost
16.7 SessionBootstrapContext
建议在 C# 版里显式做一个上下文对象,把这些易散落的字段收在一起:
IsInteractiveClientTypeEntrypointSessionSourceTrustStateHostModeResumeModeRemoteModeWorktreeMode
Claude Code 现在是分散在 bootstrap state、env、CLI options 里的。 你可以把它再抽干净一层。
17. 最重要的一条判断
到这里我对这块的最终判断是:
Claude Code 的启动层,本质上不是 CLI glue code,而是一个“受信边界感知的多宿主 runtime bootstrapper”。
这也是这份源码很值钱的地方:
- 它没有把所有初始化塞到一个
main() - 它没有把 trust、permission、resume、host mode 混成一团
- 它没有为每种运行形态复制一套 runtime
如果以后你要做 C# 版,我非常建议直接继承它这条设计主线:
FastPathRouter -> SafeInitializer -> SessionSetup -> TrustGate -> HostRouter。