Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Claude Code 交互式启动流程与信任边界深度分析

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 行)

按顺序执行以下操作:

  1. setSessionTrustAccepted(true) — 会话级信任标志
  2. resetGrowthBook() + void initializeGrowthBook() — 重初始化 GrowthBook
  3. void getSystemContext() — 预取系统上下文
  4. handleMcpjsonServerApprovals(root) — MCP 服务器审批
  5. 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 配置
HooksgetHooksSources().claude/settings.json 和 .claude/settings.local.json 中的 hooks
Bash 权限getBashPermissionSources()允许 Bash 执行的权限规则
API Key HelpergetApiKeyHelperSources()自定义 API Key 获取脚本
AWS 命令getAwsCommandsSources()AWS 认证刷新/导出命令
GCP 命令getGcpCommandsSources()GCP 认证刷新命令
OTel HeadersgetOtelHeadersHelperSources()遥测 header 获取脚本
危险环境变量getDangerousEnvVarsSources()不在 SAFE_ENV_VARS 白名单中的环境变量
命令中的 Bashcommands 检查自定义命令/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):

  1. 会话级(内存):setSessionTrustAccepted() / getSessionTrustAccepted() — 用于 Home 目录等不持久化的场景
  2. 项目级(磁盘):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
Grovesrc/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:

阶段含义
rendererDOM → yoga layout → screen buffer
diffscreen diff → Patch[]
optimizepatch merge/dedupe
write序列化 patches → ANSI → stdout
yogacalculateLayout() 时间
commitReact reconcile 时间
yogaVisitedlayoutNode() 调用次数
yogaMeasuredtext 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() 判断何时需要清屏:

  1. 终端尺寸变化 → 'resize'
  2. 当前帧高度 ≥ 视口高度 → 'offscreen'
  3. 前一帧高度 ≥ 视口高度 → '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 提供两种模式:

  1. 采样日志:100% ant 用户 + 0.5% 外部用户,阶段数据上报 Statsig
  2. 详细分析: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.tsx104-298
对话渲染原语src/interactiveHelpers.tsx39-103
Trust Dialog UIsrc/components/TrustDialog/TrustDialog.tsx23-264
Trust 检查工具src/components/TrustDialog/utils.ts全文件
信任状态持久化src/utils/config.ts697-761
会话级信任src/bootstrap/state.ts1317-1323
安全 env varssrc/utils/managedEnv.ts93-199
延迟预取src/main.tsx388-431
帧时序src/ink/frame.ts36-124
FPS 追踪src/utils/fpsTracker.ts全文件
闪烁检测src/ink/ink.tsx604-618
项目 onboardingsrc/projectOnboardingState.ts全文件
启动分析器src/utils/startupProfiler.ts全文件
init 初始化src/entrypoints/init.ts57-100
REPL 启动器src/replLauncher.tsx全文件
对话启动器src/dialogLaunchers.tsx全文件
bypassPermissionssrc/utils/permissions/permissionSetup.ts778-786
setup 环境src/setup.ts56-200