跳转至

Deep Dive | src/main.tsx 4683 行 — Claude Code 进程入口

重要性:⭐⭐⭐⭐⭐(整个 CLI 进程的入口——所有命令、所有模式、所有启动优化都从这里开始) 真实位置src/main.tsx4683 行角色:bun 单文件可执行入口;负责 argv 解析、preflight 启动优化、Commander CLI 定义、preAction init 管道、subcommand 分发 关联topics/deep-dive-repl-screen.md(REPL 屏幕)、topics/deep-dive-repl-bridge.md(Bridge 协议)


1. 文件全景

main.tsx (4683 行)
├── 行 1-100    :顶部副作用(profileCheckpoint + startMdmRawRead + startKeychainPrefetch + imports)
├── 行 100-210  :剩余 imports + 4 段 feature-gated lazy require()(DCE 友好)
├── 行 211-271  :logManagedSettings + isBeingDebugged + 调试检查 process.exit(1)
├── 行 273-431  :telemetry & 启动优化(5 个函数)
│   ├── logSessionTelemetry  (行 279)
│   ├── getCertEnvVarTelemetry  (行 291)
│   ├── logStartupTelemetry  (行 307)
│   ├── runMigrations  (行 326)
│   ├── prefetchSystemContextIfSafe  (行 360)
│   └── startDeferredPrefetches  (行 388)
├── 行 432-515  :CLI flag 解析
│   ├── loadSettingsFromFlag  (行 432)
│   ├── loadSettingSourcesFromFlag  (行 484)
│   └── eagerLoadSettings  (行 502)
├── 行 517-583  :initializeEntrypoint (行 517)
├── 行 585-855  :main()  — 进程入口函数
├── 行 857-882  :getInputPrompt (行 857)  — stdin → string/AsyncIterable
├── 行 884-4514 :run()  — **~3630 行 CLI 定义**(核心)
├── 行 4514+    :logTenguInit + maybeActivateProactive + maybeActivateBrief + resetCursor + extractTeammateOptions

总览: - 顶部 210 行:副作用 + imports - 行 211-583:13 个 helper 函数(telemetry、settings、entrypoint) - 行 585-855main() — 270 行 - 行 884-4514run()3630 行(占文件 78%) - 行 4514+:6 个小 helper

run() 是绝对的"主菜"。


2. 顶部:3 个并行预取的精妙设计(行 1-100)

// These side-effects must run before all other imports:
// 1. profileCheckpoint marks entry before heavy module evaluation begins
// 2. startMdmRawRead fires MDM subprocesses (plutil/reg query) so they run in
//    parallel with the remaining ~135ms of imports below
// 3. startKeychainPrefetch fires both macOS keychain reads (OAuth + legacy API
//    key) in parallel — isRemoteManagedSettingsEligible() otherwise reads them
//    sequentially via sync spawn inside applySafeConfigEnvironmentVariables()
//    (~65ms on every macOS startup)
import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();
import { ensureKeychainPrefetchCompleted, startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();

3 个立即触发的副作用

副作用 触发时机 节省时间 平台
profileCheckpoint('main_tsx_entry') 模块求值开始 跨平台(埋点)
startMdmRawRead() subprocess plutil/reg query 与 imports 并行 ~135ms 跨平台(mac/win/lin)
startKeychainPrefetch() 2 个 keychain 读 节省 ~65ms sync spawn macOS

巧妙: - 这些不等 import 完成就 fire - import 解析是 ~135ms,期间这些 IO 并行进行 - 等到 await ensureMdmSettingsLoaded() / await ensureKeychainPrefetchCompleted() 在 preAction 钩子里 await 时,绝大多数已经完成

结果:每次启动节省 ~200ms(注释说 macOS 上)。

反模式警告

// eslint-disable-next-line custom-rules/no-top-level-side-effects
项目里有一条自定义 lint 规则禁止"顶级副作用",但这 3 处是白名单——并且注释里写明原因


3. Lazy require + bun:bundle feature() — DCE 死代码消除(行 60-205)

// Dead code elimination: conditional import for COORDINATOR_MODE
const coordinatorModeModule = feature('COORDINATOR_MODE') 
  ? require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js') 
  : null;

// Dead code elimination: conditional import for KAIROS (assistant mode)
const assistantModule = feature('KAIROS') 
  ? require('./assistant/index.js') as typeof import('./assistant/index.js') 
  : null;
const kairosGate = feature('KAIROS') 
  ? require('./assistant/gate.js') as typeof import('./assistant/gate.js') 
  : null;

feature('X') 是什么: - 来自 bun:bundle —— 编译时常量 - 在打包时根据 build flags 把 true / false 直接替换为字面量 - false 分支的 require 永远不会执行 → DCE 砍掉整个模块

4 个 feature-gated 模块(行 60-205):

feature 名称 模块 用途
COORDINATOR_MODE ./coordinator/coordinatorMode.js 多 agent 协调器
KAIROS ./assistant/index.js + ./assistant/gate.js assistant 模式(Agent SDK daemon)
TRANSCRIPT_CLASSIFIER ./utils/permissions/autoModeState.js auto mode 权限分类器
LODESTONE 深链 URI handler claude:// / cc+unix:// / macOS URL scheme

为什么用 require() 而不是 import: - ES import静态的——会在 module top 强制求值 - require()条件的——feature()=false根本不会执行 - 配合require()main() 执行时才被求值(lazy)

循环依赖解决方案

// Lazy require to avoid circular dependency: teammate.ts -> AppState.tsx -> ... -> main.tsx
const getTeammateUtils = () => require('./utils/teammate.js') as typeof import('./utils/teammate.js');
模式getter 包 require——只在第一次调用 getTeammateUtils() 时才 require。

teammate.tsAppState.tsx → ... → main.tsx 形成循环。用 require() 而非 import 打破。


4. isBeingDebugged() + process.exit(1) — 反调试(行 232-271)

function isBeingDebugged() {
  const isBun = isRunningWithBun();
  const hasInspectArg = process.execArgv.some(arg => {
    if (isBun) {
      return /--inspect(-brk)?/.test(arg);
    } else {
      return /--inspect(-brk)?|--debug(-brk)?/.test(arg);
    }
  });
  const hasInspectEnv = process.env.NODE_OPTIONS && /--inspect(-brk)?|--debug(-brk)?/.test(process.env.NODE_OPTIONS);
  try {
    const inspector = (global as any).require('inspector');
    const hasInspectorUrl = !!inspector.url();
    return hasInspectorUrl || hasInspectArg || hasInspectEnv;
  } catch {
    return hasInspectArg || hasInspectEnv;
  }
}

// Exit if we detect node debugging or inspection
if ("external" !== 'ant' && isBeingDebugged()) {
  process.exit(1);
}

3 个检查源: - process.execArgv —— Node/Bun 启动参数 - process.env.NODE_OPTIONS —— 环境变量(容易绕过但常见) - inspector.url() —— 已激活的调试器(最可靠的信号)

"external" !== 'ant' 编译时门: - 商业产品 = "external" === 'ant' === false反调试开启 - Anthropic 内部构建 = "external" !== 'ant' === true关闭(需要调试)

生产环境反调试目的: - 防止竞品/破解者挂调试器分析代码 - 阻止动态分析 / hook


5. startDeferredPrefetches() — 首屏后预取(行 388-431)

export function startDeferredPrefetches(): void {
  if (isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER) ||
      isBareMode()) {
    return;
  }

  // Process-spawning prefetches (consumed at first API call, user is still typing)
  void initUser();
  void getUserContext();
  prefetchSystemContextIfSafe();
  void getRelevantTips();
  if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && !isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) {
    void prefetchAwsCredentialsAndBedRockInfoIfSafe();
  }
  if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) && !isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) {
    void prefetchGcpCredentialsIfSafe();
  }
  void countFilesRoundedRg(getCwd(), AbortSignal.timeout(3000), []);

  // Analytics and feature flag initialization
  void initializeAnalyticsGates();
  void prefetchOfficialMcpUrls();
  void refreshModelCapabilities();

  // File change detectors deferred from init() to unblock first render
  void settingsChangeDetector.initialize();
  if (!isBareMode()) {
    void skillChangeDetector.initialize();
  }

  // Event loop stall detector — logs when the main thread is blocked >500ms
  if ("external" === 'ant') {
    void import('./utils/eventLoopStallDetector.js').then(m => m.startEventLoopStallDetector());
  }
}

3 个触发条件: - CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER=1 → 跳过(用于 perf 基准测试) - --bare 模式 → 跳过(裸模式不要任何缓存预热) - 否则 → 触发

预取的 12 件事(全部 void = 不 await):

类别 任务 目的
用户上下文 initUser / getUserContext / prefetchSystemContextIfSafe 用户信息预热
提示 getRelevantTips 智能提示
AWS Bedrock prefetchAwsCredentialsAndBedRockInfoIfSafe 3P 凭据预热
GCP Vertex prefetchGcpCredentialsIfSafe 3P 凭据预热
文件统计 countFilesRoundedRg(..., 3s timeout) 仓库规模(rg 计数)
Analytics initializeAnalyticsGates 门控初始化
MCP 官方 prefetchOfficialMcpUrls 官方 MCP 注册表
模型能力 refreshModelCapabilities 模型能力缓存
变更检测 settingsChangeDetector.initialize / skillChangeDetector.initialize 监听文件变化
Event loop startEventLoopStallDetector ANT-ONLY:检测主线程阻塞

关键设计: - 首屏后才跑(注释:runs after first render) - 不阻塞首屏 paint - 用户在输入 prompt 时("typing window")这些 IO 并行


6. loadSettingsFromFlag() — JSON 字符串 → 临时文件(行 432-482)

function loadSettingsFromFlag(settingsFile: string): void {
  try {
    const trimmedSettings = settingsFile.trim();
    const looksLikeJson = trimmedSettings.startsWith('{') && trimmedSettings.endsWith('}');
    let settingsPath: string;
    if (looksLikeJson) {
      // It's a JSON string - validate and create temp file
      const parsedJson = safeParseJSON(trimmedSettings);
      if (!parsedJson) {
        process.stderr.write(chalk.red('Error: Invalid JSON provided to --settings\n'));
        process.exit(1);
      }

      // Create a temporary file and write the JSON to it.
      // Use a content-hash-based path instead of random UUID to avoid
      // busting the Anthropic API prompt cache. The settings path ends up
      // in the Bash tool's sandbox denyWithinAllow list, which is part of
      // the tool description sent to the API. A random UUID per subprocess
      // changes the tool description on every query() call, invalidating
      // the cache prefix and causing a 12x input token cost penalty.
      // The content hash ensures identical settings produce the same path
      // across process boundaries (each SDK query() spawns a new process).
      settingsPath = generateTempFilePath('claude-settings', '.json', {
        contentHash: trimmedSettings
      });
      writeFileSync_DEPRECATED(settingsPath, trimmedSettings, 'utf8');
    } else {
      // It's a file path - resolve and validate by attempting to read
      ...
    }
    setFlagSettingsPath(settingsPath);
    resetSettingsCache();
  } catch (error) {
    ...
  }
}

两种模式: 1. JSON 字符串{...} 开头结尾)→ 写到临时文件 2. 文件路径 → 读取验证

JSON 字符串 → 临时文件的关键设计

settingsPath = generateTempFilePath('claude-settings', '.json', {
  contentHash: trimmedSettings  // ← 内容哈希!
});

为什么用内容哈希而不是 UUID: - settings 路径会出现在 Bash 工具的 sandbox denyWithinAllow 列表里 - 这个列表会传给 API(工具描述的一部分) - 如果用 UUID → 每次 SDK query()(开新进程)路径都不同 - 路径不同 → 工具描述变 → prompt cache 失效12x input token 成本 - 用内容哈希 → 同样 settings 内容 → 同样路径 → cache 命中

这是一个非常隐蔽的 prompt 优化——藏在 --settings 解析里,注释解释了 12x penalty。


7. main() — 进程入口(行 585-855)

export async function main() {
  profileCheckpoint('main_function_start');

  // SECURITY: Prevent Windows from executing commands from current directory
  process.env.NoDefaultCurrentDirectoryInExePath = '1';

  initializeWarningHandler();
  process.on('exit', () => resetCursor());
  process.on('SIGINT', () => {
    if (process.argv.includes('-p') || process.argv.includes('--print')) {
      return;  // 让 print.ts 自己的 handler 处理
    }
    process.exit(0);
  });
  profileCheckpoint('main_warning_handler_initialized');

  // 4 段 argv 预处理(cc://, assistant, ssh, ...)
  if (feature('DIRECT_CONNECT')) { ... }
  if (feature('LODESTONE')) { ... }
  if (feature('KAIROS') && _pendingAssistantChat) { ... }
  if (feature('SSH_REMOTE') && _pendingSSH) { ... }

  // 检查 print / init-only
  const hasPrintFlag = cliArgs.includes('-p') || cliArgs.includes('--print');
  const hasInitOnlyFlag = cliArgs.includes('--init-only');
  const hasSdkUrl = cliArgs.some(arg => arg.startsWith('--sdk-url'));
  const isNonInteractive = hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || !process.stdout.isTTY;

  if (isNonInteractive) stopCapturingEarlyInput();
  setIsInteractive(isInteractive);
  initializeEntrypoint(isNonInteractive);

  // 8 种 clientType
  const clientType = (() => {
    if (isEnvTruthy(process.env.GITHUB_ACTIONS)) return 'github-action';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-ts') return 'sdk-typescript';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-py') return 'sdk-python';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'sdk-cli') return 'sdk-cli';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-vscode') return 'claude-vscode';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'local-agent') return 'local-agent';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop') return 'claude-desktop';
    if (process.env.CLAUDE_CODE_ENTRYPOINT === 'remote' || hasSessionIngressToken) return 'remote';
    return 'cli';
  })();
  setClientType(clientType);

  // ...
  eagerLoadSettings();
  await run();  // 3630 行
}

main() 的 7 步

  1. 安全:Windows PATH hijacking 防御
  2. 信号处理:SIGINT(Ctrl+C)和 exit
  3. argv 预处理(4 个 feature-gated 段):
  4. DIRECT_CONNECTcc:// URL → 重写为 open 子命令
  5. LODESTONE → 深链 URI / macOS URL scheme handler
  6. KAIROSclaude assistant [sessionId]
  7. SSH_REMOTEclaude ssh <host> [dir]
  8. 非交互模式判断isNonInteractive = hasPrint || hasInitOnly || hasSdkUrl || !isTTY
  9. clientType 推断:8 种入口(cli / github-action / sdk-ts / sdk-py / sdk-cli / claude-vscode / local-agent / claude-desktop / remote)
  10. settings 早期加载eagerLoadSettings()(在 run() 之前完成 --settings 解析)
  11. 进入 run():3630 行的 commander 定义

为什么预处理 argv:让 run() 中的 commander 看到的 argv 是"干净的"(如 cc:// 已经被转换为 open 子命令)。


8. run() 的结构 — 3630 行 Commander 巨兽

run() (行 884-4514, ~3630 行)
├── 行 884-906   :Commander 初始化 + help config 排序
├── 行 907-967   :preAction hook — 60 行统一 init 管道
├── 行 968-1006  :主程序选项声明(~40 个 .option())
├── 行 1006-3807 :默认 action handler(**2801 行** — 主流程)
├── 行 3807-3810 :.version() 结束主程序
├── 行 3811-3889 :program.option() 补充(worktree、tmux、ANT-ONLY 选项)
├── 行 3894-4490 :subcommands(10+ 个)— mcp / server / ssh / open / setup-token / agents /
│                 remote-control / assistant / doctor / update / up / rollback / install / log /
│                 error / export / completion / plugin / auth
├── 行 4492-4514 :program.parseAsync() + profileReport()
└── 行 4514      :return program

核心洞察:default .action() 占 2801 行(77% 的 run())——所有"非子命令"的逻辑(interactive 模式、print 模式、resume、teleport、continue、IDE、remote 等)都堆在 一个回调 里。


9. preAction hook — 60 行统一 init 管道(行 907-967)

program.hook('preAction', async thisCommand => {
  profileCheckpoint('preAction_start');

  // 1. 等待 2 个 subprocess
  await Promise.all([ensureMdmSettingsLoaded(), ensureKeychainPrefetchCompleted()]);
  profileCheckpoint('preAction_after_mdm');

  // 2. 核心 init
  await init();
  profileCheckpoint('preAction_after_init');

  // 3. 终端 title
  if (!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE)) {
    process.title = 'claude';
  }

  // 4. 初始化 logging sinks
  const { initSinks } = await import('./utils/sinks.js');
  initSinks();
  profileCheckpoint('preAction_after_sinks');

  // 5. --plugin-dir inline 处理(gh-33508)
  const pluginDir = thisCommand.getOptionValue('pluginDir');
  if (Array.isArray(pluginDir) && pluginDir.length > 0 && ...) {
    setInlinePlugins(pluginDir);
    clearPluginCache('preAction: --plugin-dir inline plugins');
  }

  // 6. 迁移老版本 settings
  runMigrations();
  profileCheckpoint('preAction_after_migrations');

  // 7. 企业远程设置(fire-and-forget)
  void loadRemoteManagedSettings();
  void loadPolicyLimits();
  profileCheckpoint('preAction_after_remote_settings');

  // 8. 用户设置上传
  if (feature('UPLOAD_USER_SETTINGS')) {
    void import('./services/settingsSync/index.js').then(m => m.uploadUserSettingsInBackground());
  }
});

8 步(6 个 profileCheckpoint): 1. await MDM + keychain 预取 2. init() —— 核心初始化 3. 终端 title 设置(macOS/Windows) 4. initSinks() —— 异步日志 sink 5. --plugin-dir 内联插件处理 6. runMigrations() —— 老配置迁移 7. 企业远程设置(fire-and-forget) 8. 用户设置上传(fire-and-forget)

为什么用 preAction 而不是 action 之前手写: - 子命令(mcp, plugin, auth 等)也走这个 hook - 避免每个子命令都重写 init 代码 - preAction 是 Commander 的标准机制

注释里点出 PR #11106 的修复:logEvent 在 sink attach 之前会默默丢失——initSinks() 必须幂等


10. 主程序选项 — 40 个 flag(行 968-1006)

.option() / .addOption() 的链式调用,40+ 个 flag:

类别 例子
调试 --debug, --debug-to-stderr, --debug-file, --verbose
模式 --print, --bare, --output-format, --input-format
权限 --dangerously-skip-permissions, --permission-mode, --allowed-tools
会话 --continue, --resume, --fork-session, --session-id
模型 --model, --effort, --thinking, --fallback-model
上下文 --system-prompt, --append-system-prompt, --add-dir
MCP --mcp-config, --strict-mcp-config, --mcp-debug
插件 --plugin-dir, --disable-slash-commands
Hooks --include-hook-events
预算 --max-turns, --max-budget-usd, --task-budget
团队 --agent, --agent-id, --agent-name, --team-name, --agent-color
远程 --sdk-url, --teleport, --remote, --remote-control, --rc

每个 flag 都带完整 help 文本(行 971-1000 全是 help string)。

特殊构造.addOption(new Option(...).argParser(...)) —— 复杂的 argParser 用于校验。

.addOption(new Option('--max-budget-usd <amount>', '...').argParser(value => {
  const amount = Number(value);
  if (isNaN(amount) || amount <= 0) {
    throw new Error('--max-budget-usd must be a positive number greater than 0');
  }
  return amount;
}))

11. 默认 action handler — 2801 行巨兽(行 1006-3807)

这是文件的"心脏"。没有拆成子函数——所有模式都挤在一个 async 回调里。

11.1 处理顺序概览

.action(async (prompt, options) => {
  // 1. 解析 options (~50 行)
  let outputFormat = options.outputFormat;
  let inputFormat = options.inputFormat;
  let verbose = options.verbose ?? getGlobalConfig().verbose;
  let print = options.print;
  // ...

  // 2. --bare 模式(env var 设置)
  if (options.bare) process.env.CLAUDE_CODE_SIMPLE = '1';

  // 3. 'code' 关键字特殊处理
  if (prompt === 'code') { /* 提示用 `claude` 启动 */ }

  // 4. KAIROS / assistant 模式(信任检查 + team 初始化)
  let kairosEnabled = false;
  let assistantTeamContext;
  if (feature('KAIROS') && ...) { ... }
  if (feature('KAIROS') && assistantModule?.isAssistantMode() && ...) { ... }

  // 5. worktree / tmux 校验
  if (tmuxEnabled) { /* 平台 + 安装检查 */ }

  // 6. teammate 选项
  if (isAgentSwarmsEnabled()) { /* 提取 agent id/name/team */ }

  // 7. remote sdk options
  // 8. IDE / remote / teleport / sandbox / etc.
  // 9. ... (几十个 if 块)
  // 10. 真实执行:print → runHeadless; interactive → launchRepl
});

这种"超长 action" 的原因: - 所有 options 互相影响(如 --print + --bare + --continue + --resume + --teleport + --worktree + ...) - 拆函数会增加跳转、难理解状态机 - 注释里写"1000 lines before showSetupScreens()"——这个 action 真的做了很多

11.2 print 模式分支

if (print) {
  // ... 设置 headless store
  void runHeadless(inputPrompt, () => headlessStore.getState(), headlessStore.setState, commandsHeadless, tools, sdkMcpConfigs, agentDefinitions.activeAgents, {
    ...
  });
}

runHeadlesscli/print.ts 的入口——单轮、无 REPL。

11.3 interactive 模式分支(多 launchRepl 调用点)

await launchRepl(root, { ... });  // 普通 interactive
await launchRepl(root, { ... });  // --teleport
await launchRepl(root, { ... });  // --continue
await launchRepl(root, { ... });  // --resume
await launchRepl(root, { ... });  // --teleport 直接
await launchRepl(root, { ... });  // 默认

为什么这么多 launchRepl 调用:每个模式(continue / resume / teleport / sandbox / IDE)先做自己的 setup(如 resume 加载历史、teleport 验证仓库),最后都委托给 launchRepl 起 REPL。

11.4 7 个 launchRepl 调用点(grep 验证)

行号 触发模式
3134 默认 interactive
3176 continue
3242 resume (string)
3338 resume + IDE
3487 teleport
3733 worktree
3798 (推测:另一个分支)

12. 9 个 subcommand(行 3894-4490)

run() 在 default action handler 之后,继续注册 subcommand:

Subcommand 处理函数 模式
mcp 3894 mcpServeHandler / mcpAddHandler / mcpListHandler / 等 9 个子命令
server 3962 直接 inline(HTTP server 启动) DIRECT_CONNECT
ssh 4046 inline SSH_REMOTE
open 4059 inline DIRECT_CONNECT
auth 4100 lazy import 多子命令
plugin 4148 registerPluginCommand 多子命令
setup-token 4267 inline 一次性
agents 4278 inline 列出
auto-mode 4289 inline (ANT-ONLY) TRANSCRIPT_CLASSIFIER
remote-control 4323 inline BRIDGE
assistant 4335 inline KAIROS
doctor 4346 inline 健康检查
update / upgrade 4362 inline 自动更新
up 4371 inline (ANT-ONLY) 内部
rollback 4382 inline (ANT-ONLY) 内部
install 4395 inline 安装
log 4412 inline (ANT-ONLY) 内部
error 4420 inline (ANT-ONLY) 内部
export 4428 inline (ANT-ONLY) 内部
task 4440 inline (ANT-ONLY) 内部
completion 4492 inline shell 补全

20+ 个 subcommand!每个都可能有自己的子命令(如 mcp 有 9 个子命令、plugin 有 10+ 个)。

模式: - 公开 subcommand:直接 inline - 复杂 subcommand:lazy import 委托给 cli/handlers/*.js - ANT-ONLY:包在 if ("external" === 'ant')


13. parseAsync 之前的最后拦截(行 3880-3890)

const isPrintMode = process.argv.includes('-p') || process.argv.includes('--print');
const isCcUrl = process.argv.some(a => a.startsWith('cc://') || a.startsWith('cc+unix://'));
if (isPrintMode && !isCcUrl) {
  profileCheckpoint('run_before_parse');
  await program.parseAsync(process.argv);
  profileCheckpoint('run_after_parse');
  return program;
}

// claude mcp
const mcp = program.command('mcp').description(...);

关键isPrintMode && !isCcUrl → 直接 parse 后返回

这意味着 print 模式 + 非 cc:// 情况下,所有 subcommand 注册代码都不会跑——节省了大量 startup 时间。

对比: - claude -p "hi" → parse + 返回(不注册 20+ subcommand) - claude mcp list → 注册所有 subcommand + parse

这是为什么 subcommand 启动慢——所有定义都执行了。


14. 关键设计模式

14.1 profileCheckpoint 散布

profileCheckpoint('main_tsx_entry');
profileCheckpoint('main_function_start');
profileCheckpoint('main_warning_handler_initialized');
profileCheckpoint('main_client_type_determined');
profileCheckpoint('main_before_run');
profileCheckpoint('main_after_run');
// ... preAction 里 8 个

总计 30+ 个埋点profileReport() 在 run() 末尾触发 Statsig 上报(采样)+ 控制台输出。

14.2 "external" === 'ant' 编译时 DCE 门

if ("external" === 'ant') {  // 编译时为 false → 整段砍掉
  program.addOption(new Option('--delegate-permissions', '...').implies({ ... }));
}
if ("external" !== 'ant' && isBeingDebugged()) {  // 商业版生效
  process.exit(1);
}

'ant' 字符串字面量被 bun:bundle 替换 —— "external" === 'ant' 直接常量折叠为 false/true → 整段不被编译进 bundle。

这个项目有大量这样的门 —— 估计 50+ 处。

14.3 eagerParseCliFlag — 跳过 Commander 直接读 argv

const settingsFile = eagerParseCliFlag('--settings');
if (settingsFile) {
  loadSettingsFromFlag(settingsFile);
}

为什么不用 commander:commander 需要 parse 整个 argv(耗时间),而 --settingsrun() 开始时就需要——手动 grep argv 提前加载。

14.4 Feature-gated 整个程序

if (feature('COORDINATOR_MODE') && coordinatorModeModule) { ... }
if (feature('KAIROS') && assistantModule?.isAssistantMode() && !spawnedTeammate) { ... }
if (feature('DIRECT_CONNECT') && process.argv.includes('cc://')) { ... }

模式feature('X') 编译时决定 + 模块级 null 兜底 + 运行时 argv 决定 → 三重门控

14.5 防御性:try/catch 包 analytics

function logManagedSettings(): void {
  try {
    const policySettings = getSettingsForSource('policySettings');
    if (policySettings) { ... }
  } catch {
    // Silently ignore errors - this is just for analytics
  }
}

模式:所有 telemetry 函数都 swallow 异常——绝不让 metrics 影响主流程

14.6 注释密度极高

例如:

// SECURITY: Prevent Windows from executing commands from current directory
// This must be set before ANY command execution to prevent PATH hijacking attacks
// See: https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-searchpathw
process.env.NoDefaultCurrentDirectoryInExePath = '1';

3 行注释解释 1 行代码——这是项目风格:解释为什么,不解释是什么。

14.7 gh-XXXXX 编号贯穿全文

// gh-33508: --plugin-dir is a top-level program option. The default
// action reads it from its own options destructure, but subcommands
// (plugin list, plugin install, mcp *) have their own actions and
// never see it. Wire it up here so getInlinePlugins() works everywhere.

gh-XXXXX 是 GitHub issue 编号——直接关联具体 bug。

14.8 @[MODEL LAUNCH] 标注

// @[MODEL LAUNCH]: Update the example model ID in the --model help text.
.option('--model <model>', `Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6').`)

发布前 checklist 标记——模型发布时要改 example。


15. 与其他文件的关系

main.tsx
  ├──→ utils/startupProfiler.js (profileCheckpoint)
  ├──→ utils/settings/mdm/rawRead.js (startMdmRawRead)
  ├──→ utils/secureStorage/keychainPrefetch.js (startKeychainPrefetch)
  ├──→ entrypoints/init.js (init)
  ├──→ utils/sinks.js (initSinks)
  ├──→ utils/migrations.js (runMigrations)
  ├──→ services/remoteManagedSettings/index.js (loadRemoteManagedSettings)
  ├──→ services/policyLimits/index.js (loadPolicyLimits)
  ├──→ replLauncher.js (launchRepl)
  ├──→ cli/print.ts (runHeadless)
  ├──→ components/TeleportProgress.js (teleportWithProgress)
  ├──→ server/server.js + sessionManager.js (server subcommand)
  ├──→ cli/handlers/mcp.js (mcp subcommands)
  ├──→ services/plugins/pluginCliCommands.js (plugin subcommands)
  ├──→ services/auth/authCommands.js (auth subcommands)
  ├──→ commands/... (all subcommands)
  └──→ 20+ subcommand handlers

main.tsx 是"调度中心"——它不实现具体逻辑,只编排: 1. 启动优化(行 1-100) 2. argv 解析(run 主体) 3. 委托给 handlers(subcommands) 4. 委托给 launchRepl / runHeadless(default action)


16. 行数分布

占比
Imports + 顶部副作用 1-210 4.5%
13 个 helper 函数 211-583 8%
main() 585-855 6%
getInputPrompt 857-882 0.5%
run() Commander 初始化 884-906 0.5%
preAction hook 907-967 1.3%
主程序选项 968-1006 0.8%
默认 action handler 1006-3807 61%
主程序收尾 + worktree 选项 3807-3889 1.8%
20+ subcommand 定义 3894-4490 13%
parseAsync + 收尾 4492-4514 0.5%
6 个小 helper 4514-4700 4%

默认 action 占 61% —— 这是为什么"理解 main.tsx"等于"理解 claude code 的所有入口"。


17. 阅读建议

  1. 不要从头读到尾——会迷失
  2. main() 开始(行 585)—— 看 7 步流程
  3. 看 preAction hook(行 907)—— 理解 init 管道
  4. 跳到 run() 末尾(行 3880)—— 看 subcommand 注册
  5. 回头看 default action(行 1006)—— 看 print vs interactive 分支
  6. 再读顶部 100 行——理解 3 个并行预取

18. 关键洞察

18.1 启动优化的"4 层并行"

  1. 模块求值 时 fire MDM + keychain subprocess(与 import 并行)
  2. preAction 时 await 这 2 个(绝大多数已完成)
  3. startDeferredPrefetches 触发 12 个 fire-and-forget(首屏后)
  4. runHeadless / launchRepl 真正运行

18.2 30+ profileCheckpoint 是"启动性能可观测性"

  • 每次启动埋 30+ 点
  • 上报 Statsig(采样)
  • profileReport() 控制台输出总时间
  • 能找到瓶颈(如某段 100ms → 优化)

18.3 feature('X') + lazy require 是"打包友好"双保险

  • DCE 在编译时砍死代码
  • null 兜底保证运行时安全
  • feature('X') && argv check 是运行时二次门控

18.4 --settings 的内容哈希是"prompt cache 优化的细节"

  • 12x input token 节省
  • 注释里写得很清楚

18.5 Print 模式 fast-path 跳过 20+ subcommand 注册

  • 节省 ~50-100ms 启动时间
  • claude -p "hi"claude(无 prompt)更快?

18.6 default action 2801 行 是技术债也是设计选择

  • 缺点:难以 navigate
  • 优点:所有模式互相影响的逻辑集中可见
  • 项目里其他大文件(5000+ 行)也是这个模式

18.7 安全意识深入骨髓

  • Windows PATH hijacking 防御
  • 反调试
  • 信任对话框
  • --dangerously-skip-permissions 警告
  • prefetchSystemContextIfSafe 命名都带 "Safe"

19. 与其他深度拆解的关系

文件 与 main.tsx 的关系
bridge/replBridge.ts launchRepl 内部使用
cli/print.ts runHeadless 实现(print 模式)
entrypoints/init.js await init() 实现
replLauncher.ts launchRepl 实现
commands.ts getCommands 提供给 REPL
tools.ts getTools 提供给 REPL

20. 阅读清单

  1. ✅ 通读 src/main.tsx 1-100 行(理解并行预取)
  2. ✅ 读 main() 行 585-855(7 步流程)
  3. ✅ 读 preAction hook 行 907-967(init 管道)
  4. ✅ 跳看 default action 行 1006-1200(前半结构)
  5. ✅ 跳看 print 模式分支(grep runHeadless
  6. ✅ 跳看 7 个 launchRepl 调用点(grep 一下)
  7. ✅ 跳看 20+ subcommand 定义行 3894-4490
  8. 📌 对照 topics/deep-dive-repl-screen.md 看 REPL 怎么起来
  9. 📌 对照 topics/deep-dive-bridge-main.md 看 Bridge 怎么 hook

21. 练习任务

  1. 数一下 30+ profileCheckpoint 名字 —— 理解启动瓶颈
  2. grep feature('X') 找所有 feature flags —— 看看 DCE 砍了哪些
  3. grep if ("external" === 'ant') —— 数 ANT-ONLY 段有多少
  4. 找 7 个 launchRepl 调用 —— 对比它们的 options 区别
  5. 手写一个最简 main.tsx(~30 行)—— Commander + 1 个 subcommand
  6. 思考:default action 2801 行能否拆成小函数?拆了有什么得失?