Claude Code Multi-Profile, v2: en ren XDG-arkitektur
Etter en måned med den gamle alias-baserte tilnærmingen bygde jeg om Claude Code multi-profile-oppsettet mitt til en XDG-kompatibel struktur under ~/.config/claude-profiles/. Den egentlige grunnen var ikke ryddighet — det var oppdagelsen av at ~/.claude.json er en separat fil i home-roten der MCP-servere lagt til via --scope user stille bodde.
For en måned siden publiserte jeg et innlegg om hvordan kjøre to Claude Code-kontoer parallelt på samme maskin — privat og jobb, via CLAUDE_CONFIG_DIR og et shell-alias. Tilnærmingen fungerte. Alt gjorde det det skulle gjøre.
Men etter en måned med reell bruk støtte jeg på et par solide problemer som det innlegget ikke dekket. Det største — en skjult særegenhet ved Claude Code som jeg ved et uhell tråkket midt oppi, da jeg la til Gmail-MCP-en og den "forsvant" fra profilen min fem minutter senere. I dag bygde jeg alt om til en ny arkitektur — XDG-kompatibel, med en symlink og en interaktiv profilvelger via gum. Dette er v2 — en evolusjon ut av reell erfaring, ikke en teoretisk forbedring.
Det som begynte å skurre
Fire ting. De tre første handler om ryddighet, synlighet og skala. Den fjerde er den ekte arkitektoniske fellen jeg ikke så med en gang. Jeg går gjennom dem i rekkefølge, fordi det er nettopp den fjerde som til slutt tvang frem ombyggingen.
1. To separate mapper i $HOME — det er rotete
~/.claude og ~/.claude-promova — to dotfolders side om side i roten av $HOME. XDG Base Directory Specification sier at configs hører hjemme i ~/.config/. Dotfile-mapper strødd rett i home er et antimønster som over tid gjør $HOME til en rotekrok. Kosmetisk, ja, men det irriterte hver gang jeg så ls -la ~.
2. Ingen visuell bekreftelse på aktiv profil
Jeg starter claude og vet ikke hvilken profil som er aktiv — før jeg kjører claude config list inne i sesjonen. Hvis jeg glemte hvilken terminal jeg startet hvor, må jeg sjekke. En bagatell, men med personal + work i parallell i to faner samler det seg opp.
3. Alias skalerer ikke
To profiler — claude og claude-promova — det er greit. Jeg legger til en tredje (en frilanskunde) — da trengs et tredje alias. En fjerde — et fjerde. Etter et halvt år ville jeg ikke huske hvilke aliaser jeg faktisk hadde opprettet.
4. Den skjulte fellen i ~/.claude.json
Og dette er den egentlige grunnen til ombyggingen. Claude Code har to forskjellige steder for konfigurasjon, og dokumentasjonen roper det ikke ut: ~/.claude/ — katalogen der projects/, sessions/, hooks/, skills/ ligger. Og separat — ~/.claude.json, en fil rett i $HOME-roten, der oauthAccount, mcpServers, projects-historikken, skillUsage og rundt 40 andre felt av ekte live state bor.
Kommandoen claude mcp add --scope user skriver akkurat til ~/.claude.json i home-roten, og ikke til ~/.claude/.claude.json eller til profilens katalog. Det visste jeg ikke. Helt til jeg en dag tråkket rett i det.
Discovery: hvorfor Gmail-MCP-en "forsvant"
I dag tidlig satt jeg opp Gmail-MCP-en i Claude Code. Standard oppsett: Google Cloud-prosjekt, OAuth-credentials, claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp. Alt OK. Startet sesjonen på nytt — virker, jeg leser mail, svarer på meldinger. En time senere begynte vi å refaktorere aliasene til en funksjon med gum-profilvelger, og deretter flyttet vi alt over til XDG. Jeg kjørte mv ~/.claude → ~/.config/claude-profiles/personal, startet CC på nytt, valgte personal i menyen. Og i den nye sesjonen åpnet jeg /mcp:
figma (failed)
playwright-test
claude.ai NotionIngen gmail. Ingen vaultforge. Bare tre servere, hvorav én til og med var failed. Og jeg hadde nettopp lagt til Gmail. Samtidig viste sesjonen i en annen terminal (work-profilen) Gmail og Vaultforge uten noe problem.
Jeg begynte å grave og fant at jeg på maskinen hadde tre ulike 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 hadde vi det. Det er nettopp det som er den arkitektoniske fellen:
claude mcp add --scope userskriver alltid til~/.claude.jsoni home-roten, uavhengig avCLAUDE_CONFIG_DIR- Når
CLAUDE_CONFIG_DIRer satt, leser Claude Code$CLAUDE_CONFIG_DIR/.claude.json— altså filen inne i profilen - Oppføringene for "user-scope MCPs" og "profilens MCPs" bor i forskjellige filer med samme navn — og det er lett å blande dem
I mitt tilfelle var ~/.claude.json (home-roten, 113 KB) den levende, aktuelle tilstanden — med gmail, vaultforge, OAuth-sesjonen, alt. Og ~/.config/claude-profiles/personal/.claude.json (29 KB) viste seg å være en gammel snapshot som av en eller annen grunn allerede tidligere lå i gamle ~/.claude/ — kanskje skrev en eldre CC-versjon dit, kanskje et plugin. jq -r 'keys[]' på begge filene viste at home-root-versjonen hadde 41 unike nøkler som ikke fantes i snapshoten.
Og de 41 nøklene er ikke søppel. Det er Claude Codes virkelige tilstand:
skillUsage— bruksstatistikk for skillsgithubRepoPaths— repo-cache for prosjektnavigeringcachedGrowthBookFeatures+cachedStatsigGates— feature flags (uten dem henter CC ferske ved hver oppstart)hasShownOpus45Notice,hasShownOpus46Notice,hasShownS1MWelcomeV2— UI-flagg (uten dem dukker modalene opp igjen ved neste oppstart)lastPlanModeUse,feedbackSurveyState,installMethod— onboarding- og UX-state
Hvis du bare kjører mv ~/.claude ~/.config/claude-profiles/personal uten merge — mister du alt dette. Du ser welcome-modalene igjen, githubRepoPaths-søket kjører på nytt, alle survey-spørsmålene kommer tilbake. Som jeg nesten gjorde.
Den nye arkitekturen
Alt bor under én felles foreldrekatalog i ~/.config/, slik XDG vil ha det. Hver profil er selvstendig — den har sin fulle state inkludert sin egen .claude.json. ~/.claude blir igjen som symlink til personal-profilen for bakoverkompatibilitet.
~/.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. Hver profil er en separat, isolert katalog der alt ligger: projects/sessions-mappene og den samme filen med MCP-servere og OAuth-tokens. Én source of truth per profil.
Hvorfor symlinken peker på personal
Alt som hardkoder stien ~/.claude/ — gamle skript, plugins, IDE-utvidelser for Claude Code, statusline-configs som claude-powerline.json — fortsetter å virke uten endringer. Symlinken løses opp til personal-profilen. Hvis du ved et uhell kjører command claude (uten wrapper-funksjonen) — havner du også i personal via default-path lookup. Personal blir den "quiet default" den var før, men bor nå fysisk på XDG-plassen.
Interaktiv velger ved oppstart — funksjon + gum
I stedet for aliaser — en funksjon claude() i ~/.zshrc som viser en pilmeny via gum (Charm sin TUI-hjelper). Funksjonen fanger opp claude-kallet på shell-nivå, lar deg velge profil og kjører command claude med tilhørende CLAUDE_CONFIG_DIR. command er viktig — det går utenom wrapper-funksjonen og kaller den ekte 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
}Slik ser det ut ved oppstart:
Claude profile:
▸ personal
promova↑↓ for å navigere, Enter for å velge, Esc for å avbryte (Claude starter rett og slett ikke). Profilen er alltid synlig — umulig å gå glipp av.
Migreringsskript
For den som leser dette og vil flytte fra det gamle oppsettet. Det mest kritiske steget er det andre: det merger ~/.claude.json fra home-roten med det som allerede ligger i personal-profilen, og kombinerer mcpServers-listene. Uten det steget mister profilen både MCP-ene sine og hele live-staten.
# 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 rettigheter er viktig i seg selv. jq | mv oppretter en fil med umask 644 (world-readable). Inni er det OAuth-tokens. chmod 600 umiddelbart etter mergen er obligatorisk.
Etter migreringen — lukk og åpne alle aktive Claude-sesjoner, last shellet på nytt (source ~/.zshrc eller ny terminal), kjør claude, velg profil, sjekk via claude mcp list at alle MCP-er er på plass. Hvis alt er greit — slett ~/.claude.json.migrated.bak. Hvis noe er galt — rollback er trivielt: mv ~/.claude.json.migrated.bak ~/.claude.json og fjern symlinken.
Det du får
- Én felles foreldrekatalog i stedet for to dotfolders i $HOME — XDG-kompatibelt
- Symlinken bevarer kompatibilitet med alt som hardkoder
~/.claude/ - Hver profil er selvstendig — sin fulle state og sine MCP-er i sin egen
.claude.json - Én source of truth per profil — slutt på foreldreløs config i home-roten som stille driver fra profilens
- Sensitiv data (oauthAccount, tokens) garantert med rettigheter 600
- Visuell bekreftelse på aktiv profil ved hver oppstart — umulig å glemme hvilken profil som er aktiv
- Å legge til en tredje profil = å legge til én linje i
casei funksjonen, ikke klone et nytt alias og huske navnet
Der det svikter
Jeg vil være ærlig. Dette er ikke en gratis forbedring — noen kompromisser følger med på kjøpet, og de er verdt å kjenne på forhånd.
gumer en ekstra avhengighet (brew install gum, ~13 MB). Hvis du av prinsipp ikke vil installere det — fallback tilselecti zsh eller en enkelread. Funker, men ser ikke like pent ut og mangler pilnavigering.- En Enter ved hver oppstart. For den som drar i gang
claudetitalls ganger om dagen — kan irritere. Alternativ under (direnv). - Symlinken
~/.claude → personalgjør personal til default. Hvis du må ha work-profilen som default — må du peke om symlinken (ln -sf). Ikke vanskelig, men det er ikke "glem det og ingenting ryker". - Symlinken kan teoretisk ryke hvis et eller annet verktøy atomisk overskriver
~/.claude.jsonvia et temp+rename-mønster (write-file-atomic). I praksis gjør Claude Code det ikke selv, men hvis du installerer tredjepartsplugins — sjekk. - Hvis du har ulike Anthropic-kontoer på profilene med ulike planer — etter bytte kan det være en sub-sekund lag mens Claude Code synker OAuth-state. I min bruk er det ikke merkbart, men det er ikke null.
Alternativer jeg vurderte
direnv — setter CLAUDE_CONFIG_DIR automatisk basert på en .envrc i roten av hvert prosjekt. Null interaksjon, null klikk. Minus: du må legge en .envrc i hver work-root, og hvis du kjører claude i en ukjent mappe — får du default-profilen (kanskje ikke den du vil ha). For den som lever i et begrenset antall work-roots og vil slippe å klikke — er direnv reelt bedre.
Symlink-basert bytting (én aktiv profil ved å peke om ~/.claude-symlinken) vurderte jeg også og forkastet umiddelbart. Du kan ikke ha to terminaler med ulike profiler åpne samtidig — den globale "gjeldende" er én. For meg er det en deal-breaker.
Konklusjon
v2 er ikke bare bedre UX oppå v1. Det er en erkjennelse av at Claude Code har en skjult arkitektonisk særegenhet (~/.claude.json som en separat fil i home-roten, skrevet av --scope user-kommandoer uavhengig av CLAUDE_CONFIG_DIR) som man må ta hensyn til hvis man vil ha ekte isolasjon mellom profiler. Den første tilnærmingen (~/.claude + ~/.claude-promova + alias) virket til 80 %, men de gjenværende 20 % viste seg som stille state-drift mellom profilene. Nå er det tatt høyde for. Hvis du så vidt begynner — start rett på v2. Hvis du allerede sitter på v1 — migreringsskriptet er over, flyttingen tar fem minutter og bryter ingenting (det er nettopp jq-mergen som redder deg fra å miste state).