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 р.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. Аліаси не масштабуються

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

З нуля (без v1 для міграції)

Якщо ти вже ганяв старий хак із двома теками + alias — гортай нижче до скрипта міграції. Ця секція — чиста збірка з нуля: свіжа машина або просто дефолтний ~/.claude від звичайного користування. Спершу визнач форму: один акаунт із кількома профілями (щоб ізолювати проєкти та їхні набори MCP — найчастіший випадок) або справді різні акаунти на профіль (особистий vs робочий).

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-root live-стан ~/.claude.json; на цілком свіжій машині ці рядки просто нічого не роблять. Решта — порожні теки, які наповнюються самі при першому запуску. А chmod 600 важливий — бо в цих файлах осідають OAuth-токени.

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-профіль, щоб усе, що хардкодить цей шлях, працювало далі, а потім додай функцію-picker claude() з gum із секції архітектури вище у ~/.zshrc. Після перезавантаження claude показує меню профілів при кожному запуску — той самий кінцевий стан, що й у мігрованої системи, лише зібраний з нуля.

4. Відбери MCP для кожного профілю

Наостанок дай кожному профілю лише ті інструменти, які він має бачити: постав disableClaudeAiConnectors: true у його settings.json і поверни потрібне через claude mcp add — рівно як у секції про конектори вище. Саме це перетворює десять однакових профілів на десять заточених під задачу.

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

Для тих, хто читає це і хоче переїхати зі старої схеми. Найкритичніший крок — другий: він зливає ~/.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.

Бонус: MCP-конектори per-profile (коли організація їх розшарює)

Архітектура вище відкриває другу вісь ізоляції — і саме нею я зрештою користуюся найбільше. Якщо твій акаунт Claude належить організації, вона може віддавати спільний набір remote MCP-конекторів (Figma, трекер задач, бекенд метрик, шлюз до бази, CMS…). Claude Code підтягує кожен увімкнений в організації конектор у кожну сесію — бо вони прив'язані до акаунта, а не до теки, з якої ти запустився.

З двома-трьома конекторами це ок. З десятьма проєктами і дюжиною організаційних конекторів — уже ні: кожна сесія вантажить інструменти, до яких їй взагалі не треба торкатися. Це шум у списку тулів моделі, це з'їдений контекст, і — найнеприємніше — це дозволяє сесії дотягнутися до системи, що належить іншому проєкту. Конектора проєкту A не має навіть існувати, поки ти працюєш над проєктом B.

Вимкни auto-fetch, потім відбирай

Важіль — одне налаштування, disableClaudeAiConnectors (Claude Code v2.1.182+). Воно діє в будь-якому scope — зокрема у власному settings.json профілю — тобто per-CLAUDE_CONFIG_DIR. Виставляєш true — і цей профіль повністю перестає підтягувати організаційні конектори:

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

Далі повертаєш лише ті конектори, які цьому профілю реально потрібні. Сервери, додані явно, прапор не чіпає — вони вантажаться як завжди. На виході — курована підмножина, профіль за профілем:

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-токен, єдина різниця — той один 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-конектора потребує одного інтерактивного логіну. Конектори, що авторизуються ключем-у-URL, повертаються повністю автоматично через claude mcp add. Конектори за OAuth попросять одноразову авторизацію в браузері при першому використанні в цьому профілі.
  • claude mcp list організаційних конекторів не показує взагалі — лише локально визначені та явно додані сервери. Auto-fetch'нуті видно тільки в живій сесії через /mcp. Не лякайся, коли mcp list виглядає "порожнім" на них.

Що ти отримуєш

  • Один батьківський каталог замість двох 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).