Skip to main content
Zurück zum Blog
claude-codeproductivityclidevtools

Claude Code Multi-Profile, v2: eine saubere XDG-Architektur

Nach einem Monat mit dem alten Alias-Ansatz habe ich mein Claude-Code-Multi-Profile-Setup auf eine XDG-konforme Struktur unter ~/.config/claude-profiles/ umgebaut. Der eigentliche Grund war keine Ordnungsliebe — ich habe entdeckt, dass ~/.claude.json eine separate Datei im Home-Root ist, in der die per --scope user hinzugefügten MCP-Server still vor sich hin lebten.

Veröffentlicht 13. Juni 20269 Min. Lesezeit

Vor einem Monat habe ich einen Beitrag darüber veröffentlicht, wie man zwei Claude-Code-Accounts parallel auf einem Gerät betreibt — privat und beruflich, über CLAUDE_CONFIG_DIR und einen Shell-Alias. Der Ansatz hat funktioniert. Alles hat das getan, was es tun sollte.

Aber nach einem Monat realem Einsatz bin ich auf ein paar substanzielle Probleme gestoßen, die jener Beitrag nicht abgedeckt hat. Das größte davon — eine versteckte Eigenheit von Claude Code, in die ich zufällig hineingelaufen bin, als ich den Gmail-MCP hinzugefügt habe und er fünf Minuten später aus meinem Profil "verschwunden" war. Heute habe ich alles auf eine neue Architektur umgebaut — XDG-konform, mit einem Symlink und einem interaktiven Profil-Picker über gum. Das ist v2 — eine Evolution aus realer Erfahrung, keine theoretische Verbesserung.

Was angefangen hat zu nerven

Vier Sachen. Die ersten drei drehen sich um Sauberkeit, Sichtbarkeit und Skalierung. Die vierte ist der echte Architektur-Gotcha, den ich nicht sofort gesehen habe. Ich gehe sie der Reihe nach durch, denn genau die vierte hat am Ende den Umbau erzwungen.

1. Zwei separate Ordner in $HOME — das ist Unordnung

~/.claude und ~/.claude-promova — zwei Dotfolder direkt nebeneinander im Root von $HOME. Die XDG Base Directory Specification sagt, dass Configs in ~/.config/ gehören. Dotfile-Ordner, die direkt in Home herumliegen, sind ein Antipattern, das $HOME mit der Zeit zur Rumpelkammer macht. Kosmetisch, klar, aber es hat mich jedes Mal genervt, wenn ich ls -la ~ gemacht habe.

2. Keine visuelle Bestätigung des aktiven Profils

Ich starte claude und weiß nicht, welches Profil aktiv ist — bis ich innerhalb der Session claude config list ausführe. Wenn ich vergessen habe, welches Terminal ich wo gestartet habe, muss ich nachschauen. Eine Kleinigkeit, aber bei parallelem Arbeiten mit personal + work in zwei Tabs summiert sich das.

3. Aliase skalieren nicht

Zwei Profile — claude und claude-promova — okay. Ein drittes dazu (ein Freelance-Kunde) — brauchst du einen dritten Alias. Ein viertes — einen vierten. Nach einem halben Jahr wüsste ich nicht mehr, welche Aliase ich überhaupt angelegt hatte.

4. Die versteckte Falle in ~/.claude.json

Und das ist der eigentliche Grund für den Umbau. Claude Code hat zwei verschiedene Orte für Konfiguration, und die Doku schreit das nicht heraus: ~/.claude/ — das Verzeichnis mit projects/, sessions/, hooks/, skills/. Und separat — ~/.claude.json, eine Datei direkt im $HOME-Root, in der oauthAccount, mcpServers, die projects-History, skillUsage und etwa 40 weitere Felder echten Live-State liegen.

Der Befehl claude mcp add --scope user schreibt genau in dieses ~/.claude.json im Home-Root, nicht in ~/.claude/.claude.json oder ins Profilverzeichnis. Das wusste ich nicht. Bis ich eines Tages reingelaufen bin.

Discovery: warum der Gmail-MCP "verschwunden" ist

Heute Morgen habe ich den Gmail-MCP in Claude Code eingerichtet. Standard-Setup: Google-Cloud-Projekt, OAuth-Credentials, claude mcp add gmail --scope user -- npx -y @gongrzhe/server-gmail-autoauth-mcp. Alles okay. Session neu gestartet — läuft, ich lese Mails, antworte auf Nachrichten. Eine Stunde später haben wir angefangen, die Aliase in eine Funktion mit gum-Profil-Picker zu refaktorieren, und danach alles auf XDG umgezogen. Ich habe mv ~/.claude → ~/.config/claude-profiles/personal gemacht, CC neu gestartet, im Menü personal gewählt. Und in der neuen Session /mcp geöffnet:

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

Kein gmail. Kein vaultforge. Nur drei Server, von denen einer sogar failed war. Dabei hatte ich Gmail gerade hinzugefügt. Die Session in einem anderen Terminal (Work-Profil) zeigte Gmail und Vaultforge gleichzeitig ohne Probleme.

Ich habe angefangen zu graben und entdeckt, dass ich auf meinem Rechner drei verschiedene Dateien namens .claude.json habe:

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

Da war es. Genau das ist der Architektur-Gotcha:

  1. claude mcp add --scope user schreibt immer in ~/.claude.json im Home-Root, unabhängig von CLAUDE_CONFIG_DIR
  2. Claude Code liest bei gesetztem CLAUDE_CONFIG_DIR die Datei $CLAUDE_CONFIG_DIR/.claude.json — also die Datei innerhalb des Profils
  3. Die Einträge "user-scope MCPs" und "Profil-MCPs" liegen in verschiedenen Dateien mit gleichem Namen — und lassen sich leicht verwechseln

In meinem Fall war ~/.claude.json (Home-Root, 113 KB) der lebendige, aktuelle State — mit gmail, vaultforge, OAuth-Session, alles. Und ~/.config/claude-profiles/personal/.claude.json (29 KB) erwies sich als alter Snapshot, der irgendwie schon vorher im alten ~/.claude/ lag — vielleicht hat eine ältere CC-Version dort geschrieben, vielleicht ein Plugin. jq -r 'keys[]' auf beiden Dateien zeigte, dass die Home-Root-Version 41 unique Keys hatte, die im Snapshot fehlten.

Und diese 41 Keys sind kein Müll. Das ist der echte State von Claude Code:

  • skillUsage — Nutzungsstatistik der Skills
  • githubRepoPaths — Repo-Cache für Project-Navigation
  • cachedGrowthBookFeatures + cachedStatsigGates — Feature Flags (ohne sie holt CC bei jedem Start frische)
  • hasShownOpus45Notice, hasShownOpus46Notice, hasShownS1MWelcomeV2 — UI-Flags (ohne sie erscheinen die Modals beim nächsten Start wieder)
  • lastPlanModeUse, feedbackSurveyState, installMethod — Onboarding- und UX-State

Wenn du einfach mv ~/.claude ~/.config/claude-profiles/personal ohne Merge machst — verlierst du das alles. Welcome-Modals kommen zurück, githubRepoPaths-Suche läuft erneut, alle Survey-Aufforderungen kommen wieder. So wie ich es fast gemacht hätte.

Die neue Architektur

Alles lebt unter einem Eltern-Verzeichnis in ~/.config/, wie XDG es will. Jedes Profil ist self-contained — hat seinen vollen State inklusive eigener .claude.json. ~/.claude bleibt als Symlink auf das Personal-Profil für Abwärtskompatibilität.

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

Keine .claude.json im $HOME-Root. Jedes Profil ist ein separates isoliertes Verzeichnis, in dem alles liegt: die Ordner projects/sessions und dieselbe Datei mit MCP-Servern und OAuth-Tokens. Eine Source of Truth pro Profil.

Alles, was den Pfad ~/.claude/ hardcodet — alte Skripte, Plugins, Claude-Code-IDE-Extensions, Statusline-Configs wie claude-powerline.json — funktioniert weiter ohne Änderungen. Der Symlink wird ins Personal-Profil aufgelöst. Wenn du versehentlich command claude (ohne Wrapper-Funktion) startest — landest du ebenfalls über den Default-Path-Lookup im Personal. Personal wird zum "quiet default", wie es vorher war, lebt aber jetzt physisch an der XDG-Location.

Interaktiver Picker beim Start — Funktion + gum

Statt Aliase — eine claude()-Funktion in ~/.zshrc, die ein Pfeil-Menü über gum (TUI-Helper von Charm) zeigt. Die Funktion fängt den claude-Aufruf auf Shell-Ebene ab, lässt dich ein Profil wählen und startet command claude mit dem passenden CLAUDE_CONFIG_DIR. command ist wichtig — es umgeht die Wrapper-Funktion und ruft das echte Binary auf.

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

So sieht es beim Start aus:

Claude profile:
▸ personal
  promova

↑↓ zum Navigieren, Enter zum Auswählen, Esc zum Abbrechen (Claude startet dann einfach nicht). Das Profil ist immer sichtbar — vergessen unmöglich.

Auf der grünen Wiese (kein v1 zu migrieren)

Wenn du den alten Zwei-Ordner-plus-Alias-Hack schon laufen hast — spring runter zum Migrationsskript weiter unten. Dieser Abschnitt ist der saubere Aufbau von null — eine frische Maschine, oder einfach das Default-~/.claude aus normalem Gebrauch. Entscheide zuerst die Form: ein Account mit mehreren Profilen (um Projekte und ihre MCP-Sets zu isolieren — der häufige Fall), oder wirklich unterschiedliche Accounts pro Profil (privat vs. beruflich).

1. Die Verzeichnisse anlegen

Terminal — one-time setup
# 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

Ein Verzeichnis pro Profil unter einem einzigen XDG-Eltern-Verzeichnis. Wenn du Claude Code schon genutzt hast, zieht das Snippet dein bestehendes ~/.claude als personal mit rein und nimmt den Live-State aus ~/.claude.json im Home-Root gleich mit; auf einer wirklich frischen Maschine machen diese Zeilen einfach nichts. Alles Übrige sind leere Verzeichnisse, die sich beim ersten Start selbst füllen — und das chmod 600 ist wichtig, denn in diesen Dateien landen am Ende OAuth-Tokens.

2. Jedes Profil authentifizieren

Terminal
# 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.

Starte jedes Profil einmal mit seinem eigenen CLAUDE_CONFIG_DIR und führe /login aus. Das entscheidende Detail: Wenn jedes Profil denselben Account nutzt, loggst du dich nur einmal ein — macOS hält die Credential in einem einzigen gemeinsamen Keychain-Eintrag, also ist jedes weitere Profil automatisch authentifiziert.

Unterschiedliche Accounts pro Profil ist der kniffligere Weg: dieser gemeinsame Keychain-Eintrag hält einen Login, also kann das Einloggen eines zweiten Accounts in ein anderes Profil den ersten überschreiben. Wenn das dein Fall ist — folge dem dedizierten Walkthrough in zwei Claude-Accounts auf einer Maschine betreiben, und prüf die Credential-Isolation auf deiner Claude-Code-Version, bevor du dich darauf verlässt.

Terminal + ~/.zshrc
# 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 launch

Zeig mit ~/.claude auf das Personal-Profil, damit alles, was diesen Pfad hardcodet, weiter funktioniert, und kipp dann den claude()-gum-Picker aus dem Architektur-Abschnitt oben in deine ~/.zshrc. Nach einem Reload zeigt claude bei jedem Start das Profil-Menü — derselbe Endzustand wie beim migrierten Setup, nur frisch aufgebaut.

4. Das MCP jedes Profils kuratieren

Gib zuletzt jedem Profil nur die Tools, die es sehen soll: setz disableClaudeAiConnectors: true in der settings.json dieses Profils und füg mit claude mcp add wieder hinzu, was es braucht — genau wie im Connectors-Abschnitt oben. Das ist es, was aus zehn austauschbaren Profilen zehn zweckgebaute macht.

Migrationsskript

Für alle, die das lesen und vom alten Schema umziehen wollen. Der kritischste Schritt ist der zweite: er merged ~/.claude.json aus dem Home-Root mit dem, was bereits im Personal-Profil liegt, und vereint die mcpServers-Listen. Ohne diesen Schritt verliert das Profil sowohl die MCPs als auch den gesamten Live-State.

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

Der dritte Schritt zu den Rechten ist für sich genommen wichtig. jq | mv erzeugt eine Datei mit umask 644 (world-readable). Darin stecken OAuth-Tokens. chmod 600 sofort nach dem Merge ist Pflicht.

Nach der Migration — alle aktiven Claude-Sessions schließen und neu öffnen, Shell neu laden (source ~/.zshrc oder neues Terminal), claude starten, Profil wählen, über claude mcp list prüfen, dass alle MCPs da sind. Wenn alles passt — ~/.claude.json.migrated.bak löschen. Wenn etwas nicht stimmt — Rollback ist trivial: mv ~/.claude.json.migrated.bak ~/.claude.json und den Symlink entfernen.

Bonus: MCP-Connectors pro Profil (wenn deine Org sie teilt)

Die Architektur oben schaltet eine zweite Isolations-Achse frei — und die ist zu dem geworden, was ich tatsächlich am meisten nutze. Wenn dein Claude-Account zu einer Organisation gehört, kann die Org ein gemeinsames Set an remote MCP-Connectors ausrollen (Figma, ein Ticket-Tracker, ein Metrics-Backend, ein Database-Gateway, ein CMS…). Claude Code zieht jeden Connector, den die Org aktiviert hat, automatisch in jede Session — weil sie am Account hängen, nicht an dem Verzeichnis, aus dem du gestartet hast.

Bei zwei oder drei Connectors ist das okay. Bei zehn Projekten und einem Dutzend Org-Connectors hört es auf, okay zu sein: jede Session lädt Tools, die sie nichts angehen. Das ist Rauschen in der Tool-Liste des Modells, das ist verschwendeter Kontext, und — der Teil, der wirklich beißt — es lässt eine Session in ein System hineingreifen, das zu einem anderen Projekt gehört. Der Connector für Projekt A sollte gar nicht erst existieren, während du an Projekt B arbeitest.

Den Auto-Fetch abschalten, dann kuratieren

Der Hebel ist eine einzige Einstellung, disableClaudeAiConnectors (Claude Code v2.1.182+). Sie lebt in jedem Settings-Scope — auch in der eigenen settings.json eines Profils — also ist sie pro CLAUDE_CONFIG_DIR. Setz sie auf true und dieses Profil hört komplett auf, die Org-Connectors automatisch zu ziehen:

~/.config/claude-profiles/frontend/settings.json
{
  "disableClaudeAiConnectors": true
}

Dann fügst du nur die Connectors wieder hinzu, die dieses Profil tatsächlich braucht. Server, die du explizit hinzufügst, werden vom Flag nicht berührt, also laden sie normal — das Ergebnis ist eine kuratierte Teilmenge, Profil für Profil:

Terminal — inside the frontend profile
# 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
ProfilWas es lädt
frontendFigma, Linear, Sentry
backendPostgres, Grafana, Linear
contentNotion, Figma, Linear

Gleicher Account, gleicher Org-Katalog — aber jedes Profil sieht nur seinen Ausschnitt, plus die gemeinsamen Connectors, die du dich entscheidest zu wiederholen (ein Ticket-Tracker verdient sich seinen Platz meist überall).

Ich habe es getestet, bevor ich ihm vertraut habe

Ich wollte mich nicht auf das Wort der Doku verlassen, also habe ich an einem Profil ein sauberes A/B gefahren — gleicher Account, gleicher OAuth-Token, der einzige Unterschied war diese eine settings.json:

A/B on one profile
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)

Es tut genau das, was es verspricht. Zwei Vorbehalte, die du kennen solltest, bevor du dich darauf verlässt:

  • Einen OAuth-Connector wieder hinzuzufügen braucht ein interaktives Login. Connectors, die sich per Key-in-URL authentifizieren, kommen über claude mcp add vollautomatisch zurück. Connectors hinter OAuth fordern beim ersten Mal, wenn du sie in diesem Profil nutzt, ein einmaliges Browser-Auth an.
  • claude mcp list zeigt Org-Connectors gar nicht an — nur lokal definierte und explizit hinzugefügte Server. Die automatisch gezogenen sind nur innerhalb einer laufenden Session über /mcp sichtbar. Keine Panik, wenn mcp list leer davon aussieht.

Was du bekommst

  • Ein Eltern-Verzeichnis statt zweier Dotfolder in $HOME — XDG-konform
  • Der Symlink erhält die Kompatibilität mit allem, was ~/.claude/ hardcodet
  • Jedes Profil ist self-contained — sein voller State und seine MCPs liegen in seiner eigenen .claude.json
  • Eine Source of Truth pro Profil — keine Orphan-Config im Home-Root mehr, die still vom Profil-Pendant abdriftet
  • Sensitive Daten (oauthAccount, Tokens) haben garantiert Rechte 600
  • Visuelle Bestätigung des aktiven Profils bei jedem Start — du kannst nicht vergessen, welches Profil aktiv ist
  • Ein drittes Profil hinzufügen = eine Zeile im case der Funktion hinzufügen, kein neuer Alias zum Klonen und kein Name zum Merken

Wo es Schwächen hat

Ich will ehrlich sein. Das ist keine kostenlose Verbesserung — ein paar Kompromisse kamen mit dazu, und die solltest du vorher kennen.

  • gum ist eine zusätzliche Abhängigkeit (brew install gum, ~13 MB). Wenn du es prinzipiell nicht installieren willst — Fallback auf select in zsh oder ein simples read. Funktioniert, sieht aber nicht so gut aus und hat keine Pfeil-Navigation.
  • Ein Enter-Druck bei jedem Start. Für Leute, die claude dutzende Male am Tag starten — kann nerven. Alternative unten (direnv).
  • Der Symlink ~/.claude → personal macht personal zum Default. Wenn du das Work-Profil als Default brauchst, musst du den Symlink umbiegen (ln -sf). Nicht schwer, aber es ist nicht "vergessen und nichts bricht".
  • Der Symlink kann theoretisch brechen, wenn irgendein Tool ~/.claude.json atomar über ein Temp+Rename-Pattern (write-file-atomic) überschreibt. In der Praxis macht Claude Code das selbst nicht, aber wenn du Drittanbieter-Plugins installierst — prüf das.
  • Wenn du auf den Profilen unterschiedliche Anthropic-Accounts mit unterschiedlichen Plans hast — kann es nach einem Wechsel einen Sub-Sekunden-Lag geben, während Claude Code den OAuth-State synct. In meiner Nutzung nicht spürbar, aber nicht null.

Alternativen, die ich erwogen habe

direnv — setzt CLAUDE_CONFIG_DIR automatisch abhängig von einer .envrc im Root jedes Projekts. Null Interaktion, null Klicks. Minus: du musst in jedem Work-Root eine .envrc ablegen, und wenn du claude in einem nicht erkannten Ordner startest — bekommst du das Default-Profil (eventuell nicht das, das du willst). Für Leute, die in einer überschaubaren Anzahl von Work-Roots leben und nie klicken wollen — ist direnv tatsächlich besser.

Symlink-basiertes Switching (ein aktives Profil über das Umbiegen des ~/.claude-Symlinks) habe ich ebenfalls erwogen und sofort verworfen. Du kannst nicht zwei Terminals mit unterschiedlichen Profilen gleichzeitig haben — das globale "aktuelle" ist nur eines. Für mich ein Deal-Breaker.

Fazit

v2 ist nicht einfach besseres UX über v1. Es ist das Eingeständnis, dass Claude Code eine versteckte Architektur-Eigenheit hat (~/.claude.json als separate Datei im Home-Root, die von --scope user-Befehlen unabhängig von CLAUDE_CONFIG_DIR geschrieben wird), die man berücksichtigen muss, wenn man echte Isolation zwischen Profilen will. Der erste Ansatz (~/.claude + ~/.claude-promova + Alias) hat zu 80% funktioniert, aber die übrigen 20% äußerten sich in stillem State-Drift zwischen den Profilen. Jetzt ist das berücksichtigt. Wenn du gerade erst anfängst — starte direkt mit v2. Wenn du schon auf v1 sitzt — das Migrationsskript ist oben, der Umzug dauert fünf Minuten und bricht nichts (gerade der jq-Merge ist der Schritt, der dich vor State-Verlust rettet).