乐于分享
好东西不私藏

ClaudeCode源码详解01-进入Agent主循环前,Claude Code启动时都做了什么

ClaudeCode源码详解01-进入Agent主循环前,Claude Code启动时都做了什么

提前总结版

在进入Agent循环之前需要进行环境准备,这个属于通用逻辑。

像这里的claude code在进入Agent循环之前,也是渲染界面、主题配置、准备Command、读取机器token信息等。统一归纳为环境准备工作,对应着CC代码就是创建Commander、init函数、读取MDM、读取Keychain、是否信任文件、主题配置等。这些事情对于Agent本身的能力并不会有什么提升。对应文件为cli.tsx跟main.tsx。

本文主要是理解一下在进入Agent运行时之前CC到底在做什么以及一些其中的功能与专业术语。

CC执行链路是什么样子

从启动claude,用户输入到最终模型响应,链路大概是这样:

  1. 先进入真正的入口文件 src/entrypoints/cli.tsx

  2. cli.tsx 分发到  src/main.tsx

  3. 对话交互模式下经历 trust / onboarding 等前置流程,然后把渲染终端界面,进入 REPL

  4. 用户真正提交第一条消息后,才会调用 query(),进入 Agent 的主循环

  5. 每次迭代:压缩上下文 → 组装 system prompt → 调用模型 API → 处理工具调用 → 继续或退出

在本文当中,我们主要讲一下在用户真正提交信息进行agent循环之前,做了哪些事情

先说几个容易混淆的点

1. main.tsx 不是“主循环本体”

src/main.tsx 更准确的定位是:

  • CLI 总调度入口

  • 负责注册命令和参数

  • 负责初始化运行时

  • 负责决定当前是交互模式、非交互模式、恢复会话、远程控制还是其它分支

  • 在合适的时候把 REPL 渲染出来

真正的 Agentic Loop 在 src/query.ts 的 queryLoop() 里,而不是 main.tsx

2. launchRepl() 不是“开始跑 Agent”

src/replLauncher.tsx 里的 launchRepl() 很短,本质上只是:

  • 动态加载 App

  • 动态加载 REPL

  • 调用 renderAndRun() 把界面挂到 Ink root 上

它做的是“把 REPL 画出来”,不是“开始跑主循环”。

真正进入模型调用,是在 REPL 内部收到用户输入后,才会进一步进入 query()

cli.tsx 做了哪些事情

src/entrypoints/cli.tsx 不是简单地 import('./main.tsx') 然后执行。它会先检查命令行参数,判断是否能走“轻量路径”。

典型分支包括:

  • --version:直接输出版本号,不加载完整 CLI

  • remote-control / rc:走远程控制 bridge 相关逻辑

  • daemon:进入守护进程模式

  • ps / logs / attach / kill:进入后台会话管理

  • --worktree + --tmux:直接走 worktree/tmux 快路径

只有这些分支都没命中,才会继续进入完整 CLI。

从性能角度看,这一层非常重要,因为很多命令根本不需要把“Agent 运行时”整个拉起来。

我们能学到什么

不需要调用Agent运行时的都提前进行处理,降低响应时间,其实大家应该都是这么做的

main.tsx 主要负责什么

如果说 cli.tsx 是前置分发层,那么 src/main.tsx 才是完整 CLI 的总调度器。

它主要做四件事:

  1. 注册命令(例如plugin命令)和参数(例如–agents)

  2. 在 preAction 中完成通用初始化

  3. 进入默认 action 后(就是执行claude命令),决定当前启动路径

  4. 在交互模式下创建 UI 并启动 REPL

main.tsx 负责把 Claude Code CLI 这台机器“装起来”,但真正持续运转的 Agent 循环在后面的 query() 里。

什么是 REPL

REPL 是 Read-Eval-Print Loop 的缩写。

放在 Claude Code 里,可以简单理解成:

  • 终端里输入一句话

  • 系统处理

  • 输出结果

  • 继续等待下一次输入

不过这里的 “Eval” 并不只是执行一段表达式,而是一次完整的 Agent 过程:模型采样、工具调用、结果回写、必要时继续下一轮。

为什么要创建 Commander 程序对象

源码里有这样一段:

constprogram=newCommanderCommand().configureHelp(createSortedHelpConfig()).enablePositionalOptions();...program.option('-w, --worktree [name]''Create a new git worktree for this session (optionally specify a name)');program.option('--tmux''Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux.');...program.addOption(newOption('--remote-control [name]''Start an interactive session with Remote Control enabled (optionally named)').argParser(value=>value||true).hideHelp());...constmcp=program.command('mcp').description('Configure and manage MCP servers').configureHelp(createSortedHelpConfig()).enablePositionalOptions();

后面会在 program 上不断挂载各种 option 和 subcommand,比如:

  • mcp

  • plugin

  • doctor

  • --worktree

  • --tmux

  • --remote-control

它的意义并不只是“解析一下 argv”,而是把整个 CLI 的命令树描述出来。指明Claude能用哪些命令,哪些参数。

为什么要在执行命令前挂一个 preAction hook

在 main.tsx 里,程序会注册一个 program.hook('preAction', async ...)

  program.hook('preAction', async thisCommand => {    ...    # 先等待Mdm配置加载以及Keychain提取完成    await Promise.all([ensureMdmSettingsLoaded(), ensureKeychainPrefetchCompleted()]);    # 通用初始化    await init();    # 初始化日志 sink    initSinks();...}

这个 hook 的关键意义是:

只要真正要执行某个命令,就先把通用初始化做完;但如果只是 --help 之类的展示场景,就不必支付这部分成本。

这个 hook 里至少会做三件重要的事:

  1. 等待 MDM 配置和 Keychain 预取完成

  2. 调用 init()

  3. 初始化日志 sink、迁移、远程配置等公共能力

这种做法的好处是:

  • 避免重复初始化逻辑散落在每个命令里

  • 确保默认命令和子命令有一致的基础运行环境

  • 又不会影响纯帮助输出的速度

init() 并不负责“启动 REPL”,它负责“启动底座”

init() 的职责更接近于“把运行时底座准备好”,而不是“开始具体业务流程”。

从源码能看到,它会处理的事情包括:

  • 启用配置系统

  • 应用安全相关环境变量

  • 处理证书、网络、代理等运行时配置

  • 设置优雅退出

  • 启动一些预热和探测任务

例如:

exportconstinit=memoize(async (): Promise<void>=> {enableConfigs()applySafeConfigEnvironmentVariables()applyExtraCACertsFromConfig()voidpopulateOAuthAccountInfoIfNeeded()setupGracefulShutdown()voidinitJetBrainsDetection()voiddetectCurrentRepository()preconnectAnthropicApi()})

所以可以把它理解成:

  • init() 负责地基

  • 后面的 action / setup / REPL 负责真正进入使用态

MDM 和 Keychain 在这里分别是什么

这里用一句话解释就够了:

  • MDM:企业托管配置

  • Keychain:系统级凭据读取,主要是 macOS 上的认证信息缓存或预取

源码把这两个动作提前做,本质上是为了降低后续首次读取配置、认证或策略信息时的延迟。

默认命令是怎么进入交互模式的

如果用户没有走 doctormcpplugin 这些显式子命令,而是直接执行:

claude

那么会命中顶层默认命令的 .action(async (prompt, options) => { ... })

program.name('claude').description(`Claude Code - starts an interactive session by default, use -p/--print for non-interactive output`).argument('[prompt]''Your prompt'String).action(async (promptoptions=> {profileCheckpoint('action_handler_start');}

这里才是“普通用户进入 Claude 对话”的主入口。

但需要注意,这个 action 里做的事情远不止“调用 launchRepl()”。

进入交互模式下的前置动作有哪些

1. 创建 Ink root

如果当前不是非交互模式,代码会先创建终端 UI 的 root:

const { createRoot } =awaitimport('./ink.js')root=awaitcreateRoot(ctx.renderOptions)

这一步的意义是:先把终端 UI 容器搭起来,后面很多初始化流程都可能需要弹界面、显示对话框或 onboarding。

2. 执行 showSetupScreens()

紧接着会调用:

constonboardingShown=awaitshowSetupScreens(root,permissionMode,allowDangerouslySkipPermissions,commands,enableClaudeInChrome,devChannels)

这个函数会在 REPL 渲染前,依次检查是不是需要先显示一些“前置页面”,一般都是首次启动引导,完成一些初始配置,例如主题等

3. 最后才调用 launchRepl()

等上述准备工作都结束之后,交互模式才会走到:

awaitlaunchRepl(rootappPropsreplPropsrenderAndRun)

这里非常适合点明一句:

到这一步,Claude Code 只是“进入了可交互状态”,还没有正式跑进 queryLoop()

4. launchRepl() 到底做了什么

launchRepl() 的实现非常短:

exportasyncfunctionlaunchRepl(...) {const { App } =awaitimport('./components/App.js')const { REPL } =awaitimport('./screens/REPL.js')awaitrenderAndRun(root,<App {...appProps}><REPL {...replProps/></App>  )}

这段代码传达的信息很明确:

  • App 负责上层 provider 和全局状态包装

  • REPL 才是真正的交互界面

  • launchRepl() 的职责是把这两个组件挂起来

所以严格来说,它启动的是“交互界面”,不是“Agent 主循环”。