Claude Code 多配置文件,v2:干净的 XDG 架构
用了一个月旧的别名方案后,我把自己的 Claude Code 多配置文件 setup 重做成了 XDG 兼容的结构,放在 ~/.config/claude-profiles/ 下。真正的原因不是为了整洁——而是发现 ~/.claude.json 是 home 根目录下一个独立的文件,通过 --scope user 添加的 MCP 服务器一直悄悄地住在那里。
一个月前我发了一篇关于如何在同一台设备上并行运行两个 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. 别名无法扩展
两个配置——claude 和 claude-promova——还可以。加第三个(一个自由职业客户)——就得加第三个别名。第四个——加第四个。半年之后我根本不会记得自己到底建了哪些别名。
4. ~/.claude.json 里隐藏的陷阱
而这才是重做的真正原因。Claude Code 有两个不同的配置位置,文档对此并没有大声强调:~/.claude/——这是放 projects/、sessions/、hooks/、skills/ 的目录。另外还有 ~/.claude.json——直接位于 $HOME 根目录的一个文件,里面住着 oauthAccount、mcpServers、projects 历史、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:
figma (failed)
playwright-test
claude.ai Notion没有 gmail。没有 vaultforge。只有三个服务器,其中一个还 failed。而我刚刚才添加了 Gmail。与此同时,另一个终端里(work 配置)的会话却好好地显示着 Gmail 和 Vaultforge。
我开始深挖,发现我机器上居然有三个不同的、名字都叫 .claude.json 的文件:
| Path | Size | mcpServers |
|---|---|---|
~/.claude.json (home root) | 113 KB | figma, gmail, vaultforge |
~/.config/claude-profiles/personal/.claude.json | 29 KB | figma, playwright-test |
~/.config/claude-profiles/promova/.claude.json | 40 KB | figma, playwright-test, vaultforge |
找到了。这就是那个架构陷阱:
claude mcp add --scope user始终写入 home 根目录的~/.claude.json,与CLAUDE_CONFIG_DIR无关- 当
CLAUDE_CONFIG_DIR设置时,Claude Code 读的是$CLAUDE_CONFIG_DIR/.claude.json——也就是 profile 内部的那个文件 - "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 每次启动都得重新去拉)hasShownOpus45Notice、hasShownOpus46Notice、hasShownS1MWelcomeV2——UI 标记(没了它们,下次启动那些 modal 又会再弹一遍)lastPlanModeUse、feedbackSurveyState、installMethod——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。
为什么 symlink 指向 personal
所有硬编码了 ~/.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 claude。command 很关键——它绕开 wrapper 函数,直接调用真正的二进制。
# 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。
# 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)。