Claude Code 源码分析第 34 课 · 第 05
第 34 课

迁移机制

Claude Code 如何跨版本升级用户配置和模型设置 - 不会破坏任何内容或让用户感到惊讶。

01 Overview

每次启动 Claude Code 时,它可能会默默地修复过时的设置、重新映射已弃用的模型别名、在文件之间移动字段或重新显示 UI 对话框 - 所有这些都在您看到第一个提示之前进行。 这是 迁移系统:一组小型但精确设计的一次性函数,在内部运行 runMigrations() in main.tsx.

覆盖源文件
src/migrations/*.ts  ·  src/main.tsx (第 323 课-352 行)·  src/utils/config.ts  ·  src/utils/releaseNotes.ts

Claude Code 中的迁移是 不是数据库架构迁移。 没有迁移表,没有回滚,也没有运行器框架。 相反,每个迁移函数在设计上都是幂等的——它会检测自己的前置/后置条件,如果工作已经完成,则立即退出。 整个集合在每次启动时运行,并受到单个版本号的保护,一旦应用了所有迁移,该版本号就会短路整个块。

类型1

设置促销活动

将字段从 ~/.claude.json (全局配置)进入 settings.json

2型

模型别名升级

将过时或已删除的模型字符串重新映射到当前别名 userSettings

3型

配置键重命名

重命名泄漏到公共配置中的实现细节密钥

4型

一键重置

当用户体验发生变化且用户需要第二次选择机会时,清除标志以重新显示对话框

5型

异步文件迁移

将配置数据移至单独的文件(更改日志缓存)而不阻塞 UI

02 跑步者: runMigrations()

所有同步迁移都包含在定义于的单个函数中 main.tsx。 在指挥官期间被称为 preAction hook — 加载配置后,REPL 启动之前。

// main.tsx — line 323
// @[MODEL LAUNCH]: Consider any migrations you may need for model strings.
// See migrateSonnet1mToSonnet45.ts for an example.

// Bump this when adding a new sync migration so existing users re-run the set.
const CURRENT_MIGRATION_VERSION = 11;

function runMigrations(): void {
  if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
    migrateAutoUpdatesToSettings();
    migrateBypassPermissionsAcceptedToSettings();
    migrateEnableAllProjectMcpServersToSettings();
    resetProToOpusDefault();
    migrateSonnet1mToSonnet45();
    migrateLegacyOpusToCurrent();
    migrateSonnet45ToSonnet46();
    migrateOpusToOpus1m();
    migrateReplBridgeEnabledToRemoteControlAtStartup();
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      resetAutoModeOptInForDefaultOffer();
    }
    if ("external" === 'ant') {   // internal Anthropic builds only
      migrateFennecToOpus();
    }
    // Stamp the version so we skip next time
    saveGlobalConfig(prev => prev.migrationVersion === CURRENT_MIGRATION_VERSION
      ? prev
      : { ...prev, migrationVersion: CURRENT_MIGRATION_VERSION });
  }

  // Async migration — fire-and-forget, non-blocking
  migrateChangelogFromConfig().catch(() => {});
}

这里有三个突出的设计决策:

  1. 版本门: migrationVersion 存储在 ~/.claude.json。 一旦等于 CURRENT_MIGRATION_VERSION,整个同步块被跳过——避免 11 个冗余 saveGlobalConfig 每次启动时都会进行锁定+重新读取循环。
  2. 版本碰撞规则: 该评论明确指出——每当添加新的同步迁移时都会改变常量,以便现有用户重新运行全套。
  3. 异步分离: migrateChangelogFromConfig 是异步的并且涉及文件 I/O,因此它在同步块之后运行即发即弃。
创业安置
runMigrations() 在指挥官中被称为 preAction 里面有钩子 main.tsx,直接在 init() 就在之前 loadRemoteManagedSettings()。 一个 profileCheckpoint('preAction_after_migrations') 调用立即跟随它以进行延迟跟踪。
03 迁移目录

目前有 11 个同步迁移函数和 1 个异步迁移函数。 这是每个文件的用途、幂等性机制以及它涉及的配置存储。

File Category 它的作用 幂等性守卫
migrateAutoUpdatesToSettings.ts Settings 移动用户禁用的 autoUpdates 标志来自 GlobalConfig into userSettings.env.DISABLE_AUTOUPDATER = "1"。 还设置 process.env 立即生效,无需重新启动即可生效。 跳过如果 globalConfig.autoUpdates !== false 或者如果该标志是由本机保护设置的。 成功后从 GlobalConfig 中删除该字段。
migrateBypassPermissionsAcceptedToSettings.ts Settings Moves bypassPermissionsModeAccepted 从 GlobalConfig 进入 userSettings.skipDangerousModePermissionPrompt。 旧名称将实现细节泄露到面向用户的配置文件中。 跳过如果 bypassPermissionsModeAccepted 不在 GlobalConfig 中。 支票 hasSkipDangerousModePermissionPrompt() 在写入之前避免覆盖现有值。
migrateEnableAllProjectMcpServersToSettings.ts Settings 移动三个 MCP 服务器批准字段(enableAllProjectMcpServers, enabledMcpjsonServers, disabledMcpjsonServers)从项目配置到 localSettings。 将数组字段与重复数据删除合并以避免丢失现有数据。 如果项目配置中不存在这三个字段,则跳过。 为了 enableAllProjectMcpServers,在写入之前检查目标设置是否已设置。
resetProToOpusDefault.ts Reset 记录时间戳(opusProMigrationTimestamp)适用于没有设置自定义模型的第一方 Pro 订阅者,因此 UI 可以显示一次性通知,表明 Opus 4.5 现在是他们的默认设置。 完成标志: globalConfig.opusProMigrationComplete。 立即为非专业版或非firstParty 用户标记为完成。
migrateSonnet1mToSonnet45.ts Model 固定拥有以下权限的用户 sonnet[1m] 保存在 userSettings 到明确的 sonnet-4-5-20250929[1m] 细绳。 之所以需要,是因为 Sonnet 4.6 1M 提供给与 Sonnet 4.5 1M 不同的用户组。 如果已设置,还会更新内存中主循环模型覆盖。 完成标志: globalConfig.sonnet1m45MigrationComplete.
migrateLegacyOpusToCurrent.ts Model 重写显式 Opus 4.0/4.1 模型字符串(claude-opus-4-20250514, claude-opus-4-1-20250805等)到 opus 别名在 userSettings。 还记录 legacyOpusMigrationTimestamp 这样 UI 就可以显示一次性通知。 仅针对启用了旧版重映射的第一方用户运行。 读取和写入相同的源(userSettings),使其成为自幂等 - 迁移后模型字符串不再匹配,因此它会提前退出。
migrateSonnet45ToSonnet46.ts Model 将固定到 Sonnet 4.5 显式字符串的 Pro/Max/Team Premium 用户升级回 sonnet (or sonnet[1m]) 别名,现在解析为 4.6。 跳过全新用户(numStartups <= 1)以避免向从未使用过 4.5 的人显示通知。 自幂等:仅在以下情况下写入 userSettings.model 匹配 Sonnet 4.5 字符串。 Gate:仅限第一方 + Pro/Max/TeamPremium。
migrateOpusToOpus1m.ts Model 对于第一方的 Max/Team Premium 订阅者,升级 userSettings.model = 'opus' to 'opus[1m]' 当启用 Opus 1M 合并时。 如果 opus[1m] 解析为与默认模型相同的模型,它会清除该字段(没有不必要的引脚)。 自幂等:仅当模型完全正确时才写入 'opus'。 门: isOpus1mMergeEnabled().
migrateReplBridgeEnabledToRemoteControlAtStartup.ts Config 重命名内部配置键 replBridgeEnabled to remoteControlAtStartup。 旧名称将实现细节泄露到公共配置文件中。 使用无类型强制转换来访问不再属于 TypeScript 类型的键。 跳过如果 replBridgeEnabled 不存在,或者如果 remoteControlAtStartup 已设置。
resetAutoModeOptInForDefaultOffer.ts Reset Clears skipAutoPermissionPrompt 适用于接受旧的 2 选项自动模式对话框但从未将自动设置为默认模式的用户。 这会重新显示对话框,以便他们看到新的“使其成为我的默认值”选项。 仅当以下情况时才会触发 TRANSCRIPT_CLASSIFIER 功能标志处于活动状态。 完成标志: globalConfig.hasResetAutoModeOptInForDefaultOffer。 也被守护着 getAutoModeEnabledState() === 'enabled'.
migrateFennecToOpus.ts Model 仅限内部 Anthropic(USER_TYPE === 'ant')。 迁移删除了“fennec”模型别名(fennec-latest, fennec-latest[1m], fennec-fast-latest, opus-4-5-fast)到其 Opus 等效项,包括快速模式。 自幂等:仅当模型以 fennec 前缀开头时才起作用。 只写 userSettings.
releaseNotes.ts: migrateChangelogFromConfig() 配置(异步) 将缓存的变更日志字符串从 globalConfig.cachedChangelog 到磁盘上的一个单独的文件中。 异步运行,因此不会阻塞 UI。 用途 wx write 标志,因此它仅在文件不存在时创建该文件。 跳过如果 globalConfig.cachedChangelog 未设置。 文件写入用途 flag: 'wx' (仅创建)以避免破坏现有数据。
04 两种幂等性模式

每次迁移都必须能够安全地多次调用。 代码库中出现了两种模式:

模式 A — GlobalConfig 中的完成标志

当迁移需要运行一次但“已完成”状态从数据中不是不言而喻时使用(例如 resetProToOpusDefault, migrateSonnet1mToSonnet45)。 布尔值或时间戳被写入 ~/.claude.json 并在函数顶部进行检查。

// migrateSonnet1mToSonnet45.ts — completion flag pattern
export function migrateSonnet1mToSonnet45(): void {
  const config = getGlobalConfig()
  if (config.sonnet1m45MigrationComplete) {
    return  // already done — exit immediately
  }

  const model = getSettingsForSource('userSettings')?.model
  if (model === 'sonnet[1m]') {
    updateSettingsForSource('userSettings', {
      model: 'sonnet-4-5-20250929[1m]',
    })
  }

  saveGlobalConfig(current => ({
    ...current,
    sonnet1m45MigrationComplete: true,
  }))
}

模式 B——自幂等(数据不言自明)

当迁移条件是对当前值的直接检查时使用 - 如果数据已经迁移,则检查简单地返回 false 并且不写入任何内容(例如所有模型别名迁移)。 读取和写入相同的设置源(userSettings) 是这种模式的关键 - 中的注释 migrateLegacyOpusToCurrent.ts explains: “读取和写入相同的源可以在没有完成标志的情况下保持幂等性,并且避免为仅将其固定在一个项目中的用户默默地将‘opus’提升为全局默认值。”

// migrateLegacyOpusToCurrent.ts — self-idempotent pattern
export function migrateLegacyOpusToCurrent(): void {
  if (getAPIProvider() !== 'firstParty') return
  if (!isLegacyModelRemapEnabled()) return

  const model = getSettingsForSource('userSettings')?.model
  if (
    model !== 'claude-opus-4-20250514' &&
    model !== 'claude-opus-4-1-20250805' &&
    model !== 'claude-opus-4-0' &&
    model !== 'claude-opus-4-1'
  ) {
    return  // data already clean — nothing to do
  }

  updateSettingsForSource('userSettings', { model: 'opus' })
  saveGlobalConfig(current => ({
    ...current,
    legacyOpusMigrationTimestamp: Date.now(),
  }))
  logEvent('tengu_legacy_opus_migration', { from_model: model })
}
05 设置层规则

Claude Code 具有多个按优先级顺序合并的设置源: userSettingsprojectSettingslocalSettingspolicySettings。 迁移的范围被故意限制为仅接触 userSettings (并且偶尔 localSettings)。 几乎每个模型迁移文件中的注释都重复相同的理由:

关键约束
“仅触及 userSettings。项目/本地/策略设置中的旧字符串单独保留(我们不能/不应该重写这些),并且仍然在运行时重新映射 parseUserSpecifiedModel。 读取和写入相同的源可以在没有完成标志的情况下保持幂等性,并避免为仅将其固定在一个项目中的用户默默地将“opus”提升为全局默认值。”

此约束可以防止一类微妙的错误:如果迁移从 merged 设置,它可能会看到项目范围的设置并“有帮助地”将该值写入全局 userSettings,突然使每个项目的首选项成为所有地方的新默认值。

flowchart LR A["userSettings\n(~/.claude/settings.json)"] -->|"migration reads & writes HERE only"| M["Migration\nfunction"] B["projectSettings\n(.claude/settings.json)"] -->|"runtime merge only\nnever mutated"| R["Merged\nSettings"] C["localSettings\n(.claude/settings.local.json)"] -->|"some migrations\nwrite here (MCP)"| M D["policySettings\n(managed)"] -->|"runtime merge only\nnever mutated"| R M --> R A --> R B --> R C --> R D --> R
06 内存中配置迁移

除了启动迁移功能之外, config.ts 包含一个较低级别的 migrateConfigFields() 每次配置文件运行时运行 从磁盘读取。 这处理最旧的架构更改——在当前版本化迁移系统存在之前。

// config.ts — runs on every config read
function migrateConfigFields(config: GlobalConfig): GlobalConfig {
  if (config.installMethod !== undefined) {
    return config  // already migrated
  }

  // autoUpdaterStatus is removed from the type but may exist in old configs
  const legacy = config as GlobalConfig & {
    autoUpdaterStatus?: 'migrated' | 'installed' | 'disabled' | 'enabled' | ...
  }

  switch (legacy.autoUpdaterStatus) {
    case 'migrated':  installMethod = 'local';   break
    case 'installed': installMethod = 'native';  break
    case 'disabled':  autoUpdates  = false;      break
    ...
  }

  return { ...config, installMethod, autoUpdates }
}

还有一个 removeProjectHistory() 剥离旧内联的函数 history 每次读取时项目配置中的字段 - 该字段已迁移到单独的 history.jsonl 文件,但旧的配置仍然包含该字段。

Pattern
读取时迁移(内联 migrateConfigFields) 用于非常旧的架构更改,其中旧字段名称不再存在于 TypeScript 类型中 - 需要非类型化强制转换才能访问它。 新的迁移进入 migrations/ 目录并通过以下方式运行 runMigrations().
07 分析仪器

迁移是可观察到的。 大多数函数调用 logEvent() 记录 Anthropic 的遥测管道发生的情况。 这可以让团队知道何时仍在将迁移应用于野外用户以及何时可以安全地删除迁移代码。

活动名称Migration
tengu_migrate_autoupdates_to_settingsmigrateAutoUpdatesToSettings
tengu_migrate_autoupdates_errormigrateAutoUpdatesToSettings(错误路径)
tengu_migrate_bypass_permissions_acceptedmigrateBypassPermissionsAcceptedToSettings
tengu_migrate_mcp_approval_fields_successmigrateEnableAllProjectMcpServersToSettings
tengu_migrate_mcp_approval_fields_errormigrateEnableAllProjectMcpServersToSettings(错误路径)
tengu_reset_pro_to_opus_defaultresetProToOpusDefault
tengu_legacy_opus_migrationmigrateLegacyOpusToCurrent
tengu_sonnet45_to_46_migrationmigrateSonnet45ToSonnet46
tengu_opus_to_opus1m_migrationmigrateOpusToOpus1m
tengu_migrate_reset_auto_opt_in_for_default_offerresetAutoModeOptInForDefaultOffer

尤其, migrateSonnet1mToSonnet45 and migrateReplBridgeEnabledToRemoteControlAtStartup 不发出分析事件——它们被认为是风险较低的内务处理。

08 添加新的迁移

源代码甚至包含一条注释,为未来的工程师指明了正确的方向:

main.tsx 中的开发者注释
// @[MODEL LAUNCH]: Consider any migrations you may need for model strings. See migrateSonnet1mToSonnet45.ts for an example.

现有代码库的配方:

  1. Create src/migrations/myNewMigration.ts 具有单个导出函数。
  2. 选择幂等策略:GlobalConfig 中的完成标志,或自幂等数据检查。
  3. 只能读取和写入 userSettings (or localSettings 对于项目范围的数据)。 切勿读取合并的设置。
  4. Call logEvent('tengu_my_migration_name', {...}) 与相关元数据。
  5. 将主体包裹在 try/catch 中并调用 logError 需要注意的是——迁移绝不能抛出或破坏启动。
  6. 将函数导入到 main.tsx 并将其添加到 if (migrationVersion !== CURRENT_MIGRATION_VERSION) block.
  7. Bump CURRENT_MIGRATION_VERSION 因此现有用户重新运行更新后的设置。
// Template: src/migrations/myNewMigration.ts
import { logEvent } from '../services/analytics/index.js'
import { logError } from '../utils/log.js'
import {
  getSettingsForSource,
  updateSettingsForSource,
} from '../utils/settings/settings.js'

export function myNewMigration(): void {
  // self-idempotent guard: check if work is already done
  const model = getSettingsForSource('userSettings')?.model
  if (model !== 'old-alias') return

  try {
    updateSettingsForSource('userSettings', { model: 'new-alias' })
    logEvent('tengu_my_new_migration', {})
  } catch (error) {
    logError(new Error(`Failed to run myNewMigration: ${error}`))
  }
}

要点

  • runMigrations() in main.tsx 是单一入口点——由以下函数保护的函数调用的平面列表 CURRENT_MIGRATION_VERSION = 11.
  • 版本门在 globalConfig.migrationVersion 应用所有迁移后,可防止每次启动时重新运行 11 配置保存。
  • 两种幂等策略: 完成标志 在 GlobalConfig 中用于不言而喻的一次性操作,以及 自幂等数据检查 对于数据不言自明的情况。
  • 所有模型迁移仅触及 userSettings (从不合并设置)以避免意外全球化项目范围的模型首选项。
  • 迁移绝不能抛出错误——错误会被捕获、记录并默默地吞掉,以避免破坏启动。
  • 大多数迁移调用 logEvent() 因此 Anthropic 可以跟踪旧数据形状何时从已安装的基础上完全消失。
  • 添加迁移需要碰撞 CURRENT_MIGRATION_VERSION 因此,已经通过该关卡的现有用户将重新运行更新后的集合。
  • 异步迁移(文件 I/O 类似 migrateChangelogFromConfig)在同步块之后即发即弃。

知识检查

Q1. 当发生什么 getGlobalConfig().migrationVersion === CURRENT_MIGRATION_VERSION?
Q2. 为什么模型迁移函数读取 userSettings 直接而不是来自合并的设置?
Q3. 哪个迁移还会更新内存运行时状态(不仅仅是设置文件)?
Q4. 将新迁移添加到同步块时必须做什么?
Q5. migrateAutoUpdatesToSettings calls process.env.DISABLE_AUTOUPDATER = '1' 写信给之后 userSettings。 为什么?