225 lines
7.3 KiB
Markdown
225 lines
7.3 KiB
Markdown
---
|
|
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.
|