From b7ed0f84352afe9ca169b3d89df2cfd4be7c6a30 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 25 Mar 2026 16:00:43 -0500 Subject: [PATCH] =?UTF-8?q?docs:=20sync=20KB=20=E2=80=94=20claude-code-mul?= =?UTF-8?q?ti-account.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- workstation/claude-code-multi-account.md | 224 +++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 workstation/claude-code-multi-account.md diff --git a/workstation/claude-code-multi-account.md b/workstation/claude-code-multi-account.md new file mode 100644 index 0000000..1085d05 --- /dev/null +++ b/workstation/claude-code-multi-account.md @@ -0,0 +1,224 @@ +--- +title: "Claude Code Multi-Account Setup with CLAUDE_CONFIG_DIR" +description: "Guide to running multiple Claude Code OAuth accounts simultaneously using CLAUDE_CONFIG_DIR, direnv, and symlinked config directories." +type: guide +domain: workstation +tags: [claude-code, oauth, direnv, config, multi-account] +--- + +# Claude Code Multi-Account Setup + +Run two different Claude OAuth accounts simultaneously — one for specific projects, another for everything else — using the `CLAUDE_CONFIG_DIR` environment variable and direnv. + +## How CLAUDE_CONFIG_DIR Works + +`CLAUDE_CONFIG_DIR` is an undocumented but fully implemented environment variable in the Claude Code binary. It controls where Claude Code looks for its entire configuration directory. + +### Behavior When Set + +| Aspect | Default | With CLAUDE_CONFIG_DIR | +|--------|---------|----------------------| +| Config directory | `~/.claude` | `$CLAUDE_CONFIG_DIR` | +| Global state file | `~/.claude.json` | `$CLAUDE_CONFIG_DIR/.config.json` | +| OAuth credentials | `~/.claude/.credentials.json` | `$CLAUDE_CONFIG_DIR/.credentials.json` | +| Keychain entry name | `Claude Code` | `Claude Code-` (8-char hash of config path) | + +### Internal Implementation + +From the Claude Code binary source: + +**Config dir resolution:** +```js +process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude") +``` + +**Global state file (`.config.json` fallback):** +```js +// When CLAUDE_CONFIG_DIR is set, checks for .config.json first +if (existsSync(path.join(configDir, ".config.json"))) + return path.join(configDir, ".config.json"); +``` + +**Keychain collision avoidance:** +```js +// Adds hash suffix when CLAUDE_CONFIG_DIR is set +let suffix = !process.env.CLAUDE_CONFIG_DIR ? "" + : `-${hash(configDir).substring(0, 8)}`; +return `Claude Code${suffix}`; +``` + +**Propagated to subagents:** `CLAUDE_CONFIG_DIR` is in the explicit list of env vars passed to child processes (subagents, background agents), ensuring they use the same config directory. + +## Setup Procedure + +### Prerequisites + +- Claude Code installed (native binary at `~/.local/bin/claude`) +- direnv (`sudo dnf install direnv` on Fedora/Nobara) + +### 1. Create Alternate Config Directory + +```bash +mkdir -p ~/.claude-ac +``` + +The `-ac` suffix stands for "alternate config" — name it whatever you like. + +### 2. Symlink Shared Config Files + +Share configuration between accounts so both get the same CLAUDE.md, settings, MCP servers, hooks, etc.: + +```bash +# Core config files +ln -s ~/.claude/CLAUDE.md ~/.claude-ac/CLAUDE.md +ln -s ~/.claude/settings.json ~/.claude-ac/settings.json +ln -s ~/.claude/command-permissions.json ~/.claude-ac/command-permissions.json + +# Directories (agents, commands, hooks, skills, patterns, memory, plugins) +for dir in agents commands hooks skills patterns memory plugins; do + ln -s ~/.claude/$dir ~/.claude-ac/$dir +done + +# MCP servers + global state +# When CLAUDE_CONFIG_DIR is set, Claude reads .config.json instead of ~/.claude.json +ln -s ~/.claude.json ~/.claude-ac/.config.json +``` + +#### What NOT to Symlink + +These should remain independent per account: + +| Path | Why independent | +|------|----------------| +| `.credentials.json` | Different OAuth tokens per account | +| `projects/` | Session state tied to account | +| `sessions/` | Active session registry | +| `history.jsonl` | Conversation history | +| `todos/`, `tasks/`, `plans/` | Conversation-scoped data | +| `debug/`, `telemetry/`, `logs/` | Per-instance diagnostics | + +These directories are created automatically by Claude Code on first use — no need to pre-create them. + +### 3. Hook direnv Into Your Shell + +**Fish** (`~/.config/fish/config.fish`, inside `if status is-interactive`): +```fish +direnv hook fish | source +``` + +**Bash** (`~/.bashrc`): +```bash +eval "$(direnv hook bash)" +``` + +### 4. Create `.envrc` for the Target Directory + +For example, to use the alternate account in `~/work`: + +```bash +# ~/work/.envrc +export CLAUDE_CONFIG_DIR="$HOME/.claude-ac" +``` + +Allow it: +```bash +direnv allow ~/work/.envrc +``` + +direnv automatically sets/unsets the env var when you `cd` into or out of `~/work` (and all subdirectories). + +### 5. Log In With the Second Account + +From within the target directory (where direnv activates): + +```bash +cd ~/work +claude auth login +``` + +This stores OAuth tokens in `~/.claude-ac/.credentials.json`, completely separate from the primary account. + +### 6. Verify + +```bash +# In ~/work — should show alternate account +cd ~/work && claude auth status + +# Outside ~/work — should show primary account +cd ~ && claude auth status +``` + +Both accounts can run simultaneously in separate terminal windows. + +## Current Configuration on This Workstation + +| Location | Account | Purpose | +|----------|---------|---------| +| `~/.claude` | Primary (cal.corum@gmail.com) | All projects except ~/work | +| `~/.claude-ac` | Alternate | ~/work projects | +| `~/work/.envrc` | — | direnv trigger for CLAUDE_CONFIG_DIR | + +## How It All Fits Together + +``` +Terminal in ~/work/some-project/ + ↓ cd triggers direnv + ↓ CLAUDE_CONFIG_DIR=~/.claude-ac + ↓ claude starts + ├── Config dir: ~/.claude-ac/ + ├── Auth: ~/.claude-ac/.credentials.json (alt account) + ├── Settings: ~/.claude-ac/settings.json → symlink → ~/.claude/settings.json + ├── MCP servers: ~/.claude-ac/.config.json → symlink → ~/.claude.json + ├── Hooks: ~/.claude-ac/hooks/ → symlink → ~/.claude/hooks/ + └── Keychain: "Claude Code-a1b2c3d4" (hashed, no collision) + +Terminal in ~/other-project/ + ↓ no .envrc, CLAUDE_CONFIG_DIR unset + ↓ claude starts + ├── Config dir: ~/.claude/ (default) + ├── Auth: ~/.claude/.credentials.json (primary account) + └── Keychain: "Claude Code" (default) +``` + +## Troubleshooting + +### Auth shows the wrong account +Check that direnv loaded correctly: +```bash +echo $CLAUDE_CONFIG_DIR +# Should show ~/.claude-ac when in ~/work, empty otherwise +``` + +If empty, ensure: +1. direnv hook is in your shell config +2. You opened a new shell after adding the hook +3. `direnv allow ~/work/.envrc` was run + +### MCP servers not loading +Verify the `.config.json` symlink: +```bash +ls -la ~/.claude-ac/.config.json +# Should point to ~/.claude.json +``` + +### direnv doesn't activate in subdirectories +direnv walks up the directory tree, so `~/work/.envrc` covers all of `~/work/**`. If it doesn't activate: +```bash +direnv status # Shows which .envrc is loaded and why +``` + +### Need to re-login +```bash +cd ~/work # Ensure direnv sets the env var +claude auth logout +claude auth login +``` + +### Subagents use wrong account +`CLAUDE_CONFIG_DIR` is in Claude Code's propagated env var list, so subagents inherit it automatically. If a subagent somehow uses the wrong account, verify the parent process has `CLAUDE_CONFIG_DIR` set. + +## Caveats + +- **Undocumented feature**: `CLAUDE_CONFIG_DIR` does not appear in `claude --help` or official docs. However, it is deeply integrated into the binary (config resolution, keychain naming, subagent propagation), suggesting it's intentional infrastructure. +- **Version sensitivity**: When Claude Code updates add new shared config files to `~/.claude/`, the alternate config directory won't automatically get symlinks for them. Periodically check for new files that should be symlinked. +- **Session isolation**: Even with symlinked memory, session history and project state are per-config-dir. Each account maintains its own conversation history.