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.
Fra bunnen av (ingen v1 å migrere)
Hvis du allerede har kjørt det gamle to-mapper-+-alias-hacket, hopp til migreringsskriptet under. Denne seksjonen er den rene byggingen fra null — en fersk maskin, eller bare default-~/.claude fra vanlig bruk. Bestem formen først: én konto med flere profiler (for å isolere prosjekter og MCP-settene deres — det vanlige tilfellet), eller genuint forskjellige kontoer per profil (privat vs jobb).
1. Legg ut katalogene
# 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/nullÉn katalog per profil under én felles XDG-forelder. Hvis du allerede har brukt Claude Code, folder snippeten din eksisterende ~/.claude inn som personal og tar med seg den live tilstanden i home-roten ~/.claude.json; på en helt fersk maskin gjør de linjene rett og slett ingenting. Alt det andre er tomme mapper som fyller seg selv ved første oppstart — og chmod 600 betyr noe, for disse filene ender opp med å holde OAuth-tokens.
2. Autentiser hver 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.Start hver profil én gang med sin egen CLAUDE_CONFIG_DIR og kjør /login. Den viktige detaljen: hvis hver profil bruker samme konto, logger du inn bare én gang — macOS holder kredentialen i én enkelt delt Keychain-oppføring, så hver eneste andre profil blir autentisert automatisk.
Forskjellige kontoer per profil er den vanskeligere veien: den delte Keychain-oppføringen holder ett enkelt logg-inn, så det å logge inn en annen konto i en annen profil kan overskrive den første. Hvis det er ditt tilfelle, følg den dedikerte gjennomgangen i å kjøre to Claude-kontoer på én maskin, og verifiser kredential-isolasjonen på din Claude Code-versjon før du lener deg på den.
3. Symlink og velgeren
# 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 launchPek ~/.claude mot personal-profilen så alt som hardkoder den stien fortsetter å virke, og slipp så claude()-gum-velgeren fra arkitekturseksjonen over inn i ~/.zshrc. Etter en reload viser claude profilmenyen ved hver oppstart — den samme slutt-tilstanden som det migrerte oppsettet, bare bygget på nytt.
4. Kuratér hver profils MCP
Til slutt, gi hver profil bare verktøyene den skal se: sett disableClaudeAiConnectors: true i den profilens settings.json og legg tilbake det den trenger med claude mcp add, akkurat som i connector-seksjonen over. Det er det som gjør ti utbyttbare profiler om til ti formålsbygde.
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.
Bonus: MCP-connectorer per profil (når organisasjonen din deler dem)
Arkitekturen over låser opp en ny isolasjonsakse — og det ble faktisk det jeg bruker aller mest. Hvis Claude-kontoen din tilhører en organisasjon, kan organisasjonen dele ut et felles sett med remote MCP-connectorer (Figma, en ticket-tracker, et metrikk-backend, en database-gateway, et CMS…). Claude Code henter automatisk hver eneste connector organisasjonen har aktivert, inn i hver eneste sesjon, fordi de er knyttet til kontoen, ikke til katalogen du startet fra.
Med to eller tre connectorer går det greit. Med ti prosjekter og et dusin org-connectorer slutter det å gå greit: hver sesjon laster verktøy den ikke har noe med å røre. Det er støy i modellens verktøyliste, det er bortkastet kontekst, og — den delen som faktisk svir — det lar en sesjon nå inn i et system som hører til et annet prosjekt. Connectoren for prosjekt A burde ikke engang eksistere mens du jobber på prosjekt B.
Skru av auto-hentingen, så kuratér
Spaken er én enkelt innstilling, disableClaudeAiConnectors (Claude Code v2.1.182+). Den bor i hvilken som helst settings-scope — inkludert en profils egen settings.json — så den er per-CLAUDE_CONFIG_DIR. Sett den til true, og den profilen slutter helt å auto-hente org-connectorene:
{
"disableClaudeAiConnectors": true
}Legg så tilbake kun de connectorene den profilen faktisk trenger. Servere du legger til eksplisitt, blir ikke berørt av flagget, så de lastes som normalt — resultatet er et kuratert delsett, profil for 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 | Hva den laster |
|---|---|
frontend | Figma, Linear, Sentry |
backend | Postgres, Grafana, Linear |
content | Notion, Figma, Linear |
Samme konto, samme org-katalog — men hver profil ser bare sin skive, pluss de felles connectorene du velger å gjenta (en ticket-tracker fortjener som regel plassen sin overalt).
Jeg testet det før jeg stolte på det
Jeg ville ikke ta dokumentasjonen på ordet, så jeg kjørte en ren A/B på én profil — samme konto, samme OAuth-token, der den eneste forskjellen var den ene 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)Den gjør akkurat det den lover. To forbehold verdt å kjenne før du lener deg på den:
- Å legge til en OAuth-connector på nytt krever én interaktiv innlogging. Connectorer som autentiserer med nøkkel-i-URL kommer tilbake helt automatisk via
claude mcp add. Connectorer bak OAuth ber om en engangs browser-auth første gang du bruker dem i den profilen. claude mcp listviser ikke org-connectorer i det hele tatt — bare lokalt definerte og eksplisitt tillagte servere. De auto-hentede er bare synlige inne i en levende sesjon via/mcp. Ikke få panikk når de ikke dukker opp imcp list.
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).