很多人看到 Claude Code 的入口文件第一反应是这不就是一个 CLI 程序吗解析一下参数然后把界面挂起来就完了。真顺着源码看下去你会发现完全不是这么回事。Claude Code 的启动流程里不只有参数解析和界面启动还夹着顶层副作用、setup()初始化、命令和 agent 预加载、信任校验、前置对话框、环境变量生效、遥测初始化以及首屏之后的延迟预取。换句话说Claude Code 的“启动”不是一个瞬时动作而是一条被明确设计过的链路。它既要快又要安全还要给后面的 REPL 主循环准备好上下文。这篇文章就只做一件事把 Claude Code 从main.tsx到第一屏界面的启动过程拆开讲清楚帮你建立一个后面可以反复复用的启动心智模型。说明本文仍然基于当前可见的claude code泄露 源码快照写作只讨论源码可证实的启动链路不延伸到仓库外的构建、发布和线上部署细节。本篇看点Claude Code 的启动为什么不是“参数解析 render”这么简单入口文件最开始那几个顶层副作用到底在干什么setup()为什么要和getCommands()并行showSetupScreens()为什么其实是启动链路里的信任边界launchRepl()之后为什么还不算真正“全部启动完”先给结论Claude Code 的启动可以拆成 5 个阶段如果你现在只想先抓主线可以先记这个版本顶层副作用提前启动一些昂贵但可并行的事情setup()负责底层初始化并尽量和 commands/agents 预加载并行在真正进入主界面前还要跑一轮 setup screenslaunchRepl()把 React 版终端界面挂起来首屏之后再用startDeferredPrefetches()把一批缓存和检测器悄悄补齐这条链路里最值得注意的不是“函数名字”而是 Claude Code 对启动阶段的三个态度它非常在意首屏速度它把信任边界放在真正进入主界面之前它把“首屏可用”和“后台补齐”明确拆开了第一阶段main.tsx一开始就有顶层副作用如果你从src/main.tsx最上面开始看会先看到一段很少见但很有信息量的注释// src/main.tsx:1-20// These side-effects must run before all other imports:// 1. profileCheckpoint marks entry before heavy module evaluation begins// 2. startMdmRawRead fires MDM subprocesses ...// 3. startKeychainPrefetch fires both macOS keychain reads ...import{profileCheckpoint}from./utils/startupProfiler.js;profileCheckpoint(main_tsx_entry);import{startMdmRawRead}from./utils/settings/mdm/rawRead.js;startMdmRawRead();import{startKeychainPrefetch}from./utils/secureStorage/keychainPrefetch.js;startKeychainPrefetch();这段代码很值得细读因为它说明 Claude Code 连“模块还没全 import 完”这个时机都在利用。这里至少有三层意思profileCheckpoint(main_tsx_entry)在最早的时机打点方便后面分析启动耗时startMdmRawRead()提前拉起 MDM 相关读取让它和后续 import 并行startKeychainPrefetch()提前拉起 keychain 读取避免后面某些路径串行阻塞这不是为了炫技而是一个很现实的启动优化思路只要某些动作不依赖后面的大部分模块就尽量提前并行启动。这一段对阅读有什么帮助它会立刻提醒你Claude Code 的入口层不是“等所有 import 完再开始工作”而是从最开始就在考虑启动关键路径。[源码事实]顶层副作用被明确标注为必须在其他 import 之前运行。[工程含义]这套启动设计很在意 time-to-first-render而不是单纯追求代码表面整洁。第二阶段setup()不是单干它会和 commands/agents 预加载并行这部分是整条启动链路里最值得看的设计点之一。先看main.tsx// src/main.tsx:1903-1932const{setup}awaitimport(./setup.js);constpreSetupCwdgetCwd();if(process.env.CLAUDE_CODE_ENTRYPOINT!local-agent){initBuiltinPlugins();initBundledSkills();}constsetupPromisesetup(...);constcommandsPromiseworktreeEnabled?null:getCommands(preSetupCwd);constagentDefsPromiseworktreeEnabled?null:getAgentDefinitionsWithOverrides(preSetupCwd);commandsPromise?.catch((){});agentDefsPromise?.catch((){});awaitsetupPromise;这里最关键的不是某一行 API而是这个结构本身setup()在跑getCommands(preSetupCwd)在跑getAgentDefinitionsWithOverrides(preSetupCwd)也在跑也就是说Claude Code 没有把启动流程串成一条单线程长链而是在能并行的地方尽量并行。为什么这里要特判worktreeEnabled源码注释已经把原因写得很清楚--worktree可能导致setup()里发生process.chdir()。如果在cwd还没稳定时就并行跑 commands/agents读到的路径上下文可能不对。所以这里并不是“无脑并行”而是“在路径稳定时并行”。这正是一个很工程化的点性能优化不是盲目开并发而是建立在语义安全之上。setup()自己到底负责什么如果你跟进src/setup.ts会发现这个函数干的事情相当多// src/setup.ts:56-66exportasyncfunctionsetup(cwd:string,permissionMode:PermissionMode,allowDangerouslySkipPermissions:boolean,worktreeEnabled:boolean,worktreeName:string|undefined,tmuxEnabled:boolean,...):Promisevoid{继续往下看它大致承担了这些职责检查 Node.js 版本根据模式启动 UDS messaging恢复可能中断的终端备份配置setCwd(cwd)确保后面所有依赖 cwd 的逻辑有统一起点捕获 hooks 配置快照初始化 FileChanged watcher处理--worktree分支包括process.chdir()、保存 worktree 状态、更新 hooks 配置启动一些 background jobs提前做一部分 prefetch特别值得注意的是这里这一句注释// src/setup.ts:175// IMPORTANT: this must be called befiore getCommands(), otherwise /eject wont be available.这句注释非常有代表性。它说明setup()不是一个纯底层初始化函数它和上层“命令可见性”已经有因果关系了。也因此main.tsx那段并行初始化才要对worktreeEnabled这么谨慎。第三阶段setup()完成后再汇合 commands 和 agents很多人会下意识把“setup 完成”理解成“启动完成”。Claude Code 这里不是这样。setup()完成后main.tsx还要把前面并行 kick 掉的 commands 和 agents 汇合回来// src/main.tsx:2021-2027constcurrentCwdworktreeEnabled?getCwd():preSetupCwd;const[commands,agentDefinitionsResult]awaitPromise.all([commandsPromise??getCommands(currentCwd),agentDefsPromise??getAgentDefinitionsWithOverrides(currentCwd),]);这个阶段回答的是另一个问题现在真正稳定下来的工作目录是什么命令和 agent 定义应该按哪个 cwd 去看。这一步为什么不能省因为 Claude Code 的 commands 和 agents 不是完全静态的。它们可能受这些因素影响当前 cwdworktree 是否介入插件和技能是否已经注册某些 feature/mode 是否生效所以这里不是简单地“等前面的 Promise 结束”而是在“路径语义稳定”之后把真正要给后续启动阶段使用的 commands 和 agent definitions 整理出来。第四阶段第一屏之前其实还隔着一整层showSetupScreens()到这里Claude Code 仍然没有直接进 REPL。接下来是启动链路里最容易被忽略、但其实非常关键的一层// src/main.tsx:2236-2239constsetupScreensStartDate.now();constonboardingShownawaitshowSetupScreens(root,permissionMode,allowDangerouslySkipPermissions,commands,enableClaudeInChrome,devChannels);从函数名就能看出来这不是“一个 setup screen”而是一组 setup screens。showSetupScreens()真正在做什么跟到src/interactiveHelpers.tsx你会发现它承担的是启动前的“最后一道编排层”。先看开头// src/interactiveHelpers.tsx:96-100exportasyncfunctionrenderAndRun(root:Root,element:React.ReactNode):Promisevoid{root.render(element);startDeferredPrefetches();awaitroot.waitUntilExit();awaitgracefulShutdown(0);}而showSetupScreens()本身则处理了很多“你以为还没开始其实已经是启动核心”的事情。1. Onboarding 可能先出现如果全局配置里还没有 theme 或者用户没完成 onboarding会先弹 onboarding// src/interactiveHelpers.tsx:107-120constconfiggetGlobalConfig();letonboardingShownfalse;if(!config.theme||!config.hasCompletedOnboarding){onboardingShowntrue;const{Onboarding}awaitimport(./components/Onboarding.js);awaitshowSetupDialog(root,doneOnboarding onDone{(){...}}/);}这说明 Claude Code 的第一屏不一定就是 REPL。本地首次运行时前面可能先过 onboarding。2. TrustDialog 是真正的信任边界接着看这段// src/interactiveHelpers.tsx:128-148if(!isEnvTruthy(process.env.CLAUBBIT)){if(!checkHasTrustDialogAccepted()){const{TrustDialog}awaitimport(./components/TrustDialog/TrustDialog.js);awaitshowSetupDialog(root,doneTrustDialog commands{commands}onDone{done}/);}setSessionTrustAccepted(true);resetGrowthBook();voidinitializeGrowthBook();voidgetSystemContext();}这段很关键原因有三个它说明 trust 并不是“附带提示”而是一个明确的启动边界trust 通过之后当前 session 才会被标记为 trusted一些依赖 trust 的后续动作只有在这个边界之后才会开始这也是 Claude Code 启动设计里最值得学的一点之一它没有把“能不能用工具”和“工作目录是否可信”混成一件事而是明确把 workspace trust 单独作为一个边界处理。3. trust 之后才会做一批真正敏感的事情继续往下看// src/interactiveHelpers.tsx:153-188const{errors:allErrors}getSettingsWithAllErrors();if(allErrors.length0){awaithandleMcpjsonServerApprovals(root);}if(awaitshouldShowClaudeMdExternalIncludesWarning()){...}applyConfigEnvironmentVariables();setImmediate(()initializeTelemetryAfterTrust());这一段特别有“启动安全边界”的味道如果 settings 没问题就检查mcp.json里有没有需要审批的 server检查CLAUDE.md外部 include 是否需要确认trust 之后才应用完整环境变量telemetry 也在 trust 之后初始化换句话说showSetupScreens()不是 UI 点缀它本质上是“让启动链路跨过信任边界”的地方。4. 还有 API key、危险模式、auto mode 等前置确认再往后还有这些逻辑自定义 API key 新出现时弹审批bypassPermissions或allowDangerouslySkipPermissions时弹危险模式确认某些 auto mode 场景下弹 opt-in 对话框这进一步说明“第一屏之前发生了什么”绝不只是一个简单 splash screen而是一组和安全、权限、用户确认直接相关的启动流程。第五阶段真正进入 REPL启动才来到“可交互阶段”等上面这一串都完成后Claude Code 才真正进入 REPL// src/replLauncher.tsx:12-18exportasyncfunctionlaunchRepl(root:Root,appProps:AppWrapperProps,replProps:REPLProps,renderAndRun:...){const{App}awaitimport(./components/App.js);const{REPL}awaitimport(./screens/REPL.js);awaitrenderAndRun(root,App{...appProps}REPL{...replProps}//App,);}这里有两个非常明确的结论Claude Code 的主界面是 React 组件树不是手写 stdout 拼接真正的“启动成功”不是某个 Promise resolve而是主界面已经被挂到 root 上renderAndRun()为什么也值得看刚才已经贴过它的实现这里再换个角度说一次// src/interactiveHelpers.tsx:96-100exportasyncfunctionrenderAndRun(root:Root,element:React.ReactNode):Promisevoid{root.render(element);startDeferredPrefetches();awaitroot.waitUntilExit();awaitgracefulShutdown(0);}这 4 行几乎把 Claude Code 的运行时节奏写清楚了先 renderrender 之后立刻启动延迟预取然后挂起等待整个 UI 生命周期结束最后做优雅退出这不是“画完第一屏就结束”而是“第一屏之后才进入真正的交互阶段”。第六阶段首屏之后还有一批“故意不阻塞首屏”的延迟预取这部分是很多启动分析文章会漏掉的但我觉得 Claude Code 这里恰恰很值得写因为它把“首屏可用”和“后续体验优化”拆得非常明确。// src/main.tsx:388-425exportfunctionstartDeferredPrefetches():void{// This function runs after first render, so it doesnt block the initial paint.if(isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER)||isBareMode()){return;}voidinitUser();voidgetUserContext();prefetchSystemContextIfSafe();voidgetRelevantTips();voidcountFilesRoundedRg(getCwd(),AbortSignal.timeout(3000),[]);voidinitializeAnalyticsGates();voidprefetchOfficialMcpUrls();voidrefreshModelCapabilities();voidsettingsChangeDetector.initialize();if(!isBareMode()){voidskillChangeDetector.initialize();}}这段实现非常直白地说明它明确运行在 first render 之后它的目标就是不阻塞 initial paint里面塞的是一批“首轮交互前最好已经 warm up 好”的事情这些预取在解决什么问题本质上是在利用“用户看见界面到真正开始输入”这段时间把首轮体验所需的一些缓存提前补齐例如用户信息用户上下文system contexttips文件数统计analytics gatesMCP 官方 URLmodel capabilitiessettings 和 skills 变更探测器这个思路很实用不是所有东西都值得塞进首屏前但有些东西又确实值得尽快 warm up。Claude Code 的做法就是把这批工作放到“界面已经可见但用户大概率还没开始第一轮操作”的窗口里。[工程含义]启动链路不只是在追求“更快 render”还在追求“首轮交互更顺滑”。把整条链路连起来你会看到什么如果把前面的阶段压缩成一句话大概是先用顶层副作用抢时间再让setup()和 commands/agents 预加载并行跑接着在showSetupScreens()里跨过 trust 和一系列前置确认边界然后用launchRepl()挂起主界面最后在 first render 之后用startDeferredPrefetches()悄悄补齐一批缓存和探测器。这整条链路体现出来的不只是“功能完整”更是三个很鲜明的工程取向1. 启动性能是显式目标并行化、顶层副作用、首屏后预取这些都不是偶然写出来的。2. trust 是启动边界不是提示文案环境变量、遥测、MCP 审批、CLAUDE.md 外部 include都和 trust 绑定得很紧。3.launchRepl()只是“可交互”的起点不是启动工作的终点真正的启动观应该是首屏和后台补齐是两个阶段而不是一个阶段。如果你要自己顺着源码读建议这样走第一步先看这 6 个点建议你先全局搜这 6 个关键词startMdmRawReadstartKeychainPrefetchsetup(showSetupScreens(launchRepl(startDeferredPrefetches(这 6 个点几乎就是第二篇的启动骨架。第二步边看边画一张启动时序图建议你自己随手画成下面这种顺序你画完这张图之后再回去看细节理解速度会快很多。第三步暂时不要陷进 feature flag 细节里main.tsx和showSetupScreens()里 feature flag 很多很容易把注意力吸走。第一次读时先抓这三件事就够哪些步骤发生在 render 之前哪些步骤发生在 trust 之后哪些步骤被故意推迟到了 first render 之后这三件事抓住了后面的 feature 分支就没那么容易把你带偏。这篇文章最想帮你建立的判断如果只用一句话总结第二篇我会这样说Claude Code 的启动不是“把 REPL 挂起来”这么简单而是一条同时照顾性能、信任边界和首轮体验的分阶段启动链路。这也是为什么第二篇值得单写。因为只要你把启动链路读顺后面去看命令系统、REPL、QueryEngine、Tool 系统时都会自然带着“这块是在什么时候接进来”的时序感去理解而不是只看见一堆孤立模块。如果你下一步要继续跟源码第三篇最值得写的就是命令系统commands.ts到底是怎么把 Claude Code 的操作面装配出来的。这一篇先把“程序是怎么起来的”讲清楚。后面会继续沿着命令系统、REPL 交互、QueryEngine、工具边界、权限机制和扩展能力往下写尽量把每一篇都落在一个具体问题上而不是做泛泛的源码摘抄。后续会持续更新这个系列。