Skip to main content
← 返回博客
claude-codeproductivityclidevtools

Claude Code 多配置文件,v2:干净的 XDG 架构

用了一个月旧的别名方案后,我把自己的 Claude Code 多配置文件 setup 重做成了 XDG 兼容的结构,放在 ~/.config/claude-profiles/ 下。真正的原因不是为了整洁——而是发现 ~/.claude.json 是 home 根目录下一个独立的文件,通过 --scope user 添加的 MCP 服务器一直悄悄地住在那里。

发布于 2026年6月13日7 分钟阅读

一个月前我发了一篇关于如何在同一台设备上并行运行两个 Claude Code 账号的文章——个人和工作,通过 CLAUDE_CONFIG_DIR 和一个 shell 别名实现。这套方案是能用的。一切都做了它该做的事。

但用了一个月真实使用之后,我撞上了几个那篇文章没覆盖的实质问题。最关键的一个——是 Claude Code 一个隐藏的特性,我是在一次意外中踩到它的:加了 Gmail MCP,五分钟后它就从我的配置文件里"消失"了。今天我把整套东西重做成了一个新架构——XDG 兼容,带 symlink,并通过 gum 实现交互式配置选择。这就是 v2——从真实经验中演化出来的,不是理论上的改进。

开始让人不舒服的地方

四件事。前三件关于整洁度、可见性和扩展性。第四件才是我没第一眼看出来的真正的架构陷阱。我按顺序一个个来,因为最后正是第四件逼我把整套东西重做的。

1. $HOME 下两个独立目录——一团乱

~/.claude~/.claude-promova——两个 dotfolder 紧挨着躺在 $HOME 根目录下。XDG Base Directory Specification 明确说配置应该住在 ~/.config/。dotfile 文件夹直接散落在 home 里是一种反模式,时间一长 $HOME 就变成杂物抽屉。是表面问题,但每次 ls -la ~ 我都觉得碍眼。

2. 没有当前活跃配置的视觉确认

我启动 claude,根本不知道当前是哪个配置文件——除非进了会话里再跑一遍 claude config list。要是忘了哪个终端启的是哪个,就得去查。小事,但当 personal + work 在两个标签页并行的时候,这种小事会累积。

3. 别名无法扩展

两个配置——claudeclaude-promova——还可以。加第三个(一个自由职业客户)——就得加第三个别名。第四个——加第四个。半年之后我根本不会记得自己到底建了哪些别名。

4. ~/.claude.json 里隐藏的陷阱

而这才是重做的真正原因。Claude Code 有两个不同的配置位置,文档对此并没有大声强调:~/.claude/——这是放 projects/sessions/hooks/skills/ 的目录。另外还有 ~/.claude.json——直接位于 $HOME 根目录的一个文件,里面住着 oauthAccountmcpServersprojects 历史、skillUsage,以及大约 40 个其他真正的 live state 字段。

claude mcp add --scope user 命令恰恰写入的就是 home 根目录下的那个 ~/.claude.json,不是 ~/.claude/.claude.json,也不是 profile 目录。这我之前不知道。直到某一天我踩到了它。

Discovery:为什么 Gmail MCP "消失"了

今天早上我在 Claude Code 里装 Gmail MCP。常规 setup:Google Cloud 项目,OAuth credentials,claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp。一切正常。重启会话——能用,我能看邮件、回信。一小时后我们开始把别名重构成一个带 gum 配置选择的函数,接着把整套东西迁到 XDG。我执行了 mv ~/.claude → ~/.config/claude-profiles/personal,重启 CC,在菜单里选了 personal。然后在新会话里打开 /mcp:

/mcp output (personal profile)
figma            (failed)
playwright-test
claude.ai Notion

没有 gmail。没有 vaultforge。只有三个服务器,其中一个还 failed。而我刚刚才添加了 Gmail。与此同时,另一个终端里(work 配置)的会话却好好地显示着 Gmail 和 Vaultforge。

我开始深挖,发现我机器上居然有三个不同的、名字都叫 .claude.json 的文件:

PathSizemcpServers
~/.claude.json (home root)113 KBfigma, gmail, vaultforge
~/.config/claude-profiles/personal/.claude.json29 KBfigma, playwright-test
~/.config/claude-profiles/promova/.claude.json40 KBfigma, playwright-test, vaultforge

找到了。这就是那个架构陷阱:

  1. claude mcp add --scope user 始终写入 home 根目录的 ~/.claude.json,与 CLAUDE_CONFIG_DIR 无关
  2. CLAUDE_CONFIG_DIR 设置时,Claude Code 读的是 $CLAUDE_CONFIG_DIR/.claude.json——也就是 profile 内部的那个文件
  3. "user-scope MCPs" 条目和 "profile MCPs" 条目分别住在不同的文件里,而且名字相同——非常容易搞混

在我这边,~/.claude.json(home 根目录,113 KB)是活的、最新的 state——里面有 gmail、vaultforge、OAuth 会话,什么都有。而 ~/.config/claude-profiles/personal/.claude.json(29 KB)结果是一个早就躺在旧 ~/.claude/ 里的老 snapshot——可能是某个老版本的 CC 写进去的,可能是某个插件。对两个文件分别跑 jq -r 'keys[]' 一看,home-root 版本里有 41 个 snapshot 里完全没有的独立 key。

而这 41 个 key 不是垃圾。这就是 Claude Code 真正的 state:

  • skillUsage——skills 使用统计
  • githubRepoPaths——用于 project 导航的仓库缓存
  • cachedGrowthBookFeatures + cachedStatsigGates——feature flags(没了它们,CC 每次启动都得重新去拉)
  • hasShownOpus45NoticehasShownOpus46NoticehasShownS1MWelcomeV2——UI 标记(没了它们,下次启动那些 modal 又会再弹一遍)
  • lastPlanModeUsefeedbackSurveyStateinstallMethod——onboarding 和 UX state

如果你直接 mv ~/.claude ~/.config/claude-profiles/personal 不做 merge——这些就全没了。welcome modal 重新出现,githubRepoPaths 搜索重新跑,所有 survey 提示重新弹一遍。我差点就这么干了。

新架构

所有东西都住在 ~/.config/ 下一个统一的父目录里,正如 XDG 所期望的。每个配置都是自包含的——它有自己的完整 state,包括自己的 .claude.json~/.claude 作为指向 personal 配置的 symlink 保留,用于向后兼容。

~/.config/claude-profiles/
├── personal/
│   ├── .claude.json          ← full live state + MCP list (113 KB)
│   ├── projects/, sessions/, hooks/, skills/, ...
│   └── CLAUDE.md
└── promova/
    ├── .claude.json          ← work state + work MCPs (41 KB)
    └── projects/, sessions/, agents/, skills/, ...

~/.claude  →  symlink to ~/.config/claude-profiles/personal/

$HOME 根目录下不再有 .claude.json。每个配置是一个单独的、隔离的目录,里面什么都有:projects/sessions 目录,以及那个装着 MCP 服务器和 OAuth tokens 的文件。每个配置一份 source of truth。

所有硬编码了 ~/.claude/ 路径的东西——老脚本、插件、Claude Code 的 IDE 扩展、像 claude-powerline.json 这样的 statusline 配置——继续正常工作,不需要任何改动。symlink 会被解析到 personal 配置。如果你不小心运行 command claude(绕过 wrapper 函数)——也会通过默认路径查找落到 personal。personal 成了它过去那种 "quiet default",只不过现在它物理上住在 XDG 位置。

启动时的交互选择——函数 + gum

不再用别名——在 ~/.zshrc 里放一个 claude() 函数,通过 gum(Charm 出的 TUI 助手)显示一个带箭头键的菜单。函数在 shell 层拦截对 claude 的调用,让你选配置,然后用对应的 CLAUDE_CONFIG_DIR 运行 command claudecommand 很关键——它绕开 wrapper 函数,直接调用真正的二进制。

~/.zshrc
# brew install gum

# Claude Code: profile picker on launch
claude() {
  local profile
  profile=$(gum choose \
    --header "Claude profile:" \
    --cursor "▸ " \
    --selected.foreground 212 \
    --cursor.foreground 212 \
    --header.foreground 244 \
    "personal" "promova") || return
  case "$profile" in
    personal) CLAUDE_CONFIG_DIR="$HOME/.config/claude-profiles/personal" command claude "$@" ;;
    promova)  CLAUDE_CONFIG_DIR="$HOME/.config/claude-profiles/promova"  command claude "$@" ;;
  esac
}

启动时看起来是这样:

Claude profile:
▸ personal
  promova

↑↓ 导航,Enter 选中,Esc 取消(Claude 就干脆不启动)。配置始终可见——绝对忘不掉。

迁移脚本

给那些读到这里、想从旧方案迁过来的人。最关键的一步是第二步:它把 home 根目录的 ~/.claude.json 和 personal 配置里已有的内容做 merge,合并 mcpServers 列表。没有这一步,配置会同时失去 MCP 和所有 live state。

Terminal
# 1. Move existing dotfolders into a new XDG-style parent.
#    APFS mv is an inode rename — safe even if claude --resume is open in
#    another terminal, file descriptors stay alive on the same inode.
mkdir -p ~/.config/claude-profiles
mv ~/.claude          ~/.config/claude-profiles/personal
mv ~/.claude-promova  ~/.config/claude-profiles/promova   # rename as needed

# 2. CRITICAL: merge ~/.claude.json (the live state — likely 100+ KB) into
#    the personal profile, unioning mcpServers so no MCP is dropped.
jq -s '.[0] * {mcpServers: (.[0].mcpServers + .[1].mcpServers)}' \
  ~/.claude.json \
  ~/.config/claude-profiles/personal/.claude.json \
  > /tmp/personal-merged.json
mv /tmp/personal-merged.json ~/.config/claude-profiles/personal/.claude.json

# 3. Fix permissions — jq+mv inherits umask (likely 644), but this file
#    holds OAuth tokens. Tighten to 600 immediately.
chmod 600 ~/.config/claude-profiles/personal/.claude.json

# 4. Back up the orphaned home-root file (delete once verified)
mv ~/.claude.json ~/.claude.json.migrated.bak

# 5. Symlink for backward compatibility (statusline configs, IDE plugins,
#    anything that hardcodes ~/.claude/ keeps working unchanged)
ln -s ~/.config/claude-profiles/personal ~/.claude

关于权限的第三步同样重要。jq | mv 会以 umask 644(world-readable)创建文件。里面有 OAuth tokens。merge 之后必须立刻 chmod 600

迁移之后——关闭并重新打开所有活跃的 Claude 会话,重新加载 shell(source ~/.zshrc 或开新终端),运行 claude,选配置,通过 claude mcp list 确认所有 MCP 都在。如果一切 OK——删掉 ~/.claude.json.migrated.bak。如果有问题——回滚很简单:mv ~/.claude.json.migrated.bak ~/.claude.json,然后把 symlink 拿掉。

你能得到什么

  • 用一个父目录替代 $HOME 下的两个 dotfolder——符合 XDG
  • symlink 保留了和所有硬编码 ~/.claude/ 的东西的兼容性
  • 每个配置都是自包含的——它的完整 state 和它的 MCP 都在它自己的 .claude.json
  • 每个配置一份 source of truth——不再有 home 根目录里那种和配置文件悄悄分裂的孤儿 config
  • 敏感数据(oauthAccount、tokens)保证拥有 600 权限
  • 每次启动都有当前活跃配置的视觉确认——不可能忘掉现在在哪个配置上
  • 添加第三个配置 = 在函数的 case 里加一行,而不是克隆一个新别名再去记它的名字

Where it falls short

我想说实话。这并不是免费的升级——一些权衡是打包附赠的,值得提前知道。

  • gum 是一个额外依赖(brew install gum,~13 MB)。如果你坚决不想装——可以退回到 zsh 的 select 或简单的 read。能用,但没那么好看,也没有箭头键导航。
  • 每次启动按一下 Enter。对那些一天要敲几十次 claude 的人——可能挺烦。下面有替代方案(direnv)。
  • ~/.claude → personal 这个 symlink 会让 personal 成为默认值。如果你需要把 work 配置作为默认,就得重新指向 symlink(ln -sf)。不难,但这不是 "忘了它也不会出事" 那种程度。
  • symlink 理论上可能被破坏——如果某个工具用 temp+rename 的方式(write-file-atomic)原子地重写 ~/.claude.json。实际上 Claude Code 自己不会这么做,但如果你装了第三方插件——请检查一下。
  • 如果你不同的配置上挂着不同的 Anthropic 账号、不同的套餐——切换之后可能会有一个小于一秒的延迟,直到 Claude Code 把 OAuth state 同步好。我用起来感觉不到,但它不是零。

我考虑过的替代方案

direnv——根据每个项目根目录里的 .envrc 自动设置 CLAUDE_CONFIG_DIR。零交互、零点击。缺点:你得在每一个 work-root 都放一个 .envrc,而且如果你在一个无法识别的目录里运行 claude——你会拿到默认配置(可能不是你想要的那个)。对那些只在有限的几个 work-root 里活动、并且坚决不想点击的人来说——direnv 确实更好。

基于 symlink 的切换(通过重新指向 ~/.claude symlink 实现唯一活跃配置)我也考虑过,然后立刻被否决了。你没法同时在两个终端里跑不同的配置——全局的 "当前" 只有一个。对我来说这是 deal-breaker。

结论

v2 不仅仅是在 v1 之上的 UX 改良。它是对一个事实的承认:Claude Code 有一个隐藏的架构特性(~/.claude.json 是 home 根目录下一个独立的文件,被 --scope user 命令写入,且与 CLAUDE_CONFIG_DIR 无关),如果你想要配置之间真正的隔离,就必须把它考虑进去。第一种方案(~/.claude + ~/.claude-promova + 别名)能搞定 80% 的事,但剩下的 20% 就表现为配置之间状态的悄无声息的漂移。现在这一点被照顾到了。如果你才刚开始——直接从 v2 起步。如果你已经在 v1 上——上面的迁移脚本拿去,迁移五分钟搞定,什么都不会坏(正是 jq merge 这一步,救你免于丢失 state)。