Skip to main content
← Tilbake til bloggen
claude-codeproductivityclidevtools

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.

Publisert 13. juni 20267 min lesing

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:

/mcp output (personal profile)
figma            (failed)
playwright-test
claude.ai Notion

Ingen 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:

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

Der hadde vi det. Det er nettopp det som er den arkitektoniske fellen:

  1. claude mcp add --scope user skriver alltid til ~/.claude.json i home-roten, uavhengig av CLAUDE_CONFIG_DIR
  2. Når CLAUDE_CONFIG_DIR er satt, leser Claude Code $CLAUDE_CONFIG_DIR/.claude.json — altså filen inne i profilen
  3. 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 skills
  • githubRepoPaths — repo-cache for prosjektnavigering
  • cachedGrowthBookFeatures + 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.

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.

~/.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
}

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.

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

Det 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 case i 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.

  • gum er en ekstra avhengighet (brew install gum, ~13 MB). Hvis du av prinsipp ikke vil installere det — fallback til select i zsh eller en enkel read. Funker, men ser ikke like pent ut og mangler pilnavigering.
  • En Enter ved hver oppstart. For den som drar i gang claude titalls ganger om dagen — kan irritere. Alternativ under (direnv).
  • Symlinken ~/.claude → personal gjø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.json via 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).