Your entire Claude Code identity is a file about 500 bytes long. Open ~/.claude/.credentials.json and that’s it: an OAuth token, nothing else. Every other thing in ~/.claude, your skills, your agents, your hooks, your plugins, your CLAUDE.md, years of transcripts, has nothing to do with who you are. It’s tooling. It would work identically no matter whose name is on the account.
Once that separation clicks, running several Claude Code accounts on one machine stops looking like an account-management headache and starts looking like a symlink problem. Same garage, same tools hanging on the wall, different license in your wallet depending on which car you’re driving that day. You don’t need three garages.
Why Bother Running More Than One
The obvious answer, “avoid rate limits,” is a bad reason to build this and not the reason I built it. Here’s the actual order of usefulness.
Keeping Work and Personal Genuinely Separate
If you use Claude Code for a day job and also for your own projects, those are two different orgs with two different billing arrangements, and mixing them under one login is how you end up debugging which org’s usage a session burned against. One machine, one terminal app, two accounts that never touch each other’s project registry or trust state.
A Blast Radius for Config Experiments
Want to try a new hook, a plugin you haven’t vetted, or a settings.json change that might break something subtle? Do it on an alternate profile first. If it wrecks something, you wrecked a throwaway profile’s state, not the config you rely on every day. This is the same instinct as testing a Docker Compose change in a scratch project before you touch the one running your actual services.
Seeing What a Member Actually Sees
If you administer a Claude org, you see it through owner eyes: every setting, every toggle, every seat. A plain member seat is a different product experience, and the only way to know what your teammates actually see when they open Claude Code is to log in as one. This caught me off guard more than once.
A Little Headroom
If one account is throttled mid-task, having a second authenticated profile sitting right there means you keep working instead of watching a countdown timer. Useful, but the least interesting item on this list.
One honest note before the how-to: the setup below runs on real paid seats inside a single org, an owner account plus two member seats. This is capacity already paid for, not account farming.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/tree/main/productivity/claude-code-multi-account/
The Mechanism: CLAUDE_CONFIG_DIR
Claude Code reads its configuration from ~/.claude by default. Set the CLAUDE_CONFIG_DIR environment variable and it reads from wherever you point it instead. That’s the whole trick. No hidden flag, no vendor-specific multi-profile feature, just an environment variable Claude Code was already checking.
The naive version of this idea is to give each account its own completely separate ~/.claude directory. Don’t do that. You’d be duplicating every skill, every agent, every hook, every plugin, and re-authenticating your MCP servers three times over, for files that have nothing to do with your identity. The better version: point CLAUDE_CONFIG_DIR at a new directory per account, then symlink everything back to your real ~/.claude except the small set of files that are genuinely per-account.
What’s Shared and What’s Isolated
This is the part worth bookmarking. Get this table wrong and you either leak your work account’s project trust into your personal one, or you spend an afternoon re-configuring plugins that should have carried over for free.
| Path | Side | What it is |
|---|---|---|
settings.json | Shared (symlink) | Global settings: model defaults, permissions, feature flags |
settings.local.json | Shared (symlink) | Machine-local overrides layered on top of settings.json |
CLAUDE.md | Shared (symlink) | Your global instructions file, same across every profile |
history.jsonl | Shared (symlink) | Prompt history, append-only, all profiles write to it |
agents/ | Shared (symlink) | Custom subagent definitions |
commands/ | Shared (symlink) | Slash commands |
skills/ | Shared (symlink) | Skills, including this blog’s writing skills |
plugins/ | Shared (symlink) | Installed plugins and their manifests |
hooks/ | Shared (symlink) | Lifecycle hooks (PreToolUse, PostToolUse, etc.) |
scripts/, config/, bin/ | Shared (symlink) | Supporting scripts and tool configuration |
projects/ | Shared (symlink) | Session transcripts, keyed by working directory |
file-history/, agent-memory/, plans/, tasks/, sessions/ | Shared (symlink) | Working state Claude Code generates during sessions |
cache/, paste-cache/, backups/, downloads/ | Shared (symlink) | Housekeeping directories |
.credentials.json | Isolated | The OAuth token. About 500 bytes. This IS the account. |
.claude.json | Isolated | 40 to 50 KB per profile: project registry, folder-trust state, onboarding state, user-scoped MCP servers, account metadata. Lives at ~/.claude.json for the default profile, and inside the profile directory once CLAUDE_CONFIG_DIR is set |
security/ | Isolated | Per-profile security state |
session-env/ | Isolated | Per-session environment snapshots |
shell-snapshots/ | Isolated | Per-profile shell state captures |
policy-limits.json | Isolated | Org policy and usage limits for that account |
remote-settings.json | Isolated | Server-pushed settings scoped to that account |
That shared list covers the directories worth sharing that actually exist on my machine, not a theoretical maximum. Claude Code creates a few more (debug/, ide/, jobs/) that are per-machine noise and stay out of it. Only link what’s actually there. More on why in the gotchas below, but the short version is that a script which creates missing directories before linking will leave junk behind that Claude Code never reads.
The Aliases
Once the directories exist, the daily interface is two shell aliases:
alias claude-alt-1="CLAUDE_CONFIG_DIR=~/.claude-alt-1 claude"alias claude-alt-2="CLAUDE_CONFIG_DIR=~/.claude-alt-2 claude"Your normal claude command keeps using ~/.claude with no changes. Typing claude-alt-1 or claude-alt-2 launches the same binary against a different identity. That’s the entire user-facing surface of this setup. The example repo’s aliases.sh bundles both aliases together with a third helper, claude-whoami, that prints which account the current pane is actually signed in as. You’ll want that the first time you have three panes open and forget which is which.
The Setup Script
Getting this script right took two wrong turns. The first pass created the target directory, then symlinked into it before checking whether the source path even existed in ~/.claude, which left empty directories sitting around forever. The second pass tried to fix “leftover cruft” by wiping the target directory before relinking, which turns out to delete real per-account state, not leftover cruft (more on exactly how in the gotchas below). The script that’s actually in the examples repo does neither. It only touches an entry if it’s missing or already a symlink, refuses to overwrite a real file or directory it finds in the way and warns you instead, skips any shared name that doesn’t exist in your primary config, and repairs a symlink that’s gone stale (pointing at the wrong place, say after you moved ~/.claude once).
#!/usr/bin/env bashset -euo pipefail
SOURCE="$HOME/.claude"TARGETS=("$HOME/.claude-alt-1" "$HOME/.claude-alt-2")DRY_RUN=false[ "${1:-}" = "--dry-run" ] && DRY_RUN=true
SHARED_FILES=(settings.json settings.local.json CLAUDE.md history.jsonl)SHARED_DIRS=(agents commands skills plugins hooks scripts config projects file-history agent-memory plans tasks sessions cache paste-cache backups bin downloads)NEVER_SHARE=(.credentials.json .claude.json security session-env shell-snapshots policy-limits.json remote-settings.json)
# Abort if a never-share name ever leaks into the shared lists.for n in "${NEVER_SHARE[@]}"; do for s in "${SHARED_FILES[@]}" "${SHARED_DIRS[@]}"; do [ "$n" = "$s" ] && { echo "REFUSING: $n is in both lists" >&2; exit 1; } donedone
link_one() { local src="$SOURCE/$1" dst="$2/$1" [ -e "$src" ] || { echo "skip (not in primary): $1"; return 0; }
if [ -L "$dst" ]; then [ "$(readlink "$dst")" = "$src" ] && return 0 # already correct $DRY_RUN && { echo "would repair stale symlink: $dst"; return 0; } ln -sfn "$src" "$dst" echo "repaired: $dst" elif [ -e "$dst" ]; then echo "SKIPPING $dst: real file or directory already there" >&2 else $DRY_RUN && { echo "would link: $dst -> $src"; return 0; } ln -s "$src" "$dst" echo "linked: $dst" fi}
for target in "${TARGETS[@]}"; do $DRY_RUN && echo "would mkdir: $target" || mkdir -p "$target" for f in "${SHARED_FILES[@]}"; do link_one "$f" "$target"; done for d in "${SHARED_DIRS[@]}"; do link_one "$d" "$target"; donedoneThere’s no wipe step anywhere in it. This is tested, not theoretical: building three profiles with it left every piece of per-account state untouched, a real directory I’d deliberately placed where a symlink belonged got preserved and reported instead of clobbered, directories missing from the primary config stayed missing instead of getting invented, and running it a second time against an already-linked setup did nothing at all. Run it once with --dry-run first if you want to see the plan for every profile before it touches anything, then for real, then authenticate each profile the first time you launch its alias. That listing is the condensed version. The copy in the examples repo also refuses to run when a profile path resolves to the primary, and it does rm -f before ln -s rather than leaning on ln -sfn.
The Gotchas Nobody Warns You About
Plugins and skills carry over. MCP servers added with claude mcp add do not. Because settings.json and plugins/ are symlinked, all sixteen of my plugins and every skill behave identically across all three profiles, no extra setup. But claude mcp add writes into the root of .claude.json, which sits on the isolated side of the table. My primary profile has one MCP server registered that way. Both alternates start with zero, and I had to add it again on each.
Project-level .mcp.json approvals reset per profile. The trust decision you made the first time Claude Code asked to enable a repo’s .mcp.json lives in .claude.json, same as everything else per-account. A repo you already approved on your main login will prompt you again on an alternate, even though it’s the exact same repo, same file, same trust decision you already made once.
Shared transcripts, separate trust. projects/ is symlinked, so all three profiles can read the same session history for a given working directory. But the project registry, the part that tracks whether you’ve trusted that directory at all, is per-profile in .claude.json. Same memory, different permissions. It’s a strange feeling to open a transcript on an alternate account and see a session you had no memory of starting, until you remember it’s the same directory your primary account was already trusted in.
history.jsonl has one file and three writers. All three profiles append to the same symlinked history log. I’ve run all three at once, exited them, and come back, with nothing corrupted so far. That’s worth being honest about: this setup is days old with a handful of sessions on the two alternates, not a battle-tested claim. A shared append-only jsonl with three concurrent writers is the piece I’d watch first if something breaks down the line.
Never mkdir -p the source side of the link. My first draft looped over that shared-directory list and ran mkdir -p "$SOURCE/$folder" before linking, so that the link always had something to point at. That list is aspirational: it includes directories Claude Code only creates when you actually use the feature. The result was a primary config carrying empty downloads/ and config/ directories that nothing ever reads, and they stay there forever because nothing cleans them up either. The other half of the same mistake is linking anyway without the mkdir: ln -s will happily point at a path that doesn’t exist and hand you a dangling symlink that fails later, at a confusing moment. Check first, link what’s there, skip what isn’t, and say which ones you skipped.
A “delete everything that isn’t a dotfile” cleanup step looks safe and isn’t. The obvious way to make a link script re-runnable is a one-liner: find "$target" -mindepth 1 -maxdepth 1 ! -name '.*' -exec rm -rf {} +, wipe anything that doesn’t start with a dot, then relink from scratch. It reads like exactly the right amount of caution, right up until you actually check the isolated column in the table above. security/, session-env/, shell-snapshots/, policy-limits.json, and remote-settings.json are all sitting there as plain, non-hidden entries in each profile directory. The dotfile heuristic protects .credentials.json and .claude.json beautifully and then quietly deletes the rest of your per-account state on every single run. The fix is to skip the wipe entirely: relink in place, replace only symlinks that are missing or pointing somewhere stale, and leave anything that isn’t already a symlink strictly alone. That’s what the script above does, and it’s the reason it has no rm in it at all.
Authenticate with the email code, not the Google button. Browser SSO happily reuses whichever Google account your browser session already has open, and if that’s your usual one, congratulations, you just authenticated two profiles as the same person. The email verification code flow forces you to actually type in the address you want that profile tied to.
Aliases for the same inbox are free through Google Workspace. If you’re setting up several invites that all need to land somewhere you’ll actually check, Google Admin Console has a straightforward path: Directory > Users > the account > Alternate Email Addresses. It’s a way to get several org invites landing in one inbox, not a way around anyone’s invite process. Plus-addressing ([email protected]) looked like the obvious shortcut and was unreliable for accepting the invites in practice; the Alternate Email Addresses field worked every time.
The Daily Workflow Payoff
The point of all this shows up once you have three terminal panes open on the same repo. Pane one runs claude, pane two runs claude-alt-1, pane three runs claude-alt-2. All three see the same skills, the same hooks, the same CLAUDE.md, the same plugin roster, and if you’re working in a directory all three have visited, the same session history. The only thing that differs is which account is footing the bill for that pane’s usage and which billing dashboard it shows up on. Three identically-configured panes also means three identical prompts staring back at you with no obvious label, which is exactly what claude-whoami is for: run it in any pane and it tells you which account that pane is actually signed in as, instead of you guessing from which one has the most scrollback.
That’s the whole payoff: identical tooling, independent accounts, zero duplicated configuration to maintain. You edit one skill once and it’s live in all three panes on your next message.
Common Questions
Do MCP servers carry over to a second Claude Code account?
No. Plugins and skills carry over automatically because they live in shared, symlinked directories, but MCP servers added with claude mcp add are written into .claude.json, which is deliberately isolated per profile. Run claude mcp add again on each alternate profile to register the same server there.
Can you run multiple Claude Code profiles at the same time in the same folder?
Yes. Each profile is just the claude binary launched with a different CLAUDE_CONFIG_DIR, so nothing stops you from running claude, claude-alt-1, and claude-alt-2 in three panes pointed at the same repo simultaneously. They share the symlinked projects/ transcripts and the same append-only history.jsonl.
Do you need separate paid seats to run multiple Claude Code accounts?
Yes, each profile authenticates as its own account and needs its own valid seat or subscription. This setup does not create free capacity out of nothing. It reuses seats you’ve already paid for (an owner account plus member seats in one org) rather than duplicating your tooling for each one.
Does this break when Claude Code updates?
It shouldn’t. The approach rests on two stable behaviours: Claude Code reads its config from one directory, and CLAUDE_CONFIG_DIR redirects that read. An update could add a new per-account file. Check any new top-level file against .claude.json (per-account state) versus settings.json (shared config) before symlinking it blindly.
Does this work on macOS the same way as Linux?
No, and this is where the approach breaks. Claude Code stores credentials in ~/.claude/.credentials.json on Linux and Windows, and CLAUDE_CONFIG_DIR relocates that file. On macOS credentials live in the encrypted Keychain, which CLAUDE_CONFIG_DIR does not scope, so every profile signs in as the same account. The symlinked tooling still works; the identity isolation does not.