Claude Code Multi-Profile, v2: o arhitectură XDG curată
După o lună pe vechea abordare bazată pe alias, mi-am reconstruit setup-ul multi-profile Claude Code într-o structură conformă XDG sub ~/.config/claude-profiles/. Motivul real nu a fost ordinea — a fost descoperirea că ~/.claude.json este un fișier separat în rădăcina home, unde trăiau discret serverele MCP adăugate cu --scope user.
Acum o lună am publicat un articol despre rularea a două conturi Claude Code în paralel pe aceeași mașină — personal și de muncă, prin CLAUDE_CONFIG_DIR și un alias de shell. Abordarea a funcționat. Totul a făcut ce trebuia să facă.
Dar după o lună de uz real m-am lovit de câteva probleme serioase pe care articolul acela nu le acoperea. Cea mai mare — o ciudățenie ascunsă a Claude Code peste care am dat din întâmplare, când am adăugat MCP-ul Gmail și acesta a "dispărut" din profilul meu cinci minute mai târziu. Azi am reconstruit tot pe o arhitectură nouă — conformă XDG, cu un symlink și un selector interactiv de profil prin gum. Asta e v2 — o evoluție ieșită din experiență reală, nu o îmbunătățire teoretică.
Ce a început să mă roadă
Patru lucruri. Primele trei țin de ordine, vizibilitate și scalare. Al patrulea e adevărata capcană de arhitectură pe care n-am văzut-o imediat. Le iau pe rând, fiindcă tocmai al patrulea a forțat până la urmă reconstrucția.
1. Două foldere separate în $HOME — e o dezordine
~/.claude și ~/.claude-promova — două dotfoldere așezate unul lângă altul în rădăcina lui $HOME. XDG Base Directory Specification spune că fișierele de configurare stau în ~/.config/. Folderele dotfile împrăștiate direct în home sunt un antipattern care, cu timpul, transformă $HOME într-un sertar de vechituri. Cosmetic, de acord, dar mă enerva de fiecare dată când vedeam ls -la ~.
2. Nicio confirmare vizuală a profilului activ
Pornesc claude și n-am idee care profil e activ — până nu rulez claude config list în interiorul sesiunii. Dacă am uitat care terminal l-am pornit unde, trebuia să verific. Un fleac, dar cu personal + muncă rulând în paralel în două taburi se adună.
3. Aliasurile nu scalează
Două profiluri — claude și claude-promova — e în regulă. Adaugi al treilea (un client de freelance) — îți trebuie un al treilea alias. Al patrulea — un al patrulea. După șase luni n-aș mai ține minte ce aliasuri chiar creasem.
4. Capcana ascunsă din ~/.claude.json
Și ăsta e adevăratul motiv al reconstrucției. Claude Code are două locuri diferite pentru configurare, iar documentația nu strigă asta: ~/.claude/ — directorul care ține projects/, sessions/, hooks/, skills/. Și separat — ~/.claude.json, un fișier chiar în rădăcina lui $HOME, unde stau oauthAccount, mcpServers, istoricul projects, skillUsage și cam alte 40 de câmpuri de stare live reală.
Comanda claude mcp add --scope user scrie exact în acel ~/.claude.json din rădăcina home, nu în ~/.claude/.claude.json sau în directorul profilului. Nu știam asta. Până când, într-o zi, m-am lovit de ea.
Descoperire: de ce a "dispărut" MCP-ul Gmail
Azi-dimineață configuram MCP-ul Gmail în Claude Code. Setup standard: proiect Google Cloud, credențiale OAuth, claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp. Totul în regulă. Am repornit sesiunea — merge, citesc mailuri, răspund la mesaje. O oră mai târziu am început să refactorizăm aliasurile într-o funcție cu un picker de profil pe gum, apoi am mutat totul pe XDG. Am făcut mv ~/.claude → ~/.config/claude-profiles/personal, am repornit CC, am ales personal în meniu. Și în noua sesiune am deschis /mcp:
figma (failed)
playwright-test
claude.ai NotionNiciun gmail. Niciun vaultforge. Doar trei servere, dintre care unul chiar eșuase. Și tocmai adăugasem Gmail. Între timp, o sesiune într-alt terminal (profilul de muncă) afișa Gmail și Vaultforge fără nicio problemă.
Am început să sap și am găsit că aveam trei fișiere diferite pe mașină numite .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 |
Iată. Asta e capcana de arhitectură:
claude mcp add --scope userscrie mereu în~/.claude.jsondin rădăcina home, indiferent deCLAUDE_CONFIG_DIR- Când
CLAUDE_CONFIG_DIRe setat, Claude Code citește$CLAUDE_CONFIG_DIR/.claude.json— fișierul din interiorul profilului - Intrările "MCP-uri user-scope" și intrările "MCP-uri de profil" stau în fișiere diferite cu același nume — și sunt ușor de confundat
În cazul meu, ~/.claude.json (rădăcina home, 113 KB) era starea vie, curentă — cu gmail, vaultforge, sesiunea OAuth, tot. Iar ~/.config/claude-profiles/personal/.claude.json (29 KB) s-a dovedit a fi un snapshot vechi care stătea de mai demult în vechiul ~/.claude/ — poate o versiune mai veche de CC a scris acolo, poate un plugin. jq -r 'keys[]' pe ambele fișiere a arătat că versiunea din rădăcina home avea 41 de chei unice care lipseau din snapshot.
Iar cele 41 de chei nu sunt gunoi. E starea reală a Claude Code:
skillUsage— statistici de utilizare a skill-urilorgithubRepoPaths— cache de repo-uri pentru navigarea prin proiectecachedGrowthBookFeatures+cachedStatsigGates— feature flags (fără ele CC le aduce proaspete la fiecare pornire)hasShownOpus45Notice,hasShownOpus46Notice,hasShownS1MWelcomeV2— flaguri UI (fără ele, modalele reapar la următoarea pornire)lastPlanModeUse,feedbackSurveyState,installMethod— stare de onboarding și UX
Dacă faci pur și simplu mv ~/.claude ~/.config/claude-profiles/personal fără merge, pierzi tot. Modalele de welcome revin, căutarea githubRepoPaths rulează din nou, fiecare cerere de sondaj se întoarce. Ceea ce era cât pe ce să fac.
Noua arhitectură
Totul trăiește sub un singur director-părinte în ~/.config/, așa cum vrea XDG. Fiecare profil e autonom — își are starea completă, inclusiv propriul .claude.json. ~/.claude rămâne ca symlink către profilul personal, pentru compatibilitate retroactivă.
~/.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/Niciun .claude.json în rădăcina lui $HOME. Fiecare profil e un director izolat separat, care ține totul: folderele projects/sessions și același fișier cu serverele MCP și token-urile OAuth. O singură sursă de adevăr per profil.
De ce symlink-ul indică spre personal
Tot ce codează hardcodat calea ~/.claude/ — scripturi vechi, plugin-uri, extensiile IDE ale Claude Code, configurări de statusline precum claude-powerline.json — funcționează în continuare fără modificări. Symlink-ul se rezolvă spre profilul personal. Dacă rulezi din greșeală command claude (ocolind funcția wrapper), tot în personal ajungi, prin căutarea pe calea implicită. Personal devine "default-ul tăcut" cum era înainte, dar acum trăiește fizic în locația XDG.
Selector interactiv la pornire — funcție + gum
În loc de aliasuri — o funcție claude() în ~/.zshrc care afișează un meniu cu săgeți prin gum (helper-ul TUI de la Charm). Funcția interceptează apelul claude la nivel de shell, te lasă să alegi un profil și rulează command claude cu CLAUDE_CONFIG_DIR-ul potrivit. command contează aici — ocolește funcția wrapper și invocă binarul real.
# 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
}Cum arată la pornire:
Claude profile:
▸ personal
promova↑↓ pentru navigare, Enter ca să alegi, Esc ca să anulezi (Claude pur și simplu nu pornește). Profilul e mereu vizibil — nu ai cum să-l ratezi.
De la zero (fără v1 de migrat)
Dacă rulezi deja vechiul hack cu două foldere + alias, sari direct la scriptul de migrare de mai jos. Secțiunea asta e construcția curată de la zero — o mașină nouă, sau pur și simplu ~/.claude-ul implicit rezultat din uz normal. Mai întâi decide forma: un cont cu mai multe profiluri (ca să izolezi proiectele și seturile lor de MCP — cazul obișnuit), sau conturi cu adevărat diferite pentru fiecare profil (personal vs. muncă).
1. Așază directoarele
# 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/nullUn director per profil, sub un singur părinte XDG. Dacă ai folosit deja Claude Code, snippetul îți pliază ~/.claude-ul existent ca personal și cară cu el și starea live din ~/.claude.json din rădăcina home; pe o mașină cu adevărat nouă, liniile astea pur și simplu nu fac nimic. Restul sunt directoare goale care se umplu singure la prima pornire — iar chmod 600 contează, fiindcă fișierele astea ajung să conțină token-uri OAuth.
2. Autentifică fiecare profil
# 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.Pornește fiecare profil o dată cu propriul CLAUDE_CONFIG_DIR și rulează /login. Detaliul-cheie: dacă fiecare profil folosește același cont, te loghezi o singură dată — macOS ține credențialul într-o singură intrare partajată în Keychain, așa că orice alt profil e autentificat automat.
Conturi diferite per profil e calea mai spinoasă: acea intrare partajată din Keychain ține un singur login, așa că logarea unui al doilea cont într-alt profil poate suprascrie primul. Dacă ăsta e cazul tău, urmează ghidul dedicat din rularea a două conturi Claude pe aceeași mașină și verifică izolarea credențialelor pe versiunea ta de Claude Code înainte să te bazezi pe ea.
3. Symlink-ul și picker-ul
# 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Îndreaptă ~/.claude spre profilul personal, ca tot ce codează hardcodat calea asta să funcționeze în continuare, apoi aruncă picker-ul claude() cu gum din secțiunea de arhitectură de mai sus în ~/.zshrc. După un reload, claude afișează meniul de profiluri la fiecare pornire — aceeași stare finală ca la setup-ul migrat, doar construită de la zero.
4. Curatoriază MCP-ul fiecărui profil
În final, dă fiecărui profil doar uneltele pe care ar trebui să le vadă: pune disableClaudeAiConnectors: true în settings.json-ul acelui profil și adaugă înapoi ce-i trebuie cu claude mcp add, exact ca în secțiunea despre conectori de mai sus. Asta transformă zece profiluri interschimbabile în zece construite pe scop.
Script de migrare
Pentru oricine citește asta și vrea să treacă de la schema veche. Pasul cel mai critic e al doilea: face merge între ~/.claude.json din rădăcina home și ce e deja în profilul personal, combinând listele mcpServers. Fără pasul ăsta, profilul își pierde și MCP-urile, și toată starea live.
# 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 ~/.claudeAl treilea pas, despre permisiuni, e important în sine. jq | mv creează un fișier cu umask 644 (citibil de oricine). Are token-uri OAuth în el. chmod 600 imediat după merge e obligatoriu.
După migrare — închide și redeschide toate sesiunile Claude active, reîncarcă shell-ul (source ~/.zshrc sau un terminal nou), rulează claude, alege un profil, verifică prin claude mcp list că toate MCP-urile sunt la locul lor. Dacă totul e bine — șterge ~/.claude.json.migrated.bak. Dacă ceva nu e în regulă — rollback-ul e trivial: mv ~/.claude.json.migrated.bak ~/.claude.json și scoate symlink-ul.
Bonus: conectori MCP per profil (când organizația ți-i partajează)
Arhitectura de mai sus deblochează o a doua axă de izolare — și a ajuns lucrul pe care de fapt îl folosesc cel mai mult. Dacă contul tău Claude aparține unei organizații, aceasta poate distribui un set partajat de conectori MCP remote (Figma, un tracker de tichete, un backend de metrici, un gateway de bază de date, un CMS…). Claude Code aduce automat fiecare conector activat de organizație în fiecare sesiune, fiindcă sunt legați de cont, nu de directorul din care ai pornit.
Cu doi-trei conectori e în regulă. Cu zece proiecte și o duzină de conectori de organizație încetează să mai fie: fiecare sesiune încarcă unelte de care n-ar avea nicio treabă să se atingă. E zgomot în lista de unelte a modelului, e context irosit și — partea care chiar doare — lasă o sesiune să întindă mâna într-un sistem care aparține altui proiect. Conectorul pentru proiectul A n-ar trebui nici măcar să existe cât timp lucrezi la proiectul B.
Dezactivează auto-fetch-ul, apoi curatoriază
Pârghia e o singură setare, disableClaudeAiConnectors (Claude Code v2.1.182+). Trăiește în orice scope de setări — inclusiv în settings.json-ul propriu al unui profil — deci e per-CLAUDE_CONFIG_DIR. Pune-o pe true și profilul acela încetează complet să mai aducă automat conectorii de organizație:
{
"disableClaudeAiConnectors": true
}Apoi adaugi înapoi doar conectorii de care profilul chiar are nevoie. Serverele adăugate explicit nu sunt atinse de flag, deci se încarcă normal — rezultatul e un subset curatoriat, profil cu profil:
# 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| Profil | Ce încarcă |
|---|---|
frontend | Figma, Linear, Sentry |
backend | Postgres, Grafana, Linear |
content | Notion, Figma, Linear |
Același cont, același catalog de organizație — dar fiecare profil vede doar felia lui, plus conectorii comuni pe care alegi să-i repeți (un tracker de tichete de obicei își merită locul peste tot).
L-am testat înainte să mă încred în el
N-am vrut să iau documentația de bună, așa că am rulat un A/B curat pe un singur profil — același cont, același token OAuth, singura diferență fiind acel unic settings.json:
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)Face exact ce promite. Două avertismente pe care merită să le știi înainte să te bazezi pe el:
- Readăugarea unui conector OAuth cere un singur login interactiv. Conectorii care se autentifică prin cheie-în-URL revin complet automat prin
claude mcp add. Conectorii din spatele OAuth vor cere o autentificare unică în browser prima dată când îi folosești în acel profil. claude mcp listnu afișează deloc conectorii de organizație — doar serverele definite local și adăugate explicit. Cei aduși automat sunt vizibili doar într-o sesiune vie, prin/mcp. Nu te panica atunci cândmcp listpare golit de ei.
Ce câștigi
- Un singur director-părinte în loc de două dotfoldere în $HOME — conform XDG
- Symlink-ul păstrează compatibilitatea cu tot ce codează hardcodat
~/.claude/ - Fiecare profil e autonom — starea lui completă și MCP-urile lui trăiesc în propriul
.claude.json - O singură sursă de adevăr per profil — gata cu configurarea orfană din rădăcina home care se depărtează tăcut de cea din profil
- Datele sensibile (oauthAccount, token-uri) au garantat permisiuni 600
- Confirmare vizuală a profilului activ la fiecare pornire — nu ai cum să uiți care profil e activ
- Adăugarea unui al treilea profil = adăugarea unei singure linii în
case-ul funcției, nu clonarea unui nou alias și ținerea minte a numelui lui
Unde lasă de dorit
Vreau să fiu sincer. Nu e o îmbunătățire gratis — au venit la pachet câteva compromisuri și merită știute din start.
gume o dependență în plus (brew install gum, ~13 MB). Dacă chiar nu vrei să-l instalezi — poți cădea înapoi peselectdin zsh sau pe un simpluread. Merge, dar nu arată la fel de bine și n-are navigare cu săgeți.- O apăsare de Enter la fiecare pornire. Pentru cine dă drumul la
claudede zeci de ori pe zi, asta poate deveni enervant. Alternativa mai jos (direnv). - Symlink-ul
~/.claude → personalface personal-ul default. Dacă îți trebuie profilul de muncă drept default, trebuie să redirecționezi symlink-ul (ln -sf). Nu e greu, dar nu e nici "uită de el și nu se strică nimic". - Symlink-ul s-ar putea, teoretic, strica dacă vreo unealtă rescrie atomic
~/.claude.jsonprintr-un pattern temp+rename (write-file-atomic). În practică, Claude Code însuși nu face asta, dar dacă instalezi plugin-uri terțe — verifică. - Dacă ai conturi Anthropic diferite pe profiluri diferite, cu planuri diferite — poate exista un lag sub o secundă după comutare, cât timp Claude Code sincronizează starea OAuth. În uzul meu nu se simte, dar nu e zero.
Alternative pe care le-am luat în calcul
direnv — setează automat CLAUDE_CONFIG_DIR pe baza unui .envrc din rădăcina fiecărui proiect. Zero interacțiune, zero clicuri. Dezavantaj: trebuie să pui un .envrc în fiecare work root, iar dacă rulezi claude într-un folder nerecunoscut, primești profilul default (care s-ar putea să nu fie cel dorit). Pentru cine trăiește într-un set restrâns de work roots și vrea să nu dea niciodată vreun clic, direnv e realmente mai bun.
Comutarea pe bază de symlink (un singur profil activ prin redirecționarea symlink-ului ~/.claude) am luat-o și eu în calcul și am renunțat imediat. Nu poți avea două terminale cu profiluri diferite deschise în același timp — "curentul" global e unic. Pentru mine, ăsta e un deal-breaker.
Concluzie
v2 nu e doar un UX mai bun peste v1. E o recunoaștere a faptului că Claude Code are o ciudățenie de arhitectură ascunsă (~/.claude.json ca fișier separat în rădăcina home, scris de comenzile --scope user indiferent de CLAUDE_CONFIG_DIR) de care trebuie să ții cont dacă vrei izolare reală între profiluri. Prima abordare (~/.claude + ~/.claude-promova + alias) mergea în proporție de 80%, dar restul de 20% se manifesta ca o derivă tăcută a stării între profiluri. Acum e luat în calcul. Dacă abia începi — pornește direct cu v2. Dacă ești deja pe v1 — scriptul de migrare e mai sus, mutarea durează cinci minute și nu strică nimic (tocmai merge-ul cu jq e pasul care te salvează de la pierderea stării).