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日9 分钟阅读

一个月前我发了一篇关于如何在同一台设备上并行运行两个 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 就干脆不启动)。配置始终可见——绝对忘不掉。

从零开始(没有 v1 需要迁移)

如果你已经在跑那套老的双文件夹 + 别名的土办法,直接跳到下面的迁移脚本。这一节讲的是从零做起的干净搭建——一台全新的机器,或者只是日常使用攒下来的默认 ~/.claude。先决定形态:一个账号配多个配置文件(用来隔离项目和它们各自的 MCP 集合——最常见的情况),还是每个配置文件用真正不同的账号(personal 对 work)。

1. 把目录摆出来

Terminal — one-time setup
# prerequisites
brew install gum jq

# 1. XDG parent for all profiles
mkdir -p ~/.config/claude-profiles

# 2. fold an existing default install in as your first profile (no-ops on a fresh machine)
if [ -d ~/.claude ] && [ ! -L ~/.claude ]; then
  mv ~/.claude ~/.config/claude-profiles/personal
  [ -f ~/.claude.json ] && mv ~/.claude.json ~/.config/claude-profiles/personal/.claude.json
fi

# 3. one directory = one profile
mkdir -p ~/.config/claude-profiles/personal ~/.config/claude-profiles/work

# 4. these files will hold OAuth tokens — lock them down
chmod 600 ~/.config/claude-profiles/*/.claude.json 2>/dev/null

在同一个 XDG 父目录下,一个配置文件一个目录。如果你已经用过 Claude Code,这段代码会把你现有的 ~/.claude 折进来作为 personal,并把 home 根目录里的 ~/.claude.json live state 一起带过来;在一台真正全新的机器上,这几行就只是空操作。其余全是空目录,首次启动时会自己填满——而那个 chmod 600 很重要,因为这些文件最终会装着 OAuth tokens。

2. 给每个配置文件做认证

Terminal
# launch each profile once and authenticate it
CLAUDE_CONFIG_DIR=~/.config/claude-profiles/work claude
#   → run /login in the session, sign in, done.
# Same account for every profile? One /login is enough — macOS keeps it in the
# shared Keychain, so the other profiles are already authenticated.

用各自的 CLAUDE_CONFIG_DIR 把每个配置文件各启动一次,然后跑 /login。关键细节:如果每个配置文件用的都是同一个账号,你只需登录一次——macOS 把这份凭据保存在 Keychain 里一条共享条目中,所以其他每个配置文件都自动完成了认证。

每个配置文件用不同账号是更棘手的那条路:那条共享的 Keychain 条目只装得下一个登录,所以把第二个账号登进另一个配置文件,可能会覆盖掉第一个。如果这是你的情况,请按照在一台机器上跑两个 Claude 账号里的专门步骤来做,并在依赖它之前,先在你这个版本的 Claude Code 上验证凭据隔离是否生效。

Terminal + ~/.zshrc
# personal becomes the default target of ~/.claude (back-compat)
ln -s ~/.config/claude-profiles/personal ~/.claude

# add the claude() gum picker from the architecture section to ~/.zshrc, then:
source ~/.zshrc
claude        # ← now shows the profile menu on every launch

~/.claude 指向 personal 配置文件,这样任何硬编码了那条路径的东西都能继续正常工作,然后把上面架构那一节里的 claude() gum 选择器放进 ~/.zshrc。重新加载之后,每次启动 claude 都会显示配置文件菜单——和迁移后的那套是同样的最终状态,只不过这是从零搭出来的。

4. 精挑每个配置文件的 MCP

最后,只给每个配置文件它该看到的工具:在那个配置文件自己的 settings.json 里设置 disableClaudeAiConnectors: true,然后用 claude mcp add 把它需要的加回来,和上面连接器那一节里完全一样。正是这一步,把十个可以互换的配置文件变成十个各司其职的配置文件。

迁移脚本

给那些读到这里、想从旧方案迁过来的人。最关键的一步是第二步:它把 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 拿掉。

彩蛋:每个配置文件独立的 MCP 连接器(当你的组织共享它们时)

上面这套架构解锁了第二个隔离维度——而它后来成了我实际用得最多的东西。如果你的 Claude 账号属于某个组织,这个组织可以下发一组共享的远程 MCP 连接器(Figma、一个工单系统、一个指标后端、一个数据库网关、一个 CMS…)。Claude Code 会把组织启用的每一个连接器自动拉进每一个会话,因为它们是绑在账号上的,而不是绑在你启动时所在的目录上。

有两三个连接器的时候这没问题。等到十个项目加上十几个组织连接器,就不再没问题了:每个会话都加载着一堆它根本不该碰的工具。这在模型的工具列表里是噪音,是被浪费的 context,而且——真正咬人的那一点——它让一个会话能伸手够到属于另一个项目的系统。在你做项目 B 的时候,项目 A 的连接器就根本不该存在。

关掉自动拉取,然后自己挑选

真正起作用的就是一个设置,disableClaudeAiConnectors(Claude Code v2.1.182+)。它存在于任何 settings 作用域里——包括某个配置文件自己的 settings.json——所以它是CLAUDE_CONFIG_DIR 区分的。把它设成 true,那个配置文件就彻底停止自动拉取组织连接器:

~/.config/claude-profiles/frontend/settings.json
{
  "disableClaudeAiConnectors": true
}

然后只把那个配置文件真正需要的连接器加回来。你显式添加的服务器不会受这个 flag 影响,所以它们照常加载——结果就是一个经过精挑细选的子集,一个配置文件一份:

Terminal — inside the frontend profile
# add back only what this profile uses — writes to the profile's own .claude.json
claude mcp add --scope user --transport http figma  https://<your-figma-connector>/mcp
claude mcp add --scope user --transport http linear https://<your-linear-connector>/mcp
配置文件它加载什么
frontendFigma, Linear, Sentry
backendPostgres, Grafana, Linear
contentNotion, Figma, Linear

同一个账号,同一份组织目录——但每个配置文件只看到属于它自己的那一片,外加你选择在多处重复的那些通用连接器(一个工单系统通常到哪儿都配得上有它一席之地)。

在信它之前我先测了一下

我不想光听文档怎么说,所以我在一个配置文件上跑了一次干净的 A/B——同一个账号,同一个 OAuth token,唯一的区别就是那一个 settings.json:

A/B on one profile
profile WITHOUT the flag
  /mcp → Figma, Linear, Grafana, Postgres, Notion, Sentry, …   (entire org catalog)

same account, profile WITH  disableClaudeAiConnectors: true
  /mcp → (nothing auto-loaded)

…then  claude mcp add  Figma + Linear
  /mcp → Figma, Linear                                         (exactly the curated subset)

它做的正是它声称的事。在你依赖它之前,有两点注意事项值得知道:

  • 重新加回一个 OAuth 连接器需要一次交互式登录。通过 key-in-URL 方式认证的连接器,用 claude mcp add 就能全自动地回来。走 OAuth 的连接器,在那个配置文件里第一次使用时会提示你做一次性的浏览器认证。
  • claude mcp list 根本不会显示组织连接器——只会显示本地定义的和显式添加的服务器。那些自动拉取来的,只能在一个活跃会话里通过 /mcp 才看得到。当 mcp list 看起来里面没有它们时,别慌。

你能得到什么

  • 用一个父目录替代 $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)。