Claude Code Multi-Profile, v2: en ren XDG-arkitektur
Efter en måned med den gamle alias-baserede tilgang byggede jeg mit Claude Code multi-profile-setup om til en XDG-kompatibel struktur under ~/.config/claude-profiles/. Den egentlige grund var ikke ryddelighed — det var opdagelsen af, at ~/.claude.json er en separat fil i home-roden, hvor MCP-serverne tilføjet via --scope user stille boede.
For en måned siden udgav jeg et indlæg om at køre to Claude Code-konti parallelt på den samme maskine — privat og arbejde, via CLAUDE_CONFIG_DIR og et shell-alias. Tilgangen virkede. Alt gjorde, hvad det skulle.
Men efter en måneds reel brug stødte jeg på et par væsentlige problemer, som det indlæg ikke dækkede. Det største — en skjult særegenhed ved Claude Code, som jeg ved et tilfælde trådte midt i, da jeg tilføjede Gmail-MCP'en, og den "forsvandt" fra min profil fem minutter senere. I dag byggede jeg det hele om til en ny arkitektur — XDG-kompatibel, med et symlink og en interaktiv profilvælger via gum. Dette er v2 — en evolution ud af reel erfaring, ikke en teoretisk forbedring.
Det, der begyndte at irritere
Fire ting. De første tre handler om ryddelighed, synlighed og skala. Den fjerde er den ægte arkitektoniske fælde, som jeg ikke så med det samme. Jeg går dem igennem i rækkefølge, for det er netop den fjerde, der til sidst tvang ombygningen.
1. To separate mapper i $HOME — det er roderi
~/.claude og ~/.claude-promova — to dotfolders side om side i roden af $HOME. XDG Base Directory Specification siger, at configs hører hjemme i ~/.config/. Dotfile-mapper spredt direkte i home er et antimønster, der med tiden gør $HOME til en skuffe med rod. Kosmetisk, javel, men det irriterede mig hver gang, jeg så ls -la ~.
2. Ingen visuel bekræftelse på aktiv profil
Jeg starter claude og ved ikke, hvilken profil der er aktiv — før jeg kører claude config list inde i sessionen. Hvis jeg har glemt, hvilken terminal jeg har startet hvor, må jeg tjekke. En bagatel, men med personal + work kørende parallelt i to faner hober det sig op.
3. Aliaser skalerer ikke
To profiler — claude og claude-promova — det er fint. Tilføj en tredje (en freelancekunde) — så skal der bruges et tredje alias. En fjerde — et fjerde. Efter et halvt år ville jeg ikke huske, hvilke aliaser jeg faktisk havde oprettet.
4. Den skjulte fælde i ~/.claude.json
Og dette er den egentlige grund til ombygningen. Claude Code har to forskellige steder til konfiguration, og dokumentationen råber det ikke ud: ~/.claude/ — kataloget, hvor projects/, sessions/, hooks/, skills/ ligger. Og separat — ~/.claude.json, en fil direkte i $HOME-roden, hvor oauthAccount, mcpServers, projects-historikken, skillUsage og omkring 40 andre felter af ægte live state bor.
Kommandoen claude mcp add --scope user skriver netop til ~/.claude.json i home-roden og ikke til ~/.claude/.claude.json eller til profilkataloget. Det vidste jeg ikke. Indtil jeg en dag trådte lige i det.
Discovery: hvorfor Gmail-MCP'en "forsvandt"
I morges satte jeg Gmail-MCP'en op i Claude Code. Sædvanligt setup: Google Cloud-projekt, OAuth-credentials, claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp. Alt OK. Genstartede sessionen — virker, jeg læser mail, svarer på beskeder. En time senere begyndte vi at refaktorere aliaserne til en funktion med gum-profilvælger, og derefter flyttede vi alt over på XDG. Jeg kørte mv ~/.claude → ~/.config/claude-profiles/personal, genstartede CC, valgte personal i menuen. Og i den nye session åbnede jeg /mcp:
figma (failed)
playwright-test
claude.ai NotionIngen gmail. Ingen vaultforge. Kun tre servere, hvoraf én oven i købet var failed. Og jeg havde lige tilføjet Gmail. Imens viste sessionen i en anden terminal (work-profilen) Gmail og Vaultforge uden mindste problem.
Jeg begyndte at grave og fandt, at jeg på maskinen havde tre forskellige filer med navnet .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 |
Der var den. Det er netop dét, der er den arkitektoniske fælde:
claude mcp add --scope userskriver altid til~/.claude.jsoni home-roden, uafhængigt afCLAUDE_CONFIG_DIR- Når
CLAUDE_CONFIG_DIRer sat, læser Claude Code$CLAUDE_CONFIG_DIR/.claude.json— altså filen inde i profilen - Posterne for "user-scope MCPs" og "profilens MCPs" bor i forskellige filer med samme navn — og de er lette at forveksle
I mit tilfælde var ~/.claude.json (home-roden, 113 KB) den levende, aktuelle tilstand — med gmail, vaultforge, OAuth-sessionen, det hele. Og ~/.config/claude-profiles/personal/.claude.json (29 KB) viste sig at være en gammel snapshot, der af en eller anden grund allerede tidligere lå i gamle ~/.claude/ — måske skrev en ældre CC-version dertil, måske et plugin. jq -r 'keys[]' på begge filer viste, at home-root-versionen havde 41 unikke nøgler, som ikke fandtes i snapshotten.
Og de 41 nøgler er ikke skrald. Det er Claude Codes virkelige tilstand:
skillUsage— brugsstatistik for skillsgithubRepoPaths— repo-cache til projektnavigeringcachedGrowthBookFeatures+cachedStatsigGates— feature flags (uden dem henter CC nye ved hver start)hasShownOpus45Notice,hasShownOpus46Notice,hasShownS1MWelcomeV2— UI-flag (uden dem dukker modalerne op igen ved næste start)lastPlanModeUse,feedbackSurveyState,installMethod— onboarding- og UX-state
Hvis du bare kører mv ~/.claude ~/.config/claude-profiles/personal uden merge — mister du det hele. Du ser welcome-modalerne igen, githubRepoPaths-søgningen kører på ny, alle survey-prompts vender tilbage. Som jeg nær havde gjort.
Den nye arkitektur
Alt bor under ét fælles forældrekatalog i ~/.config/, sådan som XDG vil have det. Hver profil er selvstændig — den har sin fulde state inklusive sin egen .claude.json. ~/.claude bliver tilbage som symlink til personal-profilen for bagudkompatibilitet.
~/.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-roden. Hver profil er et separat, isoleret katalog, hvor alt ligger: projects/sessions-mapperne og den samme fil med MCP-servere og OAuth-tokens. Én source of truth pr. profil.
Hvorfor symlinket peger på personal
Alt, der hardkoder stien ~/.claude/ — gamle scripts, plugins, Claude Codes IDE-udvidelser, statusline-configs som claude-powerline.json — bliver ved med at virke uden ændringer. Symlinket resolves til personal-profilen. Hvis du ved et tilfælde kører command claude (uden wrapper-funktionen) — havner du også i personal via default-path lookup. Personal bliver den "quiet default", den var før, men bor nu fysisk på XDG-pladsen.
Interaktiv vælger ved start — funktion + gum
I stedet for aliaser — en funktion claude() i ~/.zshrc, der viser en pilemenu via gum (Charms TUI-hjælper). Funktionen opfanger claude-kaldet på shell-niveau, lader dig vælge en profil og kører command claude med det tilsvarende CLAUDE_CONFIG_DIR. command er vigtigt — det går uden om wrapper-funktionen og kalder den ægte binær.
# 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ådan ser det ud ved start:
Claude profile:
▸ personal
promova↑↓ for at navigere, Enter for at vælge, Esc for at afbryde (Claude starter simpelthen ikke). Profilen er altid synlig — umuligt at overse.
Migrationsscript
Til den, der læser dette og vil flytte fra det gamle skema. Det mest kritiske trin er det andet: det merger ~/.claude.json fra home-roden med det, der allerede ligger i personal-profilen, og samler mcpServers-listerne. Uden det trin mister profilen både sine MCP'er og hele sin 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 trin om rettigheder er vigtigt for sig selv. jq | mv opretter en fil med umask 644 (world-readable). Inde i den ligger OAuth-tokens. chmod 600 umiddelbart efter mergen er obligatorisk.
Efter migrationen — luk og åbn alle aktive Claude-sessioner, genindlæs shellet (source ~/.zshrc eller ny terminal), kør claude, vælg en profil, tjek via claude mcp list at alle MCP'er er på plads. Hvis alt er OK — slet ~/.claude.json.migrated.bak. Hvis noget er galt — rollback er trivielt: mv ~/.claude.json.migrated.bak ~/.claude.json og fjern symlinket.
Det, du får
- Ét fælles forældrekatalog i stedet for to dotfolders i $HOME — XDG-kompatibelt
- Symlinket bevarer kompatibilitet med alt, der hardkoder
~/.claude/ - Hver profil er selvstændig — sin fulde state og sine MCP'er i sin egen
.claude.json - Én source of truth pr. profil — slut med forældreløs config i home-roden, der stille driver fra profilens
- Følsomme data (oauthAccount, tokens) garanteret med rettigheder 600
- Visuel bekræftelse på aktiv profil ved hver start — umuligt at glemme, hvilken profil der er aktiv
- At tilføje en tredje profil = at tilføje én linje i
casei funktionen, ikke klone et nyt alias og huske dets navn
Hvor det halter
Jeg vil være ærlig. Dette er ikke en gratis forbedring — et par kompromiser fulgte med i pakken, og dem er det værd at kende på forhånd.
gumer en ekstra afhængighed (brew install gum, ~13 MB). Hvis du principielt ikke vil installere det — fallback tilselecti zsh eller en simpelread. Virker, men ser ikke lige så pænt ud og mangler pilenavigation.- Et Enter-tryk ved hver start. For den, der starter
claudedusinvis af gange om dagen — kan irritere. Alternativ nedenfor (direnv). - Symlinket
~/.claude → personalgør personal til default. Hvis du har brug for work-profilen som default, skal du pege symlinket om (ln -sf). Ikke svært, men det er ikke "glem det og intet går i stykker". - Symlinket kan teoretisk gå i stykker, hvis et eller andet værktøj atomisk overskriver
~/.claude.jsonvia et temp+rename-mønster (write-file-atomic). I praksis gør Claude Code det ikke selv, men hvis du installerer tredjepartsplugins — så tjek. - Hvis du har forskellige Anthropic-konti på profilerne med forskellige planer — efter skift kan der være et sub-sekund lag, mens Claude Code synker OAuth-state. I min brug er det ikke mærkbart, men det er ikke nul.
Alternativer, jeg overvejede
direnv — sætter CLAUDE_CONFIG_DIR automatisk baseret på en .envrc i roden af hvert projekt. Nul interaktion, nul klik. Minus: du skal lægge en .envrc i hvert work-root, og hvis du kører claude i en ukendt mappe — får du default-profilen (som måske ikke er den, du vil have). For den, der lever i et begrænset antal work-roots og vil slippe for at klikke — er direnv reelt bedre.
Symlink-baseret skift (én aktiv profil ved at pege ~/.claude-symlinket om) overvejede jeg også og forkastede med det samme. Du kan ikke have to terminaler med forskellige profiler åbne på samme tid — det globale "aktuelle" er ét. For mig er det en deal-breaker.
Konklusion
v2 er ikke bare bedre UX oven på v1. Det er en anerkendelse af, at Claude Code har en skjult arkitektonisk særegenhed (~/.claude.json som separat fil i home-roden, skrevet af --scope user-kommandoer uafhængigt af CLAUDE_CONFIG_DIR), som man skal tage højde for, hvis man vil have ægte isolation mellem profiler. Den første tilgang (~/.claude + ~/.claude-promova + alias) virkede til 80 %, men de resterende 20 % viste sig som stille state-drift mellem profilerne. Nu er der taget højde for det. Hvis du lige er begyndt — start direkte på v2. Hvis du allerede sidder på v1 — migrationsscriptet er ovenover, flytningen tager fem minutter og bryder ingenting (det er netop jq-mergen, der er det trin, som redder dig fra at miste state).