ARTICLE · 1107282
Claude Code 源码阅读:从启动入口建立全局地图-理解 cli.tsx 与 main.tsx 的职责边界
如果你刚开始读 Claude Code 源码,非常建议第一步只看两个文件:src/src/entrypoints/cli.tsx 和 src/src/main.tsx。
原因很简单:这一轮的目标不是搞懂所有实现,而是先拿到“启动地图”。入口层速读只需要回答四个问题:
程序最早从哪里开始执行? 轻量入口和完整 CLI 是怎么分流的? 哪些情况下会提前退出? 最终在哪里接到主执行流程?
这篇文章就围绕这四个问题展开,不扩展到 Agent Loop,不展开工具系统,也不追进具体命令实现。
一、程序从哪里开始
最早的启动入口在 cli.tsx:33-41。
这里定义了一个 async function main(),先取 process.argv.slice(2),也就是用户真正传进来的命令行参数。紧接着,它先判断是不是最简单、最常见、最值得快速返回的路径:--version、-v、-V。如果是,就直接打印版本号然后 return。
这一步非常关键。它说明 Claude Code 的作者没有把“所有启动都先进完整 CLI”当成默认策略,而是先在最外层做了一层很薄的分流壳。这个壳的职责不是承载完整业务,而是用最低成本判断:这次调用到底只是一个快速查询,还是值得继续加载整套 CLI 运行时。
所以,第一个问题的答案可以先记成一句话:程序从 cli.tsx 的轻量 main() 开始,而不是一上来就进入完整 CLI。
二、轻量入口和完整 CLI 的分流
cli.tsx 的核心价值,不是“做很多事”,而是“尽量少做,但把方向分对”。
你可以把它理解成机场里的分流大厅。到了这里,系统先判断你是直接查信息、去专用通道,还是需要进入完整航站楼。
1)最轻的快速路径:版本号
在 cli.tsx:36-41,--version 是零额外模块加载的快速路径。命中以后,直接输出版本并返回。
// Fast-path for --version/-v: zero module loading needed if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) { // MACRO.VERSION is inlined at build time // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`${MACRO.VERSION} (Claude Code)`); return; }这说明入口层非常在意启动成本:能不加载别的模块,就不加载。
2)专用快速路径:先处理,处理完就退出
接下来 cli.tsx 还处理了一串专用入口,比如:
--dump-system-prompt,用于输出 system prompt 并退出,见 cli.tsx:50-70 --claude-in-chrome-mcp、 --chrome-native-host、--computer-use-mcp,这些会走到各自的专用 server / host 逻辑,执行完就返回,见 cli.tsx:72-92--daemon-worker,这是内部 worker 的轻量入口,执行对应 worker 后返回,见 cli.tsx:95-105 remote-control/ rc/remote/sync/bridge这类桥接模式入口,也是在入口层先判断,再进入对应桥接主流程,见 cli.tsx:108-161
这一段最好不要死背每个分支名称,你真正要抓住的是它们的共同模式:凡是可以独立成立的专用路径,入口层就地分流;只要命中,就不再落回通用 CLI 主流程。
这也解释了为什么这个文件叫 entrypoint。它不是“完整功能实现层”,而是“把不同启动意图分送到不同执行轨道的地方”。
三、提前退出的情况
在 cli.tsx:164-184,cli.tsx 还有两段快速返回逻辑:是按“为什么会提前退出”来分类。
第一类:信息型快速返回
最典型的是版本号输出。用户只是想知道当前版本,没有必要继续初始化完整运行时,所以打印后马上结束。
--dump-system-prompt 也是同一类:它要的是一个可直接输出的结果,而不是启动交互式 CLI。所以在入口层完成工作后直接退出,最合理。
第二类:专用子系统自己接管
像 Chrome Native Host、Claude in Chrome MCP、computer-use MCP、daemon worker、bridge/remote-control 这类路径,本质上都不是“先进入通用 CLI,再顺手干点别的”,而是“它们本身就是独立启动模式”。
所以一旦命中,cli.tsx 就把控制权交给对应实现,然后当前入口函数结束。这不是“提前退出失败了”,而是“提前完成分流了”。
第三类:完整 CLI 里的早退
进入 main.tsx:585-607 之后,也能看到“不要无脑一直往后跑”的设计。
main.tsx 一开始先做进程级安全和运行时准备:设置 Windows 上防止当前目录劫持可执行文件搜索路径的环境变量、初始化 warning handler、挂载退出与 SIGINT 处理。这说明完整 CLI 的接管点,不是“立即执行业务”,而是“先把运行环境调成安全、稳定、可控的状态”。
后面还有一些会在完整 CLI 里提前结束的路径。例如 deep link 处理:当命中 --handle-uri 时,会在 main.tsx:644-660 中处理 URI,拿到退出码后直接 process.exit(...)。这说明“提前退出”并不只存在于 cli.tsx,而是整个启动链条都在根据场景做最短路径处理。
所以第三个问题的答案,不该只是“很多地方 return 了”,而应该是:Claude Code 的入口设计本来就鼓励按场景尽早结束,不把所有调用都拖进同一条重启动路径。
四、主执行流程
如果前面的快速路径都没有命中,cli.tsx 最终会在 cli.tsx:291-302 动态导入 ../main.js,然后执行其中的 cliMain()。
这是入口层最值得记住的一跳。
它意味着:cli.tsx 并不拥有完整 CLI 的细节。它只负责在最外层做筛选;一旦确认这次调用需要进入“真正的 Claude Code”,才懒加载 main.tsx 对应的完整实现。
而 main.tsx 接手以后,马上就能看到它开始做更重的事情:
建立完整运行前置状态,比如安全环境变量和信号处理,见 main.tsx:585-607 规范化一些特殊入口,把 cc://、assistant、ssh 等形态整理成后续主命令容易接管的统一形式,见 main.tsx:609-815初始化 Commander 程序对象,并在 preAction中执行真正的初始化流程init(),见 main.tsx:902-917继续定义完整 CLI 的顶层参数、模式和 action handler,见 main.tsx:968-1006
读到这里,你就不用急着继续往下钻了。因为第四个问题已经有了清晰答案:通用启动路径会先经过 cli.tsx 的轻量分流,最后通过动态导入把控制权交给 main.tsx;而 main.tsx 才是完整 CLI 的正式协调入口。
五、总结
完成入口层速读后,可以将 Claude Code 的启动结构归纳为四个核心结论:

第一,程序最早从 src/src/entrypoints/cli.tsx 起步,它是轻量入口,不是完整业务层。
第二,cli.tsx 的主要职责是分流:能快速回答的就快速回答,能走专用模式的就直接交给专用模式,不会一上来就把整套 CLI 都拉起来。
第三,“提前退出”不是例外,而是这个入口设计的重要原则。它体现的是启动性能、职责边界和模式分离。
第四,真正的通用 CLI 主流程,是在 cli.tsx 动态导入 main.tsx 之后,才由 main.tsx 正式接管。
这就是为什么源码学习第一步应该停在这里。你现在已经拿到了 Claude Code 的启动骨架:外层入口负责快速判断和分流,内层入口负责完整初始化和主流程协调。
有了这张地图,下一步你再去看 main.tsx 继续通向哪些命令系统、哪些交互路径、哪些 query 执行链路,就不会一上来掉进实现细节里。
参考源码
src/src/entrypoints/cli.tsx:33-41 src/src/entrypoints/cli.tsx:50-70 src/src/entrypoints/cli.tsx:72-92 src/src/entrypoints/cli.tsx:95-105 src/src/entrypoints/cli.tsx:108-161 src/src/entrypoints/cli.tsx:291-302 src/src/main.tsx:585-607 src/src/main.tsx:609-815 src/src/main.tsx:902-917 src/src/main.tsx:968-1006