Claude Code Multi-Profile, v2: en ren XDG-arkitektur
Efter en månad med det gamla alias-baserade upplägget byggde jag om mitt multi-profile-setup för Claude Code till en XDG-kompatibel struktur under ~/.config/claude-profiles/. Den verkliga orsaken var inte snyggheten — det var att jag upptäckte att ~/.claude.json är en separat fil i home-roten där MCP-servrarna som lagts till via --scope user tyst levde sitt eget liv.
För en månad sedan publicerade jag ett inlägg om hur man kör två Claude Code-konton parallellt på samma maskin — privat och jobb, via CLAUDE_CONFIG_DIR och ett shell-alias. Upplägget fungerade. Allt gjorde det det skulle göra.
Men efter en månads riktig användning sprang jag på ett par rejäla problem som det inlägget inte täckte. Det största — en dold egenhet hos Claude Code som jag av en slump trampade rakt in i när jag lade till Gmail-MCP:n och den "försvann" från min profil fem minuter senare. Idag byggde jag om alltihop till en ny arkitektur — XDG-kompatibel, med en symlänk och en interaktiv profilväljare via gum. Det här är v2 — en evolution ur verklig erfarenhet, inte en teoretisk förbättring.
Det som började skava
Fyra saker. De första tre handlar om snygghet, synlighet och skala. Den fjärde är den riktiga arkitektoniska fällan som jag inte såg direkt. Jag går igenom dem i tur och ordning, för det är just den fjärde som till slut tvingade fram ombyggnaden.
1. Två separata mappar i $HOME — det är rörigt
~/.claude och ~/.claude-promova — två dotfolders sida vid sida i $HOME-roten. XDG Base Directory Specification säger att konfigar hör hemma i ~/.config/. Dotfile-mappar utspridda direkt i home är ett antimönster som med tiden gör $HOME till en skräplåda. Kosmetiskt, visst, men det stack i ögonen varje gång jag körde ls -la ~.
2. Ingen visuell bekräftelse på aktiv profil
Jag startar claude och vet inte vilken profil som är aktiv — inte förrän jag kör claude config list inne i sessionen. Om jag glömt vilken terminal jag startat var, måste jag kolla. En småsak, men med personal + work i parallell i två flikar tornar det upp sig.
3. Alias skalar inte
Två profiler — claude och claude-promova — okej. Jag lägger till en tredje (en frilanskund) — då behövs ett tredje alias. En fjärde — ett fjärde. Efter ett halvår skulle jag inte minnas vilka alias jag faktiskt skapat.
4. Den dolda fällan i ~/.claude.json
Och det här är det verkliga skälet till ombyggnaden. Claude Code har två olika platser för konfiguration, och dokumentationen skriker inte ut det: ~/.claude/ — katalogen där projects/, sessions/, hooks/, skills/ ligger. Och separat — ~/.claude.json, en fil direkt i $HOME-roten, där oauthAccount, mcpServers, projects-historiken, skillUsage och ungefär 40 andra fält av faktisk live state bor.
Kommandot claude mcp add --scope user skriver just till ~/.claude.json i home-roten, inte till ~/.claude/.claude.json eller profilens katalog. Det visste jag inte. Tills jag en dag trampade rätt i det.
Discovery: varför Gmail-MCP:n "försvann"
I morse satte jag upp Gmail-MCP:n i Claude Code. Vanligt setup: Google Cloud-projekt, OAuth-credentials, claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp. Allt OK. Startade om sessionen — funkar, jag läser mail, svarar på meddelanden. En timme senare började vi refaktorera aliasen till en funktion med gum-profilväljare, och flyttade sedan över alltihop till XDG. Jag körde mv ~/.claude → ~/.config/claude-profiles/personal, startade om CC, valde personal i menyn. Och i den nya sessionen öppnade jag /mcp:
figma (failed)
playwright-test
claude.ai NotionIngen gmail. Ingen vaultforge. Bara tre servrar, varav en till och med failed. Och jag hade just lagt till Gmail. Samtidigt visade sessionen i en annan terminal (work-profilen) Gmail och Vaultforge utan minsta problem.
Jag började gräva och hittade att jag på maskinen hade tre olika filer med namnet .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 |
Där hade vi det. Det är just det som är den arkitektoniska fällan:
claude mcp add --scope userskriver alltid till~/.claude.jsoni home-roten, oberoende avCLAUDE_CONFIG_DIR- När
CLAUDE_CONFIG_DIRär satt läser Claude Code$CLAUDE_CONFIG_DIR/.claude.json— alltså filen inuti profilen - Posterna för "user-scope MCPs" och "profilens MCPs" lever i olika filer med samma namn — och de är lätta att blanda ihop
I mitt fall var ~/.claude.json (home-roten, 113 KB) det levande, aktuella tillståndet — med gmail, vaultforge, OAuth-sessionen, allt. Och ~/.config/claude-profiles/personal/.claude.json (29 KB) visade sig vara en gammal snapshot som av någon anledning redan tidigare legat i gamla ~/.claude/ — kanske skrev en äldre CC-version dit, kanske ett plugin. jq -r 'keys[]' på båda filerna visade att home-root-versionen hade 41 unika nycklar som saknades i snapshoten.
Och de 41 nycklarna är inte skräp. Det är Claude Codes verkliga tillstånd:
skillUsage— användningsstatistik för skillsgithubRepoPaths— repo-cache för projektnavigeringcachedGrowthBookFeatures+cachedStatsigGates— feature flags (utan dem hämtar CC nya vid varje start)hasShownOpus45Notice,hasShownOpus46Notice,hasShownS1MWelcomeV2— UI-flaggor (utan dem dyker modalerna upp igen vid nästa start)lastPlanModeUse,feedbackSurveyState,installMethod— onboarding- och UX-state
Om du bara kör mv ~/.claude ~/.config/claude-profiles/personal utan merge — förlorar du allt detta. Du ser welcome-modalerna igen, githubRepoPaths-sökningen körs på nytt, och alla survey-prompts kommer tillbaka. Som jag nästan gjorde.
Den nya arkitekturen
Allt lever under en gemensam föräldrakatalog i ~/.config/, precis som XDG vill ha det. Varje profil är självförsörjande — den har sitt fulla state inklusive en egen .claude.json. ~/.claude stannar kvar som symlänk till personal-profilen för bakåtkompatibilitet.
~/.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/Ingen .claude.json i $HOME-roten. Varje profil är en separat, isolerad katalog där allt ligger: projects/sessions-mapparna och samma fil med MCP-servrar och OAuth-tokens. En source of truth per profil.
Varför symlänken pekar på personal
Allt som hårdkodat sökvägen ~/.claude/ — gamla skript, plugins, Claude Codes IDE-tillägg, statusline-konfigar som claude-powerline.json — fortsätter fungera utan ändringar. Symlänken resolveras till personal-profilen. Om du av misstag kör command claude (utan wrapper-funktionen) — hamnar du också i personal via default-path lookup. Personal blir den "quiet default" den var förr, men bor nu fysiskt på XDG-platsen.
Interaktiv väljare vid start — funktion + gum
I stället för alias — en funktion claude() i ~/.zshrc som visar en pilmeny via gum (Charms TUI-hjälpverktyg). Funktionen fångar claude-anropet på shell-nivå, låter dig välja profil och kör command claude med motsvarande CLAUDE_CONFIG_DIR. command är viktigt — det förbigår wrapper-funktionen och anropar den riktiga binärfilen.
# 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
}Så här ser det ut vid start:
Claude profile:
▸ personal
promova↑↓ för att navigera, Enter för att välja, Esc för att avbryta (Claude startar helt enkelt inte). Profilen är alltid synlig — omöjlig att missa.
Migreringsskript
För den som läser det här och vill flytta från det gamla upplägget. Det mest kritiska steget är det andra: det mergar ~/.claude.json från home-roten med det som redan ligger i personal-profilen, och slår ihop mcpServers-listorna. Utan det steget förlorar profilen både sina MCPs och hela sitt live state.
# 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 ~/.claudeDet tredje steget om rättigheter är viktigt i sig. jq | mv skapar en fil med umask 644 (world-readable). Den innehåller OAuth-tokens. chmod 600 direkt efter mergen är obligatoriskt.
Efter migreringen — stäng och öppna alla aktiva Claude-sessioner, ladda om shellet (source ~/.zshrc eller ny terminal), kör claude, välj profil, verifiera via claude mcp list att alla MCPs är på plats. Om allt är OK — radera ~/.claude.json.migrated.bak. Om något är fel — rollback är trivialt: mv ~/.claude.json.migrated.bak ~/.claude.json och ta bort symlänken.
Det du får
- En gemensam föräldrakatalog i stället för två dotfolders i $HOME — XDG-kompatibelt
- Symlänken bevarar kompatibiliteten med allt som hårdkodar
~/.claude/ - Varje profil är självförsörjande — sitt fulla state och sina MCPs i sin egen
.claude.json - En source of truth per profil — slut på övergiven konfig i home-roten som tyst driver isär från profilens
- Känslig data (oauthAccount, tokens) garanterat med rättigheter 600
- Visuell bekräftelse på aktiv profil vid varje start — omöjligt att glömma vilken profil som är aktiv
- Lägga till en tredje profil = lägga till en rad i
casei funktionen, inte klona ett nytt alias och komma ihåg dess namn
Där det brister
Jag vill vara ärlig. Det här är inte en gratis förbättring — några kompromisser följer med på köpet, och dem är värda att känna till i förväg.
gumär ett extra beroende (brew install gum, ~13 MB). Om du av princip inte vill installera det — fallback tillselecti zsh eller en enkelread. Funkar, men ser inte lika snyggt ut och saknar pilnavigering.- Ett Enter vid varje start. För den som drar igång
claudetiotals gånger om dagen — kan irritera. Alternativ nedan (direnv). - Symlänken
~/.claude → personalgör personal till default. Om du behöver work-profilen som default — måste du peka om symlänken (ln -sf). Inte svårt, men det är inte "glöm bort det och inget går sönder". - Symlänken kan teoretiskt gå sönder om något verktyg atomärt skriver om
~/.claude.jsonvia ett temp+rename-mönster (write-file-atomic). I praktiken gör Claude Code inte det själv, men om du installerar tredjepartsplugins — kolla. - Om du har olika Anthropic-konton på profilerna med olika planer — efter byte kan det bli en lag på under en sekund medan Claude Code synkar OAuth-state. I min användning märks det inte, men det är inte noll.
Alternativ jag övervägde
direnv — sätter automatiskt CLAUDE_CONFIG_DIR utifrån en .envrc i roten på varje projekt. Noll interaktion, noll klick. Minus: du måste lägga en .envrc i varje work-root, och om du kör claude i en okänd mapp — får du default-profilen (kanske inte den du vill ha). För den som lever i ett begränsat antal work-roots och aldrig vill klicka — är direnv faktiskt bättre.
Symlänk-baserad växling (en enda aktiv profil genom att peka om ~/.claude-symlänken) övervägde jag också och förkastade direkt. Du kan inte ha två terminaler med olika profiler öppna samtidigt — det globala "aktuella" är ett enda. För mig är det en deal-breaker.
Slutsats
v2 är inte bara bättre UX ovanpå v1. Det är ett erkännande av att Claude Code har en dold arkitektonisk egenhet (~/.claude.json som separat fil i home-roten, skriven av --scope user-kommandon oberoende av CLAUDE_CONFIG_DIR) som man måste ta hänsyn till om man vill ha riktig isolation mellan profiler. Den första ansatsen (~/.claude + ~/.claude-promova + alias) fungerade till 80 %, men de återstående 20 % visade sig som tyst state-drift mellan profilerna. Nu är det hanterat. Om du precis börjar — starta direkt med v2. Om du redan sitter på v1 — migreringsskriptet finns ovan, flytten tar fem minuter och bryter ingenting (det är just jq-mergen som räddar dig från state-förlust).