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 root, де живе 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 функції, а не клонувати новий аліас і пам'ятати його ім'я

Where it falls short

Я хочу чесно. Це не безплатне покращення — деякі компроміси прийшли в комплекті, і їх варто знати наперед.

  • 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).