上一篇挪 Codex,改个 CODEX_HOME 就完事了。Claude Code 我以为是同一件事重做一遍,结果多花了一个多小时——环境变量设完,插件一个都没加载出来。

下面是完整的六步,按顺序做。第三步是上一篇没有的坑,跳过它插件必然失效。

先看清楚目录里有什么

.claude 里这几样必须保住:

  • projects/ — 所有对话历史,按项目路径分目录存 .jsonl
  • plugins/ — 插件本体、缓存和元数据
  • skills/ — 技能
  • settings.json — 全局设置,含插件启用列表
  • .credentials.json — 登录凭据

还有一个不在这个目录里:~/.claude.json。它躺在用户主目录根下,跟 .claude 平级,存项目索引和 MCP 服务器配置,我这边 150 KB。

我这份 .claude 有 8.6 GB、7 万多个文件,大头全在 plugins/cache。装了三十多个插件,每个是一次 git clone,缓存就这么堆起来的。

第一步:复制目录

robocopy "$env:USERPROFILE\.claude" "D:\Claude\.claude" /E /COPY:DAT /R:1 /W:1 /XJ

/XJ 跳过 junction 和符号链接。plugins/cache 底下会有指向别处的链接,不跳过 robocopy 有概率顺着爬出去,甚至绕回来死循环。

/COPY:DAT 而不是上一篇用的 /COPYALL/COPYALL 要连 ACL 和审计信息一起复制,非管理员身份跑会大面积报权限错误。数据、属性、时间戳三样够用。

跑了 3 分 59 秒,71166 个文件,退出码 9。

退出码 9 看着像失败,其实是位标志相加:1 表示有文件被复制,8 表示有文件没复制成功。真正失败的是两个:

ERROR 2 (0x00000002) Accessing Destination Directory
  ...\plugins\cache\temp_git_1784715795236_nimcz8\
ERROR 2 (0x00000002) Copying File
  ...\telemetry\1p_failed_events.<session-id>.json

一个是 git clone 留下的临时目录,一个是当前会话正在写的 telemetry 文件,都是运行时垃圾,边复制边消失。robocopy 退出码小于 8 就是干净成功,等于 8 或 9 要看一眼错误行是什么,别看见非零就重跑。

第二步:补上目录外面的那个文件

Copy-Item "$env:USERPROFILE\.claude.json" "D:\Claude\.claude\.claude.json" -Force

只 copy .claude 会漏掉它。设了 CLAUDE_CONFIG_DIR 之后,Claude Code 会去配置目录里找 .claude.json,找不到就当成全新安装,项目索引和 MCP 配置全丢。

第三步:改掉插件元数据里的绝对路径

这一步是整件事的关键。plugins/ 下有两个 JSON 文件,里面存的是绝对路径

installed_plugins.json,每个插件一条:

"superpowers@claude-plugins-official": [
  {
    "scope": "user",
    "installPath": "C:\\Users\\<user>\\.claude\\plugins\\cache\\claude-plugins-official\\superpowers\\6.1.1",
    "version": "6.1.1"
  }
]

known_marketplaces.json,每个市场一条:

"claude-plugins-official": {
  "source": { "source": "github", "repo": "anthropics/claude-plugins-official" },
  "installLocation": "C:\\Users\\<user>\\.claude\\plugins\\marketplaces\\claude-plugins-official"
}

robocopy 只搬文件,不改内容。文件到了 D 盘,路径还写着 C 盘。我这边表现是:/plugin 列表里空空如也,skills 只剩内置的那几个,settings.jsonenabledPlugins 三十多条一条没生效。因为源目录我留着没删,路径本身不算失效,所以也没有任何报错。

批量改写,先备份:

$base = "D:\Claude\.claude\plugins"
Copy-Item "$base\installed_plugins.json" "$base\installed_plugins.json.bak" -Force
Copy-Item "$base\known_marketplaces.json" "$base\known_marketplaces.json.bak" -Force

foreach ($f in @("$base\installed_plugins.json", "$base\known_marketplaces.json")) {
  $s = Get-Content $f -Raw
  $s = $s.Replace('C:\\Users\\<user>\\.claude', 'D:\\Claude\\.claude')
  [IO.File]::WriteAllText($f, $s, (New-Object Text.UTF8Encoding $false))
}

两个细节。JSON 里的反斜杠是转义过的,所以匹配串要写双反斜杠。写回用 WriteAllText 配无 BOM 的 UTF8Encoding,别用 Set-Content——它默认按系统 ANSI 编码写,中文路径会变乱码。

改完确认一遍每条路径都能落地:

$j = Get-Content "D:\Claude\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json
foreach ($p in $j.plugins.PSObject.Properties) {
  if (-not (Test-Path $p.Value[0].installPath)) { "MISSING: $($p.Name)" }
}

没有输出就是全通。

~/.claude.json 里可能也有一条 C:/Users/<user>/.claude,注意看它在哪个字段。我那条在 projects 下,是「这个目录被当项目打开过」的索引记录,不是插件路径,别顺手改掉。

第四步:设环境变量

[Environment]::SetEnvironmentVariable("CLAUDE_CONFIG_DIR", "D:\Claude\.claude", "User")

Machine 级不用设。环境变量解析是 User 覆盖 Machine,你自己的终端永远读 User 那份。机器级只在「这台机器上别的 Windows 账户也要用同一份配置」时才有意义,而那意味着共用同一份 projects/ 和同一份凭据,多数情况下不是你想要的。

真要设也别指望在 Claude Code 里让它代跑。写机器级变量要动 HKLM,需要管理员权限,而 Claude Code 开的是非交互会话,UAC 弹窗弹不出来。我试了两次 Start-Process -Verb RunAs,命令返回正常,回头查注册表是空的——静默失败,没有报错。要设就手动开个管理员 PowerShell。

第五步:重启,然后更新插件

改完开新终端,旧终端读的还是老值。

新终端里先确认 CLI 能正常枚举:

claude plugin list

能列出插件、状态显示 enabled,说明第三步的路径改对了。

接着把市场和插件都刷一遍。市场元数据里的 commit SHA 是迁移前的,不更新的话下次自动检查更新可能对不上:

claude plugin marketplace update

然后逐个更新插件。claude plugin update 一次只吃一个插件名,三十多个手敲不现实,从 installed_plugins.json 里读出来循环:

$j = Get-Content "D:\Claude\.claude\plugins\installed_plugins.json" -Raw | ConvertFrom-Json
$fail = @()
foreach ($n in $j.plugins.PSObject.Properties.Name) {
  claude plugin update $n | Out-Null
  if ($LASTEXITCODE -ne 0) { $fail += $n }
}
"failed: $($fail.Count)"; $fail

我这边 9 个市场、38 个插件,全部成功。跑完还得再重启一次,plugin update 的结果下次启动才应用。

第六步:验证

Test-Path "D:\Claude\.claude\.credentials.json"
(Get-ChildItem "D:\Claude\.claude\projects").Count

我这边 projects 下 32 个目录。进 Claude Code 跑一次 /resume,历史对话能翻出来;/plugin 里插件都在,状态 enabled;/skills 能看到插件带的技能。三样都对就算成了。

源目录先别删,确认几天再清理。C 盘也不差这几天。

顺手翻了一下别的环境变量

既然在折腾配置,把 Claude Code 认哪些环境变量都摸了一遍。官方文档有完整列表,但页面很长,抓取容易被截断。

更直接的办法是从本机装的二进制里扒。Claude Code 现在发的是打包好的原生二进制,JS 源码带字符串留在里面:

$p = "$env:USERPROFILE\.local\share\claude\versions\2.1.217"
$s = [Text.Encoding]::GetEncoding('latin1').GetString([IO.File]::ReadAllBytes($p))
[regex]::Matches($s, 'process\.env\.([A-Z][A-Z0-9_]{3,})') |
  ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique

v2.1.217 里扒出 522 个。绝大多数是内部开关和 CI 检测用的,别乱设。跟配置真正相关的几类:

路径类跟这次迁移同族。CLAUDE_CODE_PLUGIN_CACHE_DIR 可以单独把插件缓存挪走——我那 8.6 GB 的大头就在这儿,早知道只挪这一个就够了,还不用碰上面第三步。另外有 CLAUDE_TMPDIRCLAUDE_CODE_DEBUG_LOGS_DIR,后者名字里带 DIR 但实际要填文件路径。

超时类常用的是 API_TIMEOUT_MS(默认 10 分钟)和 BASH_DEFAULT_TIMEOUT_MS(默认 2 分钟)。网络不好或者走中转的时候会用上。

关开关的一堆 DISABLE_* 里,CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 最省事,一把关掉所有非必要请求,比一个个关干净。

除了 CLAUDE_CONFIG_DIR,上面这些更适合写进 settings.jsonenv 块,跟着项目或用户走,不用污染整机环境。CLAUDE_CONFIG_DIR 是唯一的例外,因为它得在读 settings.json 之前就生效,只能走环境变量。