docs: sync KB — claude-code-multi-account.md
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 4s

This commit is contained in:
Cal Corum 2026-03-25 16:00:43 -05:00
parent 4ecf93a3e2
commit b7ed0f8435

View File

@ -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-<hash>` (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.