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