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.js | TUI 渲染引擎 |
| 核心 | ./query.js, ./QueryEngine.js | LLM 查询循环 |
| 配置 | ./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.ts → AppState.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— 权限接受迁移migrateSonnet1mToSonnet45→migrateSonnet45ToSonnet46→migrateOpusToOpus1m— 模型名变更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 消费:
_pendingConnect—cc://直连_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 之前运行,负责初始化基础设施:
- 等待 MDM 和钥匙串(第914行)——
ensureMdmSettingsLoaded()+ensureKeychainPrefetchCompleted(),这些子进程是在 import 阶段启动的,此时只 await 它们完成 - init()(第916行)—— 核心初始化:加载 settings、处理 env vars、配置 logger
- 设置 process.title(第922-924行)
- 初始化 log sink(第934行)—— 确保子命令能打 logEvent
- 处理
--plugin-dir(第945-949行) - 运行数据迁移(第950行)——
runMigrations() - 加载远程受管设置(第957-958行)—— enterprise policy 下发
- 上传 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-config | MCP 配置 |
--system-prompt / --system-prompt-file | 系统提示覆盖 |
--continue, --resume | 会话恢复 |
--worktree, --tmux | 工作树隔离 |
--settings, --setting-sources | 设置来源 |
部分选项使用 .hideHelp() 隐藏(如 --sdk-url、--teleport、--agent-id),这些是供 SDK 或内部使用的,不暴露给终端用户。
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 配置加载:
- 每一项先尝试解析为 JSON 对象
- 失败则尝试作为文件路径读取
- 动态作用域的配置会覆盖文件中的同名配置
- 用 enterprise policy 过滤被阻止的服务器
- 检查保留名称(
claude_in_chrome、computer_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)
模型选择优先级链:
- CLI 参数
--model('default'映射到默认模型) - 环境变量
ANTHROPIC_MODEL - Agent 定义中的 model(如果
!=='inherit') - settings 中的 model
- 全局默认模型
命令与 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 设置启用。
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 flagsrefreshGrowthBookAfterAuthChange()— 刷新 GrowthBook(特性开关系统),获取更新后的 feature flag,例如 claude.ai 的 MCP 权限Trusted Device处理 — 清除旧的受信任设备 token 然后重新注册,用于 Remote Control(远程控制)功能。两者都通过 tengu_sessions_elevated_auth_enforcement 开关自检内部控制
为什么要按这个顺序
注释里特别强调了一个关键依赖:resetUserCache() 必需在refreshGrowthBookAfterAuthChange() 之前执行,因为 GrowthBook
需要拿最新的用户凭证去拉取 feature flags。如果顺序反了,可能拿到旧的 flag
配置。
信任后初始化(2308-2336)
信任对话框被接受之后(或非交互模式隐式信任):
- 初始化 LSP manager(第2321行)——延迟到信任后,防止插件 LSP 服务器在未信任目录中执行代码
- 显示 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 配置被分为两类:
sdkMcpConfigs—type: 'sdk'的配置,由 SDK 管理regularMcpConfigs— 常规 stdio/SSE 配置
会话生命周期(2496-2542)
会话注册**(第2530行)
PID 文件写入,并发会话检测(≥2 个并发时打 telemetry)
非交互模式(--print)(2585-2861)
当 isNonInteractiveSession 为 true 时,执行 headless 路径:
- 环境变量应用(第2593行):
applyConfigEnvironmentVariables()— 在非交互模式中信任是隐式的 - Telemetry 初始化(第2597行):需要 env vars 中的 OTEL 端点
- SessionStart hooks(第2607行):与 MCP 连接并行执行
- Org 验证(第2614-2618行)
- 创建 headless store(第2653行):
createStore(headlessInitialState, onChangeAppState) - MCP 批量连接(第2691-2809行):逐个推送 pending → 替换为 connected/failed,claude.ai connectors 有 5s 超时
- 后台预热(第2816-2822行)
- 导入并执行 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) |
agentNameRegistry | Agent 名称 → ID 映射(SendMessage 路由用) |
toolPermissionContext | 权限上下文 |
mcp | MCP 客户端/工具/命令 |
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) 会话:
--remote "description"— 创建新远程会话,可选 TUI 模式--teleport <sessionId>— 恢复已有远程会话--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行)— 列出配置的 agentauto-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 后 |
| 惰性 import | 70-82行 | 条件编译 + 循环依赖规避 |
| 热缓存 | 2496行 getUserContext() | 预热后后续调用为 cache hit |
| 首屏不阻塞 | 3765行 pendingHookMessages | hooks 延迟到第一次 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 → 非交互模式