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