Skip to main content
← Назад к блогу
claude-codeproductivityclidevtools

Claude Code Multi-Profile, v2: чистая XDG-архитектура

После месяца на старом алиас-подходе я переделал свой multi-profile сетап Claude Code на XDG-совместимую структуру ~/.config/claude-profiles/. Настоящая причина — не аккуратность, а обнаружение ~/.claude.json как отдельного файла в home root, в котором тихо жили MCP-серверы, добавленные через --scope user.

Опубликовано 13 июня 2026 г.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. Алиасы не масштабируются

Два профиля — 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 пишет именно в ~/.claude.json в home root, а не в ~/.claude/.claude.json или в профильную директорию. Я этого не знал. Пока в один прекрасный день не наступил.

Discovery: почему Gmail MCP "исчез"

Сегодня утром я ставил Gmail MCP в Claude Code. Сетап обычный: Google Cloud project, 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 всегда пишет в ~/.claude.json в home root, независимо от CLAUDE_CONFIG_DIR
  2. Claude Code при наличии CLAUDE_CONFIG_DIR читает $CLAUDE_CONFIG_DIR/.claude.json — то есть файл внутри профиля
  3. Записи "user-scope MCPs" и "профильные MCPs" живут в разных файлах с одинаковым именем — и их легко перепутать

В моём случае ~/.claude.json (home root, 113 KB) был живым, актуальным state — с gmail, vaultforge, oauth-сессией, всем. А ~/.config/claude-profiles/personal/.claude.json (29 KB) оказался старым snapshot'ом, который почему-то лежал в old ~/.claude/ ещё раньше — может, старая версия CC писала туда, может, плагин. jq -r 'keys[]' на обоих файлах показал, что в home-root версии есть 41 уникальный ключ, которого нет в snapshot'е.

И эти 41 ключ — это не мусор. Это настоящее состояние Claude Code:

  • skillUsage — статистика использования skills
  • githubRepoPaths — кэш репозиториев для project-навигации
  • cachedGrowthBookFeatures + cachedStatsigGates — feature flags (без них CC ходит за свежими каждый раз при старте)
  • hasShownOpus45Notice, hasShownOpus46Notice, hasShownS1MWelcomeV2 — UI флаги (без них модальные окна снова появятся при следующем запуске)
  • lastPlanModeUse, feedbackSurveyState, installMethod — onboarding и UX state

Если просто mv ~/.claude ~/.config/claude-profiles/personal без merge — потеряешь всё это. Заново увидишь welcome-модалки, заново выполнит поиск githubRepoPaths, заново получишь все survey-просьбы. Как я чуть не сделал.

Новая архитектура

Всё живёт под одним родительским каталогом в ~/.config/, как того хочет XDG. Каждый профиль самодостаточен — имеет свой полный state, включая собственный .claude.json. ~/.claude остаётся как symlink на personal-профиль для обратной совместимости.

~/.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/

Никакого .claude.json в корне $HOME. Каждый профиль — это отдельная изолированная директория, где лежит всё: и папки projects/sessions, и тот самый файл с MCP-серверами и oauth-токенами. Один источник правды на профиль.

Всё, что хардкодит путь ~/.claude/ — старые скрипты, плагины, IDE-расширения Claude Code, statusline-конфиги типа claude-powerline.json — продолжает работать без изменений. Symlink резолвится в personal-профиль. Если случайно запустишь command claude (без обёртки-функции) — тоже попадёшь в personal через default-path lookup. Personal становится "тихим default'ом", как было раньше, но теперь физически живёт в XDG-локации.

Интерактивный выбор при запуске — функция + gum

Вместо алиасов — функция claude() в ~/.zshrc, которая показывает меню со стрелками через gum (TUI-helper от Charm). Функция перехватывает вызов claude на shell-уровне, даёт выбрать профиль и запускает command claude с соответствующим CLAUDE_CONFIG_DIR. command важно — оно обходит функцию-обёртку и вызывает реальный бинарь.

~/.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 просто не запускается). Профиль виден всегда — забыть невозможно.

Скрипт миграции

Для тех, кто это читает и хочет переехать со старой схемы. Самый критичный шаг — второй: он сливает ~/.claude.json из home root с тем, что уже лежит в personal-профиле, объединяя списки 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. Обязательно chmod 600 сразу после merge.

После миграции — закрой и открой все активные Claude сессии, перезагрузи shell (source ~/.zshrc или новый терминал), запусти claude, выбери профиль, проверь через claude mcp list, что все MCP на месте. Если всё ок — удали ~/.claude.json.migrated.bak. Если что-то не так — откат тривиален: mv ~/.claude.json.migrated.bak ~/.claude.json и убрать symlink.

Что ты получаешь

  • Один родительский каталог вместо двух dotfolder'ов в $HOME — XDG-совместимо
  • Symlink сохраняет совместимость со всем, что хардкодит ~/.claude/
  • Каждый профиль самодостаточен — свой полный state и свои MCP в собственном .claude.json
  • Один источник правды на профиль — больше нет orphan-конфига в home root, который тихо расходится с профильным
  • Sensitive data (oauthAccount, tokens) гарантированно с правами 600
  • Визуальное подтверждение активного профиля при каждом запуске — забыть, какой профиль активен, невозможно
  • Добавить третий профиль = добавить одну строку в case функции, а не клонировать новый алиас и помнить его имя

Где это даёт слабину

Хочу честно. Это не бесплатное улучшение — несколько компромиссов пришли в комплекте, и о них стоит знать заранее.

  • gum — дополнительная зависимость (brew install gum, ~13 MB). Если принципиально не хочешь ставить — fallback на select в zsh или простой read. Работает, но выглядит не так красиво и не имеет стрелочной навигации.
  • Один Enter при каждом запуске. Для тех, кто запускает claude десятки раз в день — может раздражать. Альтернатива ниже (direnv).
  • Symlink ~/.claude → personal делает personal default'ом. Если нужно сделать default work-профиль — нужно перенаправить symlink (ln -sf). Несложно, но это не "забыл — ничего не сломается".
  • Symlink теоретически может сломаться, если какой-то инструмент атомарно перезаписывает ~/.claude.json через temp+rename pattern (write-file-atomic). На практике Claude Code сам так не делает, но если ставишь сторонние плагины — проверь.
  • Если у тебя на профилях разные Anthropic-аккаунты с разными планами — после переключения может быть sub-second лаг, пока Claude Code синкает OAuth state. В моём использовании незаметно, но это не ноль.

Альтернативы, которые рассматривал

direnv — автоматически выставляет CLAUDE_CONFIG_DIR в зависимости от .envrc в корне каждого проекта. Ноль интерактива, ноль кликов. Минус: нужно прописать .envrc в каждом work-root, и если запускаешь claude в нераспознанной папке — получаешь дефолтный профиль (может быть не тот, что нужно). Для тех, кто живёт в ограниченном количестве work-roots и хочет никогда не кликать — direnv реально лучше.

Symlink-based switch (единственный активный профиль через перенаправление ~/.claude symlink) я тоже рассмотрел и сразу отбросил. Не можешь иметь два терминала с разными профилями одновременно — глобальная "текущая" одна. Для меня это deal-breaker.

Вывод

v2 — это не просто лучший UX над v1. Это признание того, что Claude Code имеет скрытую архитектурную особенность (~/.claude.json как отдельный файл в home root, который пишется --scope user командами независимо от CLAUDE_CONFIG_DIR), которую нужно учесть, если хочешь настоящую изоляцию между профилями. Первый подход (~/.claude + ~/.claude-promova + алиас) работал на 80%, но те 20%, что остались, проявлялись молчаливым расхождением состояния между профилями. Теперь это учтено. Если ты только начинаешь — стартуй сразу с v2. Если уже сидишь на v1 — скрипт миграции выше, переезд занимает пять минут и ничего не ломает (именно jq merge — это тот шаг, который спасает тебя от потери state).