claude code 源码解析 之 main.ts

21 阅读21分钟

main.tsx 逐行分析

文件位置: src/main.tsx | 行数: 4,683 | 大小: 785KB 作用: Claude Code CLI 的入口文件,负责进程初始化、CLI 参数解析、启动 REPL 或非交互模式


整体架构

main.tsx 从执行流上可以切为三层:

第一层:模块加载时副作用(第1-209行)
        ↓
第二层:main() 函数(第585-856行)
  ├── 安全加固
  ├── argv 预处理(ssh / cc:// / assistant 重写)
  ├── 调用 run()
  └── profile 打点
        ↓
第三层:run() 函数(第884-4683行)
  ├── Commander 配置 + action
  │   ├── preAction(init / MCP / 迁移)
  │   ├── 选项解析
  │   ├── 非交互模式(--print, 第2585-2861行)
  │   └── 交互模式(--continue / --resume / 普通, 第3101-3807行)
  └── 子命令注册(4000+ 行起)

第一部分:模块加载时副作用(1-209)

启动性能分析(1-12)

import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

这里用 profileCheckpoint 给启动加上时间戳打点,后面通过 profileReport 可以输出启动路径各阶段的耗时。之所以放在 import 之前,是为了测量整个模块加载耗时——包括后面所有 import 的解析时间。

关键子进程预启动(13-20)

startMdmRawRead();                   // MDM 配置(macOS plutil / reg query)
startKeychainPrefetch();             // 钥匙串预读(OAuth + API Key)

这两行是性能优化的核心:MDM 读取和钥匙串查询都是同步子进程(~65ms each),但在 import 阶段就启动它们,让子进程与后面的 import 模块解析(~135ms)并行执行。等真正需要结果时已经完成了。

主要依赖导入(21-207)

约150个 import 语句,涵盖整个 CLI 的模块依赖。几个关键分组:

类别模块职责
CLI 框架@commander-js/extra-typings命令行参数解析
渲染React, ./ink.jsTUI 渲染引擎
核心./query.js, ./QueryEngine.jsLLM 查询循环
配置./utils/config.js, ./utils/settings/settings.js全局/项目配置
MCP./services/mcp/*MCP 客户端管理
权限./utils/permissions/*权限系统
插件/技能./utils/plugins/*, ./skills/*插件系统
工具./tools.js工具注册
会话./utils/sessionStorage.js会话持久化
迁移./migrations/*数据迁移

注意第21行 import { feature } from 'bun:bundle'——这是 Bun 内建的条件编译能力,后面大量 feature('FLAG_NAME') 调用都会在编译时做 dead code elimination。

Claude Code Feature Flags 完全列表

循环依赖规避(68-82)

const getTeammateUtils = () => require('./utils/teammate.js');
const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null;

用动态 require 替代静态 import 来打破循环依赖。teammate.tsAppState.tsx → ... → main.tsx 的引用链不能有静态 import。同时 feature() 包裹的 require 会被 Bun 编译时做死代码消除——如果特性关闭,对应模块根本不会打包进去。

第二打点(209)

profileCheckpoint('main_tsx_imports_loaded');

标记所有 import 解析完成的时刻,测量整个模块加载耗时。


第二部分:辅助函数与配置(211-583)

logManagedSettings(216-229)

将受管设置(enterprise policy 下发的配置)的 key 列表上报给 Statsig 用于分析。静默捕获所有错误,不阻塞启动。

isBeingDebugged(232-263)

检测是否在调试模式下运行(--inspect, --inspect-brk, NODE_OPTIONS)。第 266-271 行在非 ant 构建中检测到调试模式时会直接 process.exit(1) ——这是安全措施,防止外部用户附加调试器。

logSessionTelemetry(279-290)

每个会话的插件/技能加载情况上报。有两个调用点(交互 + headless),因为两条路径在 main.tsx 中分支。

logStartupTelemetry(307-321)

上报启动时的环境信息:git 状态、worktree 数量、GitHub auth、sandbox、自动更新等。

runMigrations(326-352)

const CURRENT_MIGRATION_VERSION = 11;

带版本号的数据迁移系统。每次启动检查 globalConfig.migrationVersion,如果不匹配就依次执行所有迁移函数。第 11 版本包括:

  • migrateAutoUpdatesToSettings — 自动更新配置迁移
  • migrateBypassPermissionsAcceptedToSettings — 权限接受迁移
  • migrateSonnet1mToSonnet45migrateSonnet45ToSonnet46migrateOpusToOpus1m — 模型名变更
  • resetAutoModeOptInForDefaultOffer — 自动模式 opt-in 重置

每次添加新的大版本模型发布时都需要在 @[MODEL LAUNCH](第323行)注释指引下增加迁移。

prefetchSystemContextIfSafe(360-380)

安全设计:git 命令可以通过 core.fsmonitor、diff.external 等配置执行任意代码。所以在用户确认信任目录之前,不会执行 getSystemContext()(内部会跑 git status)。非交互模式(--print)信任是隐式的。

startDeferredPrefetches(388-431)

首屏渲染后的后台预热任务。这是性能关键路径——首屏必须快速展示,预热与用户打字输入并行:

优先级任务说明
进程级initUser(), getUserContext()用户/系统信息 cache
网络prefetchAwsCredentialsAndBedRockInfoIfSafe()凭证预取
文件countFilesRoundedRg()文件计数(给 token budget 用)
分析initializeAnalyticsGates(), prefetchOfficialMcpUrls()Gat 初始化
监控settingsChangeDetector.initialize()热重载检测器

loadSettingsFromFlag / loadSettingSourcesFromFlag(432-496)

--settings 同时接受 JSON 字符串和文件路径。JSON 字符串会写入内容哈希路径的临时文件——用哈希而非 UUID 是为了避免变更 Bash tool 的 sandbox denyWithinAllow 列表(这个列表是 tool description 的一部分,UUID 变化会导致每次 query 的 cache prefix 都不同,造成12倍的 input token 浪费)。

eagerLoadSettings(502-516)

init() 之前就解析 --settings--setting-sources 标志,确保 settings 从初始化一开始就被正确加载。

initializeEntrypoint(517-540)

根据进程参数和入口判断 CLAUDE_CODE_ENTRYPOINT 值,影响 telemetry 分类和后续行为。

Pending 连接类型定义(543-584)

三个惰性挂起的连接类型,由 argv 预处理填充,后面由主 action 消费:

  • _pendingConnectcc:// 直连
  • _pendingAssistantChat — Claude 助手模式
  • _pendingSSH — SSH 远程连接

第三部分:main() 函数(585-856)

安全加固(586-606)

process.env.NoDefaultCurrentDirectoryInExePath = '1';  // Windows PATH 劫持防御

Windows 上这行代码防止从当前目录加载可执行文件(类似 DLL hijacking 的变体)。

SIGINT 处理有特殊逻辑:-p/--print 模式使用自己的 SIGINT handler 来优雅中断正在进行的 query,所以这里只响应一次 process.exit(0)

argv 预处理(612-795)

这是 main.tsx 最复杂的逻辑之一,负责在 Commander 运行之前拦截并重写 argv:

暂不分析(612-677)

feature('DIRECT_CONNECT')

feature('LODESTONE')

feature('KAIROS')

......

当前公开发布的 CLI 中,以上这些功能是关闭的

feature('SSH_REMOTE')(706-795)

feature('SSH_REMOTE')这个特性开关,代表了 Claude Code 中一套旨在突破本地环境限制,让你能远程操控的模式。

这个模式是官方提供的一套解决方案。它的核心在于:你本地电脑上的 Claude Code 会话,可以被你的手机或其他设备实时接管

  • 工作流程:在你的电脑上运行 /remote-control 命令,会生成一个仅用于验证的二维码。用手机 App 扫一下,就能直接看到并管理电脑上的任务了。
  • 安全保障:它的流量完全走 Anthropic 的 API,并且使用短期凭证,全程有 TLS 加密,可以说是相当安全
  • 适用场景:这种模式最适合你离开工位后,还想随时关注或调整代码的场景。比如你让 AI 在服务器跑一个大型任务,你下班回家躺沙发上,用手机就能继续指挥它、检查结果。

入口点初始化(797-848)

const isNonInteractive = hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || !process.stdout.isTTY;

判断交互/非交互的关键行。没有 TTY 或指定了 -p/--print / --sdk-url 都算非交互。

clientType 的解析在 818-833 行,通过多个环境变量判断当前是 CLI、GitHub Action、SDK (TypeScript/Python)、VSCode、Desktop 还是 remote 模式。这个值会影响 telemetry 分类和部分功能开关。

eagerLoadSettings + run()(852-855)

eagerLoadSettings();
await run();

至此 main() 的 argv 预处理和状态初始化完成,进入 run() 函数——Commander 驱动的完整 CLI 生命周期。


第四部分:run() 函数

Commander 配置(884-1006)

使用 @commander-js/extra-typings 构建 CLI 参数树:

const program = new CommanderCommand()
  .configureHelp(createSortedHelpConfig())
  .enablePositionalOptions();
preAction hook(907-967)

在 Commander 执行任何子命令的 action 之前运行,负责初始化基础设施:

  1. 等待 MDM 和钥匙串(第914行)—— ensureMdmSettingsLoaded() + ensureKeychainPrefetchCompleted(),这些子进程是在 import 阶段启动的,此时只 await 它们完成
  2. init()(第916行)—— 核心初始化:加载 settings、处理 env vars、配置 logger
  3. 设置 process.title(第922-924行)
  4. 初始化 log sink(第934行)—— 确保子命令能打 logEvent
  5. 处理 --plugin-dir(第945-949行)
  6. 运行数据迁移(第950行)—— runMigrations()
  7. 加载远程受管设置(第957-958行)—— enterprise policy 下发
  8. 上传 settings sync(第963-965行)
CLI 选项列表(968-1006)

约 40+ 个 CLI 选项,按功能分组:

选项用途
-d, --debug调试模式
-p, --print非交互模式
--model, --effort模型选择
--permission-mode权限模式
--dangerously-skip-permissions跳过所有权限检查
--allowed-tools, --disallowed-tools工具白/黑名单
--mcp-configMCP 配置
--system-prompt / --system-prompt-file系统提示覆盖
--continue, --resume会话恢复
--worktree, --tmux工作树隔离
--settings, --setting-sources设置来源

部分选项使用 .hideHelp() 隐藏(如 --sdk-url--teleport--agent-id),这些是供 SDK 或内部使用的,不暴露给终端用户。

--permission-mode 使用说明

action 函数(1006-3808)

这是 run() 的核心——当 Commander 匹配到默认命令(即不是子命令)时执行的逻辑。

--bare 模式(1009-1016)
if (options.bare) {
  process.env.CLAUDE_CODE_SIMPLE = '1';
}

单纯设一个环境变量,后面大量 isBareMode() 调用会检查它。

Assistant 模式初始化(1048-1089)

暂不分析

选项解构与校验(1090-1389)

从 Commander 解析的 options 对象中解构出所有运行时需要的参数,做了大量校验:

  • session-id(1277-1302):必须是有效的 UUID,不能与已有 session 冲突,与 --continue/--resume 同时使用必须带 --fork-session
  • --file(1305-1331):文件下载需要 CLAUDE_CODE_SESSION_ACCESS_TOKEN
  • --fallback-model(1337-1340):不能和主模型相同
  • --system-prompt-file(1344-1361):读取文件,处理 ENOENT
  • --append-system-prompt-file(1364-1382):同上
权限模式解析(1390-1411)
const { mode: permissionMode, notification: permissionModeNotification }
  = initialPermissionModeFromCLI({ permissionModeCli, dangerouslySkipPermissions });

同时检测 --enable-auto-mode--permission-mode auto 和 settings 中 defaultMode: auto,决定是否进入自动模式。

--mcp-config 解析(1414-1523)

处理复杂的 MCP 配置加载:

  1. 每一项先尝试解析为 JSON 对象
  2. 失败则尝试作为文件路径读取
  3. 动态作用域的配置会覆盖文件中的同名配置
  4. 用 enterprise policy 过滤被阻止的服务器
  5. 检查保留名称(claude_in_chromecomputer_use
(1525-1595)

暂不分析

Chicago Computer Use MCP(1608-1630)

macOS 独占,通过 @ant/computer-use-mcp 提供屏幕截图和键盘控制,静默失败以保持 dogfooding 体验。

通道系统(Channels, 1641-1719)

暂不分析

权限上下文初始化(1744-1777)
const initResult = await initializeToolPermissionContext({
  allowedToolsCli, disallowedToolsCli, baseToolsCli,
  permissionMode, allowDangerouslySkipPermissions, addDirs
});

这一步会加载 settings 中的 allowedTools/disallowedTools,合并 CLI 参数,生成最终的 toolPermissionContext

对于 ant 用户,过于宽泛的 shell 权限(Bash(*)PowerShell(*))会被静默移除。

MCP 配置延迟加载(1799-1814)
const mcpConfigPromise = (strictMcpConfig || isBareMode()
  ? Promise.resolve({ servers: {} })
  : getClaudeCodeMcpConfigs(dynamicMcpConfig));

这里只读文件,不连接任何服务器。真正的 MCP 连接在后面 await 之后才进行。isBareMode() 跳过自动发现的 MCP 配置(.mcp.json、user settings、plugins)。

setup() 与并发加载(1904-1936)
const setupPromise = setup(preSetupCwd, permissionMode, ...);
const commandsPromise = worktreeEnabled ? null : getCommands(preSetupCwd);
const agentDefsPromise = worktreeEnabled ? null : getAgentDefinitionsWithOverrides(preSetupCwd);
await setupPromise;

这里做了一个重要的并发优化:setup()(约28ms,主要是 Unix socket bind)和 commands/agents 的磁盘加载并行执行。但注意 worktreeEnabled 时不能并行——因为 setup() 会 process.chdir() 到 worktree 目录,而 commands/agents 需要 post-chdir 的 cwd。

非交互模式的特殊处理(1952-1989)

--print 模式下,需要尽早开始一些耗时操作:

applyConfigEnvironmentVariables();   // 应用所有环境变量(包括 PATH)
void getSystemContext();             // 预热 git status(被 memoize)
void getUserContext();               // 预热用户上下文
void ensureModelStringsInitialized();// 预热模型字符串

这些 void 调用会启动但不等它们完成,后续在 print.ts 中通过 cache 命中拿到结果。

模型解析(2014-2112)

模型选择优先级链:

  1. CLI 参数 --model'default' 映射到默认模型)
  2. 环境变量 ANTHROPIC_MODEL
  3. Agent 定义中的 model(如果 !=='inherit'
  4. settings 中的 model
  5. 全局默认模型
命令与 Agent 定义加载(2026-2052)
const [commands, agentDefinitionsResult] = await Promise.all([
  commandsPromise ?? getCommands(currentCwd),
  agentDefsPromise ?? getAgentDefinitionsWithOverrides(currentCwd)
]);

commandsPromise 在 setup() 之前就启动了,这里 join 进来,如果已经在跑了就直接拿到结果。

主线程 Agent 决议(2054-2080)
const agentSetting = agentCli ?? getInitialSettings().agent;
const mainThreadAgentDefinition = agentDefinitions.activeAgents
  .find(agent => agent.agentType === agentSetting);

Agent 可以由 --agent CLI 参数或 settings.json 中的 agent 字段指定。找不到时静默降级为默认行为(只打一条 debug 日志)。

Advisor 模式(2117-2138)

可选的服务器端 advisor 模型,当前模型必须支持 advisor 模式,指定的 advisor 模型必须是有效的模型 ID。通过 --advisor 或者 settings 中的 advisor 设置启用。

什么是advisor模式?

Proactive / Brief 模式激活(2174-2209)

暂不分析

REPL 初始化(2213-2241)

if (!isNonInteractiveSession) {
  const ctx = getRenderContext(false);
  const { createRoot } = await import('./ink.js');
  root = await createRoot(ctx.renderOptions);
  const onboardingShown = await showSetupScreens(root, ...);
}

交互模式会创建 Ink(React for terminal)的 root 节点,然后展示设置界面(信任对话框、OAuth 登录、新手上路、会话选择器)。

启动时间统计:第2235-2238行在首屏渲染前记录启动耗时,这样不会把用户停留在对话框的时间算进去(旧代码在 REPL 第一次渲染时记录,p99 被拉高到~70s)。

跳过重复的 /login 命令(2276-2278)

if (onboardingShown && prompt?.trim().toLowerCase() === '/login') {
   prompt = '';
}

用户在 onboarding 流程中已经登录过了,如果紧接着又触发了 /login 命令,直接丢弃,避免重复执行登录。

刷新依赖登录态的服务(2279-2306)

onboarding 登录完成后,执行一系列后处理:

  • refreshRemoteManagedSettings() — 刷新远程托管配置(如组织下发的策略配置)
  • refreshPolicyLimits() — 刷新策略限制(合规/权限相关)
  • resetUserCache() — 清空用户数据缓存,必须在 GrowthBook 刷新之前执行,确保用最新的凭证去获取 feature flags
  • refreshGrowthBookAfterAuthChange() — 刷新 GrowthBook(特性开关系统),获取更新后的 feature flag,例如 claude.ai 的 MCP 权限
  • Trusted Device 处理 — 清除旧的受信任设备 token 然后重新注册,用于 Remote Control(远程控制)功能。两者都通过 tengu_sessions_elevated_auth_enforcement 开关自检内部控制

为什么要按这个顺序

注释里特别强调了一个关键依赖:resetUserCache() 必需在refreshGrowthBookAfterAuthChange() 之前执行,因为 GrowthBook 需要拿最新的用户凭证去拉取 feature flags。如果顺序反了,可能拿到旧的 flag 配置。

信任后初始化(2308-2336)

信任对话框被接受之后(或非交互模式隐式信任):

  1. 初始化 LSP manager(第2321行)——延迟到信任后,防止插件 LSP 服务器在未信任目录中执行代码
  2. 显示 settings 验证错误(第2325-2336行)——非 MCP 的配置错误会弹框提示用户

启动时的后台数据预取(prefetch)逻辑(第2344-2375行)

控制 Claude Code 启动时的后台数据预取(prefetch),在保证数据及时性的同时避免频繁发起网络请求

核心是通过 throttle 机制(tengu_cicada_nap_ms)防止频繁启动时的重复请求:

Feature Flag作用
tengu_cicada_nap_ms预取最小间隔(毫秒),默认为 0 表示不限制

执行流程:

启动
  │
  ├─ Bare 模式?──────────────────┐
  │                               │
  ├─ 距上次预取 < 节流阈值?──────┤
  │                               │
  └─ 否(执行预取)               └─ 是(跳过)
       │                                │
       ├─ checkQuotaStatus()            └─ resolveFastModeStatusFromCache()
       ├─ fetchBootstrapData()
       ├─ prefetchPassesEligibility()
       └─ Fast Mode 预取

checkQuotaStatus()fetchBootstrapData()prefetchPassesEligibility()prefetchFastModeStatus(),核心是通过 throttle 机制(tengu_cicada_nap_ms)防止频繁启动时的重复请求

跳过预取时:

  • 仅记录日志
  • resolveFastModeStatusFromCache() — 直接从缓存解析 fast mode 状态,确保不卡在 pending

设计要点:

  • 节流可动态配置:通过 feature flag 远程控制,无需发版
  • Kill switch 降级tengu_miraculo_the_bard 可快速关闭 fast mode 网络预取,优雅降级为缓存读取
  • 状态一致性:跳过的场景下也主动解析缓存,避免状态残留为 pending
  • 首次 vs 后续启动:通过 startupPrefetchedAt 区分首次启动(值为 0)和重复启动,首次不做节流

MCP 配置加载(2380-2430)

const { servers: existingMcpConfigs } = await mcpConfigPromise;
const allMcpConfigs = { ...existingMcpConfigs, ...dynamicMcpConfig };

之前启动并行的 mcpConfigPromise 现在 await 拿到结果,CLI flag 的 --mcp-config 覆盖文件配置。

MCP 配置被分为两类:

  • sdkMcpConfigstype: 'sdk' 的配置,由 SDK 管理
  • regularMcpConfigs — 常规 stdio/SSE 配置

会话生命周期(2496-2542)

会话注册**(第2530行)

PID 文件写入,并发会话检测(≥2 个并发时打 telemetry)

非交互模式(--print)(2585-2861)

isNonInteractiveSession 为 true 时,执行 headless 路径:

  1. 环境变量应用(第2593行):applyConfigEnvironmentVariables() — 在非交互模式中信任是隐式的
  2. Telemetry 初始化(第2597行):需要 env vars 中的 OTEL 端点
  3. SessionStart hooks(第2607行):与 MCP 连接并行执行
  4. Org 验证(第2614-2618行)
  5. 创建 headless store(第2653行):createStore(headlessInitialState, onChangeAppState)
  6. MCP 批量连接(第2691-2809行):逐个推送 pending → 替换为 connected/failed,claude.ai connectors 有 5s 超时
  7. 后台预热(第2816-2822行)
  8. 导入并执行 runHeadless(第2825-2859行)
MCP 连接的核心逻辑(2691-2718)
const connectMcpBatch = (configs, label) => {
  // Step 1: 推入 pending 状态(让 ToolSearch 能看到)
  headlessStore.setState(prev => ({ ...prev, mcp: { ...prev.mcp,
    clients: [...prev.mcp.clients, ...Object.entries(configs).map(...)]
  }}));
  // Step 2: 逐个连接或失败,用 getMcpToolsCommandsAndResources 回调更新
  return getMcpToolsCommandsAndResources(onUpdate, configs);
};

交互模式的 AppState 初始化(2926-3036)

const initialState: AppState = {
  settings: getInitialSettings(),
  tasks: {},
  agentNameRegistry: new Map(),
  verbose: ...,
  mainLoopModel: ...,
  ...
};

AppState 是 React store 的初始状态树,涵盖 60+ 字段:

字段说明
settings用户设置
tasks后台任务(agents)
agentNameRegistryAgent 名称 → ID 映射(SendMessage 路由用)
toolPermissionContext权限上下文
mcpMCP 客户端/工具/命令
replBridge*Remote Control 桥接状态
notifications通知队列
fileHistory文件变更历史
fastMode快速模式
teamContext团队上下文(Agent Swarms)

第 3035 行展示了如何合并团队上下文:

teamContext: feature('KAIROS')
  ? assistantTeamContext ?? computeInitialTeamContext?.()
  : computeInitialTeamContext?.()

KAIROS 模式优先使用 assistant 预初始化的团队上下文。

会话恢复路径(3101-3807)

交互模式的四大分支:

1. --continue(3101-3155)
const result = await loadConversationForResume(undefined, undefined);
// 找到最近的会话文件,恢复消息和历史
const loaded = await processResumedConversation(result, ...);
await launchRepl(root, { ...initialState }, {
  ...sessionConfig,
  initialMessages: loaded.messages,
  initialFileHistorySnapshots: loaded.fileHistorySnapshots,
  ...
}, renderAndRun);

先清除陈旧缓存,然后加载最近的会话文件。成功则恢复所有状态并启动 REPL;失败则用 exitWithError 退出。

2. cc:// 直连(3156-3192)
claude cc://server-url...
→ createDirectConnectSession → launchRepl(directConnectConfig)

createDirectConnectSession 会创建 WebSocket 连接到指定的服务器,返回的 config 中包含 sessionId 和通信参数。

3. SSH 远程(3193-3258)
claude ssh user@host [dir] [flags]
→ 探测远程 → 部署 binary → SSH 隧道 → launchRepl(sshSession)

SSH 模式通过 Unix socket -R 转发验证回本地机器,远程无需配置 API 密钥。

--local 标志用于 e2e 测试,在本地启动一个模拟的远程进程。

4. --remote / --teleport(3355-3807)

CCR (Claude Code Remote) 会话:

  1. --remote "description" — 创建新远程会话,可选 TUI 模式
  2. --teleport <sessionId> — 恢复已有远程会话
  3. --teleport(无参数)— 交互式选择器

Remote Control (--rc) 是另一个相关但独立的功能(通过 initReplBridge.ts 实现),在 AppState 中通过 replBridgeEnabled 控制。

--resume 的完整搜索链(3382-3704)

--resume 的 session 查找逻辑做了多层 fallback:

① 是否是有效 UUID?
  → 是:用 UUID 直接查找会话
  → 否:
    ② 是否是文件路径(ant-only)?
      → 是:loadTranscriptFromFile
      → 否:
        ③ 是否是 ccshare URL(ant-only)?
          → 是:loadCcshare → loadConversationForResume
          → 否:用搜索词打开交互式选择器

第 3384-3398 行还做了自定义标题匹配:如果 --resume "my project" 匹配到唯一会话标题,可以直接恢复,无需打开选择器。

5. 新鲜会话(3760-3807)

没有任何 --continue/--resume/--remote/--teleport 标志时,走干净启动:

// hooks 不阻塞首屏渲染——延迟到第一次 API 调用前
const pendingHookMessages = hooksPromise && hookMessages.length === 0
  ? hooksPromise : undefined;
await launchRepl(root, { ...initialState }, {
  ...sessionConfig,
  initialMessages: deepLinkBanner ? [deepLinkBanner] : (hookMessages.length > 0 ? hookMessages : undefined),
  pendingHookMessages,
}, renderAndRun);

这里 hooks 不阻塞首屏——pendingHookMessages 作为 promise 传给 REPL,REPL 在第一次 API 调用前才会等待它完成。用户体验上,输入框立即出现,不需要等待 SessionStart hooks 执行完毕。

如果通过 deep link 启动,还会注入一条 provenance banner 提示用户会话来自外部来源。

子命令注册(3892+)

Commander 的剩余部分注册了所有子命令:

mcp 子命令(3894-3958)
mcp serve              — 启动 MCP server
mcp add                — 添加 MCP 服务器
mcp remove <name>      — 删除 MCP 服务器
mcp list               — 列出 MCP 服务器
mcp get <name>         — 查看详情
mcp add-json           — 通过 JSON 添加
mcp add-from-claude-desktop — 从 Desktop 导入
mcp reset-project-choices — 重置项目选择
server 子命令(3962-4038)

claude server — 启动会话服务器(Direct Connect)

--port <number>       — HTTP 端口
--unix <path>         — Unix socket
--workspace <dir>     — 默认工作目录
--idle-timeout <ms>   — 空闲超时
--max-sessions <n>    — 最大并发
auth 子命令(4100-4136)
auth login    — OAuth 登录(支持 --email, --sso, --console, --claudeai)
auth status   — 登录状态(支持 --json, --text)
auth logout   — 登出
plugin 子命令(4148-4263)
plugin validate        — 验证插件清单
plugin list            — 列出插件
plugin marketplace add/list/remove/update — 市场管理
plugin install         — 安装插件
plugin uninstall       — 卸载
plugin enable/disable  — 启用/禁用
plugin update          — 更新
其他子命令
  • agents(4278行)— 列出配置的 agent
  • auto-mode defaults(4290行)— 打印自动模式的默认规则
  • skill — 技能管理
  • config — 配置管理
  • doctor — 诊断
  • update — 自动更新
  • setup-token(4267行)— 设置长期 token

辅助函数(文件末尾)

logTenguInit(4600+ 行区域)

汇总所有初始化相关的 telemetry:模型、权限模式、MCP、插件、选项等。用一个调用包打所有初始化事件。

maybeActivateProactive

检查 --proactive 标志或 CLAUDE_CODE_PROACTIVE 环境变量,激活自主探索模式。

maybeActivateBrief

检查 --brief 标志,启用 SendUserMessage 工具。同时检查 GrowthBook 门控(isBriefEntitled)。

resetCursor

在进程退出时恢复终端光标显示(SHOW_CURSOR 是 DEC 控制序列 \x1b[?25h)。

extractTeammateOptions

从 Commander options 中提取 --agent-id--agent-name--team-name 等队友标识参数。


关键设计模式总结

1. 启动性能优化

技术示例位置效果
子进程预启动12-20行MDM + Keychain 与 import 并行
profileCheckpoint全文件启动各阶段耗时监控
Promise 并发2027行 setup/commands/agents~28ms 隐藏在其他 I/O 后
惰性 import70-82行条件编译 + 循环依赖规避
热缓存2496行 getUserContext()预热后后续调用为 cache hit
首屏不阻塞3765行 pendingHookMessageshooks 延迟到第一次 API 调用

2. 安全设计

措施位置
Windows PATH 劫持防御591行
目录信任隔离360-380行(信任前不执行 git)
LSP 延迟初始化2321行(信任前不启动插件 LSP)
调试器检测232-271行
权限模式独立1390-1411行
Enterprise MCP 策略1584-1595行
Slug 路径校验worktree.ts

3. 容错设计

模式行为
Agent 定义缺失静默降级为默认
MCP 连接失败单独失败,不影响其他 MCP
插件初始化失败catched,telemetry 记录
数据迁移失败静默存活,下次再试
Remote 创建失败exitWithError + 清理
ccshare 加载失败telemetry 记录后 fallthrough

4. 条件编译

大量使用 feature('FLAG_NAME') + "external" !== 'ant' 做死代码消除:

if ("external" === 'ant') {
  // 这整个分支在外部构建中不存在
}
if (feature('KAIROS')) {
  // 这整个分支在 KAIROS gate 关闭时不存在
}

Bun 的 bun:bundle 会将这些不可达分支在编译时完全移除。


文件依赖关系

main.tsx 是依赖树的根节点,直接 import 约 150 个模块。核心依赖路径:

main.tsx
├── replLauncher.ts → REPL.tsx → screens/*
├── query.ts → QueryEngine.ts → services/api/claude.ts
├── setup.ts → 启动上下文
├── tools.js → 所有 Tool 定义
├── commands.js → 所有 slash command
├── services/mcp/* → MCP 客户端
├── utils/settings/* → 配置加载
├── utils/permissions/* → 权限判断
├── utils/plugins/* → 插件系统
├── utils/sessionStorage.js → 会话持久化
├── state/AppStateStore.js → 状态管理
└── cli/print.js → 非交互模式