Claude Code 交互式启动流程与信任边界深度分析
交互式会话启动总览
Claude Code 的交互式启动流程是一个精心设计的顺序对话瀑布(sequential dialog waterfall),核心目标是在进入 REPL 之前完成所有安全检查、用户确认和环境初始化。
整个流程由 showSetupScreens() 统一编排(src/interactiveHelpers.tsx:104-298),关键入口链路为:
CLI 入口 → setup.ts (环境初始化) → main.tsx (Ink root 创建) → showSetupScreens() → REPL
完整对话序列(showSetupScreens 瀑布)
前置条件检查(第 105 行)
if ("production" === 'test' || isEnvTruthy(false) || process.env.IS_DEMO) {
return false;
}
测试环境和 DEMO 模式跳过所有 setup 对话。
第 1 步:Onboarding 对话(第 111-123 行)
- 触发条件:
!config.theme || !config.hasCompletedOnboarding - 做什么:首次运行时显示主题选择和功能引导
- 门控:全局配置中的
hasCompletedOnboarding和lastOnboardingVersion - 关键路径:动态 import
./components/Onboarding.js(懒加载,非首次不加载) - 副作用:调用
completeOnboarding()写入全局配置
第 2 步:Trust 信任对话(第 131-140 行)⭐ 安全边界
- 触发条件:
!isEnvTruthy(process.env.CLAUBBIT)且!checkHasTrustDialogAccepted() - 做什么:询问用户是否信任当前工作区
- 门控:几乎所有后续安全功能的唯一入口
- 快速路径(第 135 行):如果 CWD 已被信任过,跳过 import + render
- 组件:
TrustDialog(src/components/TrustDialog/TrustDialog.tsx) - 两个选项:
"Yes, I trust this folder"/"No, exit"
信任后:序贯解锁(第 144-176 行)
按顺序执行以下操作:
setSessionTrustAccepted(true)— 会话级信任标志resetGrowthBook()+void initializeGrowthBook()— 重初始化 GrowthBookvoid getSystemContext()— 预取系统上下文handleMcpjsonServerApprovals(root)— MCP 服务器审批- ClaudeMd 外部包含检查(第 164-170 行)— 如果有外部 CLAUDE.md 引用需要确认
第 3 步:环境变量应用(第 184 行)
applyConfigEnvironmentVariables();
信任建立后才应用全部环境变量(包括危险的 LD_PRELOAD、PATH 等)。
第 4 步:遥测初始化(第 190 行)
setImmediate(() => initializeTelemetryAfterTrust());
延迟到下一个 tick,确保 OTel 动态 import 在首次渲染之后解析。
第 5 步:Grove 策略对话(第 191-201 行)
- 触发条件:
await isQualifiedForGrove()返回 true - 做什么:显示 Grove 使用策略
- 门控:用户按 Escape 则退出整个会话
第 6 步:自定义 API Key 审批(第 206-217 行)
- 触发条件:存在
ANTHROPIC_API_KEY环境变量 且 非 Homespace - 做什么:用户确认使用自定义 API Key
- 门控:
getCustomApiKeyStatus() === 'new'(新 key 需要确认)
第 7 步:bypassPermissions 模式警告(第 218-223 行)
- 触发条件:
permissionMode === 'bypassPermissions'且!hasSkipDangerousModePermissionPrompt() - 做什么:显示危险模式警告,用户必须确认
- 安全意义:bypassPermissions 会跳过所有工具执行权限检查
第 8 步:Auto Mode Opt-In 对话(第 224-235 行)
- 触发条件:
feature('TRANSCRIPT_CLASSIFIER')且permissionMode === 'auto'且未 opt-in - 做什么:用户同意自动模式
- 门控:
declineExits === true,拒绝则退出
第 9 步:Dev Channels 对话(第 241-288 行)
- 触发条件:
feature('KAIROS')且有 dev channels 需要确认 - 做什么:开发者渠道确认
- 前置检查:
checkGate_CACHED_OR_BLOCKING('tengu_harbor')— GrowthBook gate
第 10 步:Chrome Onboarding(第 291-296 行)
- 触发条件:
claudeInChrome且首次使用 - 做什么:Claude in Chrome 首次引导
信任边界设计
信任对话是什么
TrustDialog(src/components/TrustDialog/TrustDialog.tsx:23-264)是 Claude Code 的核心安全边界。它检查当前工作区的以下潜在风险:
| 检查项 | 函数 | 说明 |
|---|---|---|
| MCP 服务器 | getMcpConfigsByScope("project") | 项目级 MCP 配置 |
| Hooks | getHooksSources() | .claude/settings.json 和 .claude/settings.local.json 中的 hooks |
| Bash 权限 | getBashPermissionSources() | 允许 Bash 执行的权限规则 |
| API Key Helper | getApiKeyHelperSources() | 自定义 API Key 获取脚本 |
| AWS 命令 | getAwsCommandsSources() | AWS 认证刷新/导出命令 |
| GCP 命令 | getGcpCommandsSources() | GCP 认证刷新命令 |
| OTel Headers | getOtelHeadersHelperSources() | 遥测 header 获取脚本 |
| 危险环境变量 | getDangerousEnvVarsSources() | 不在 SAFE_ENV_VARS 白名单中的环境变量 |
| 命令中的 Bash | commands 检查 | 自定义命令/skills 中使用 Bash 工具的 |
所有检查逻辑定义在 src/components/TrustDialog/utils.ts 中。
信任对话门控了什么
信任建立后解锁以下功能(按代码顺序):
setSessionTrustAccepted(true)
├── GrowthBook 初始化(附带 auth headers)
├── 系统上下文预取
├── MCP 服务器审批
├── CLAUDE.md 外部包含检查
├── GitHub repo 路径映射
├── 全部环境变量应用(含危险的 LD_PRELOAD/PATH)
├── 遥测初始化(OTel、exporters)
├── Grove 策略对话
└── 后续所有对话...
信任前只能应用安全的环境变量(来自可信来源:userSettings、flagSettings、policySettings),见 src/utils/managedEnv.ts:105-109:
const TRUSTED_SETTING_SOURCES = [
'userSettings', // ~/.claude/settings.json
'flagSettings', // --settings CLI 参数
'policySettings', // 企业管理配置
] as const;
信任后才应用项目级环境变量(可能被恶意提交的 .claude/settings.json 中的 ANTHROPIC_BASE_URL 等)。
信任状态的持久化
信任状态通过两层机制管理(src/utils/config.ts:697-743):
- 会话级(内存):
setSessionTrustAccepted()/getSessionTrustAccepted()— 用于 Home 目录等不持久化的场景 - 项目级(磁盘):
hasTrustDialogAccepted字段写入~/.claude.json的projects[path]
checkHasTrustDialogAccepted() 使用 latch 模式(只 false→true 转换):
let _trustAccepted = false;
export function checkHasTrustDialogAccepted(): boolean {
return (_trustAccepted ||= computeTrustDialogAccepted());
}
computeTrustDialogAccepted() 还会向上遍历目录树查找祖先目录的信任状态。
安全含义
信任对话是 Claude Code 防止供应链攻击的关键防线:
- 恶意
.claude/settings.json:攻击者可以在项目中提交包含ANTHROPIC_BASE_URL的配置,将 API 请求重定向到攻击者服务器 - 恶意 Hooks:项目级 hooks 可以在每次工具执行前后运行任意代码
- 恶意 MCP 服务器:项目级 MCP 配置可能连接不受控的服务器
- 危险环境变量:
LD_PRELOAD等可以注入恶意共享库
Onboarding 流程(Workspace + CLAUDE.md 步骤)
项目级 onboarding 状态定义在 src/projectOnboardingState.ts 中:
export function getSteps(): Step[] {
const hasClaudeMd = getFsImplementation().existsSync(join(getCwd(), 'CLAUDE.md'));
const isWorkspaceDirEmpty = isDirEmpty(getCwd());
return [
{
key: 'workspace',
text: 'Ask Claude to create a new app or clone a repository',
isComplete: false,
isCompletable: true,
isEnabled: isWorkspaceDirEmpty, // 只在空目录时启用
},
{
key: 'claudemd',
text: 'Run /init to create a CLAUDE.md file with instructions for Claude',
isComplete: hasClaudeMd,
isCompletable: true,
isEnabled: !isWorkspaceDirEmpty, // 只在非空目录时启用
},
];
}
显示条件(shouldShowProjectOnboarding()):
- 未完成
hasCompletedProjectOnboarding - 查看次数 < 4(
projectOnboardingSeenCount < 4) - 非 DEMO 模式
自动完成检测:maybeMarkProjectOnboardingComplete() 在每次 prompt 提交时检查,通过 isProjectOnboardingComplete() 判断所有可完成步骤是否已完成。
启动性能优化
并行预取模式(startDeferredPrefetches)
src/main.tsx:388-431 — 在 REPL 渲染后调用,不影响首帧:
export function startDeferredPrefetches(): void {
if (isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER) || isBareMode()) {
return; // benchmark 模式或脚本模式跳过
}
// 进程级预取(用户还在打字时完成)
void initUser();
void getUserContext();
prefetchSystemContextIfSafe();
void getRelevantTips();
// 云凭证预取
void prefetchAwsCredentialsAndBedRockInfoIfSafe();
void prefetchGcpCredentialsIfSafe();
// 文件系统探测
void countFilesRoundedRg(getCwd(), AbortSignal.timeout(3000), []);
// 分析和 feature gate 初始化
void initializeAnalyticsGates();
void prefetchOfficialMcpUrls();
void refreshModelCapabilities();
// 文件变更检测器
void settingsChangeDetector.initialize();
void skillChangeDetector.initialize();
}
懒动态 import 策略
showSetupScreens() 中每个对话组件都使用 await import(...) 动态导入,只有在需要显示时才加载:
| 对话 | 动态 import 路径 |
|---|---|
| Onboarding | ./components/Onboarding.js |
| TrustDialog | ./components/TrustDialog/TrustDialog.js |
| ClaudeMdExternalIncludes | ./components/ClaudeMdExternalIncludesDialog.js |
| Grove | src/components/grove/Grove.js |
| ApproveApiKey | ./components/ApproveApiKey.js |
| BypassPermissionsMode | ./components/BypassPermissionsModeDialog.js |
| AutoModeOptIn | ./components/AutoModeOptInDialog.js |
| DevChannels | ./components/DevChannelsDialog.js |
| ClaudeInChrome | ./components/ClaudeInChromeOnboarding.js |
init.ts 中的安全分层
src/entrypoints/init.ts:57 中的 init() 使用 memoize() 保证只执行一次:
- 信任前:
applySafeConfigEnvironmentVariables()— 只应用安全的 env vars - 信任后:
applyConfigEnvironmentVariables()— 应用全部 env vars
遥测初始化被延迟到 setImmediate() 确保 OTel 动态 import 不阻塞首帧。
帧时序与闪烁检测
FpsTracker
src/utils/fpsTracker.ts — 极简帧率追踪器:
export class FpsTracker {
private frameDurations: number[] = [];
record(durationMs: number): void { /* 记录每帧耗时 */ }
getMetrics(): FpsMetrics | undefined {
// averageFps = totalFrames / totalTime
// low1PctFps = 1000 / p99FrameTime (最差 1% 帧率)
}
}
帧时序 Bench 模式
src/interactiveHelpers.tsx:319-341 — 通过 CLAUDE_CODE_FRAME_TIMING_LOG 环境变量启用:
const frameTimingLogPath = process.env.CLAUDE_CODE_FRAME_TIMING_LOG;
// 每帧记录 JSONL:
// { total, renderer, diff, optimize, write, patches, yoga, commit,
// yogaVisited, yogaMeasured, yogaCacheHits, yogaLive, rss, cpu }
帧阶段定义在 src/ink/frame.ts:42-65:
| 阶段 | 含义 |
|---|---|
renderer | DOM → yoga layout → screen buffer |
diff | screen diff → Patch[] |
optimize | patch merge/dedupe |
write | 序列化 patches → ANSI → stdout |
yoga | calculateLayout() 时间 |
commit | React reconcile 时间 |
yogaVisited | layoutNode() 调用次数 |
yogaMeasured | text wrap/width 调用次数 |
yogaCacheHits | _hasL 缓存命中次数 |
yogaLive | 存活的 yoga Node 实例数(增长=泄漏) |
闪烁检测
src/ink/ink.tsx:604-618 — 检测 clearTerminal 类型的 patch:
const flickers: FrameEvent['flickers'] = [];
for (const patch of diff) {
if (patch.type === 'clearTerminal') {
flickers.push({
desiredHeight: frame.screen.height,
availableHeight: frame.viewport.height,
reason: patch.reason // 'resize' | 'offscreen' | 'clear'
});
}
}
闪烁报告逻辑(src/interactiveHelpers.tsx:343-361):
- 支持同步输出的终端(DEC 2026 BSU/ESU)跳过闪烁报告
- 过滤
resize原因的闪烁 - 1 秒去重,避免刷屏
- 上报
tengu_flicker事件,包含desiredHeight、actualHeight、reason
src/ink/frame.ts:105-124 的 shouldClearScreen() 判断何时需要清屏:
- 终端尺寸变化 →
'resize' - 当前帧高度 ≥ 视口高度 →
'offscreen' - 前一帧高度 ≥ 视口高度 →
'offscreen'
对话渲染架构
showDialog(第 39-44 行)
最基础的对话渲染函数:
export function showDialog<T = void>(
root: Root,
renderer: (done: (result: T) => void) => React.ReactNode
): Promise<T> {
return new Promise<T>(resolve => {
const done = (result: T): void => void resolve(result);
root.render(renderer(done));
});
}
- 将 React 节点渲染到 Ink root
- 返回 Promise,由
done回调解析 - 对话组件通过调用
done(result)通知调用者
showSetupDialog(第 86-92 行)
showDialog 的封装,自动添加 AppStateProvider + KeybindingSetup 包装:
export function showSetupDialog<T = void>(
root: Root,
renderer: (done: (result: T) => void) => React.ReactNode,
options?: { onChangeAppState?: typeof onChangeAppState }
): Promise<T> {
return showDialog<T>(root, done =>
<AppStateProvider onChangeAppState={options?.onChangeAppState}>
<KeybindingSetup>{renderer(done)}</KeybindingSetup>
</AppStateProvider>
);
}
这确保每个 setup 对话都有正确的状态管理和键绑定上下文。
renderAndRun(第 98-103 行)
主 UI 渲染入口,处理通用的收尾工作:
export async function renderAndRun(root: Root, element: React.ReactNode): Promise<void> {
root.render(element);
startDeferredPrefetches(); // 启动延迟预取
await root.waitUntilExit();
await gracefulShutdown(0); // 优雅关闭
}
启动期间的错误处理
exitWithError(第 52-57 行)
Ink 已 patchConsole 后,console.error 会被吞掉,所以必须通过 React 树渲染错误:
export async function exitWithError(root: Root, message: string, beforeExit?: () => Promise<void>): Promise<never> {
return exitWithMessage(root, message, { color: 'error', beforeExit });
}
exitWithMessage(第 65-80 行)
export async function exitWithMessage(root: Root, message: string, options?): Promise<never> {
const { Text } = await import('./ink.js');
root.render(color ? <Text color={color}>{message}</Text> : <Text>{message}</Text>);
root.unmount();
await options?.beforeExit?.();
process.exit(exitCode);
}
注意 ./ink.js 是动态 import,避免在错误路径中过早加载。
启动性能分析工具
src/utils/startupProfiler.ts 提供两种模式:
- 采样日志:100% ant 用户 + 0.5% 外部用户,阶段数据上报 Statsig
- 详细分析:
CLAUDE_CODE_PROFILE_STARTUP=1,完整报告含内存快照
阶段定义:
const PHASE_DEFINITIONS = {
import_time: ['cli_entry', 'main_tsx_imports_loaded'],
init_time: ['init_function_start', 'init_function_end'],
settings_time: ['eagerLoadSettings_start', 'eagerLoadSettings_end'],
total_time: ['cli_entry', 'main_after_run'],
};
Mermaid 流程图
关键代码路径汇总
| 功能 | 文件 | 行号 |
|---|---|---|
| 启动编排 | src/interactiveHelpers.tsx | 104-298 |
| 对话渲染原语 | src/interactiveHelpers.tsx | 39-103 |
| Trust Dialog UI | src/components/TrustDialog/TrustDialog.tsx | 23-264 |
| Trust 检查工具 | src/components/TrustDialog/utils.ts | 全文件 |
| 信任状态持久化 | src/utils/config.ts | 697-761 |
| 会话级信任 | src/bootstrap/state.ts | 1317-1323 |
| 安全 env vars | src/utils/managedEnv.ts | 93-199 |
| 延迟预取 | src/main.tsx | 388-431 |
| 帧时序 | src/ink/frame.ts | 36-124 |
| FPS 追踪 | src/utils/fpsTracker.ts | 全文件 |
| 闪烁检测 | src/ink/ink.tsx | 604-618 |
| 项目 onboarding | src/projectOnboardingState.ts | 全文件 |
| 启动分析器 | src/utils/startupProfiler.ts | 全文件 |
| init 初始化 | src/entrypoints/init.ts | 57-100 |
| REPL 启动器 | src/replLauncher.tsx | 全文件 |
| 对话启动器 | src/dialogLaunchers.tsx | 全文件 |
| bypassPermissions | src/utils/permissions/permissionSetup.ts | 778-786 |
| setup 环境 | src/setup.ts | 56-200 |