← Learnings

One brain, two wallets: two Claude Code accounts on one machine

The usage limit lands mid-week, and a second subscription sits unused on another email. Switching with /logout swaps everything else along with the bill: skills, servers, hooks, every session you've run. It doesn't have to: two aliases, a few symlinks, two short scripts, and nothing you already have gets touched.

Hand the whole thing to Claude Code instead
PROMPTCLAUDE CODE
Set up a second Claude Code account on this machine, sharing everything except
billing and history. Work in order, and stop at any step whose preflight does
not match what you expect.

GOAL
  ~/.claude keeps working exactly as it does today. A new ~/.claude-personal
  has its own login and its own transcripts, and reads the same skills,
  settings, hooks, plugins and MCP servers through symlinks.

NEVER
  - overwrite or move whatever `command -v claude` points at
  - copy, merge or symlink oauthAccount between the two .claude.json files
  - touch ~/.claude/projects, history.jsonl, or any transcript
  - run `claude` or `claude-personal` yourself — I do the logins
  - edit ~/.zshrc or PATH without showing me the diff and waiting

0  PREFLIGHT — report, change nothing
     command -v claude
     ls -la ~/.claude
     jq '.mcpServers | keys' ~/.claude.json
   Tell me the real binary path and how many MCP servers are defined.
   Stop if the binary path is inside a directory you were going to
   install the script into.

1  mkdir -p ~/.claude-personal

2  Append to ~/.zshrc, diff first:
     alias claude-personal='CLAUDE_CONFIG_DIR=~/.claude-personal claude'

3  STOP. Tell me to run `source ~/.zshrc` then `claude-personal`, and wait.
   That first run writes ~/.claude-personal/.claude.json with its own login.
   Do not continue until I confirm it finished.

4  For each of skills, CLAUDE.md, settings.json, settings.local.json, plugins:
   if ~/.claude/NAME exists and ~/.claude-personal/NAME does not,
     ln -s ~/.claude/NAME ~/.claude-personal/NAME
   Report anything you skipped and why.

5  Copy across the servers and nothing else. Back the file up first:
     jq --slurpfile mcp <(jq '.mcpServers' ~/.claude.json) \
        '.mcpServers = $mcp[0]' ~/.claude-personal/.claude.json \
        > /tmp/merged.json && mv /tmp/merged.json ~/.claude-personal/.claude.json
   Then confirm oauthAccount in that file is byte-identical to before.

6  Write the resume script into a folder the real binary is NOT in, set REAL
   to the step 0 path, chmod +x it, and put that folder ahead of REAL's folder
   in PATH. Diff ~/.zshrc first.

7  VERIFY without starting a session, and report all four:
     command -v claude                          -> the script
     REAL --version                             -> still runs
     ls -l ~/.claude-personal                   -> five symlinks + its own .claude.json
     jq -r '.oauthAccount.emailAddress' on both -> two different addresses

Anything already in place: say so and leave it alone rather than recreating it.

Give each account its own folder

My ~/.claude holds 28 skills, seven MCP servers, hooks on eight events, my memory files, and a transcript of every session I've ever run. The account's login sits right there with all of it, in ~/.claude.json — which is why logging out feels like it takes everything with it. It does.

WHAT ~/.claude ACTUALLY IS ~/.claude.json THE ACCOUNT LOGIN — AND YOUR MCP SERVERS ~/.claude/ skills/ 28 OF THEM CLAUDE.md settings.json settings.local.json plugins/ projects/ 37 FOLDERS OF TRANSCRIPTS memory/
FIG 1 · ONE FOLDER, AND THE LOGIN SITTING NEXT TO IT

So you don't swap the login inside one folder. You give the second account a folder of its own, and CLAUDE_CONFIG_DIR is how Claude Code decides which one to read. It defaults to ~/.claude, which is why the work account needs no alias at all — only the new one does.

~/.ZSHRCSHELL
# Claude Code second account
alias claude-personal='CLAUDE_CONFIG_DIR=~/.claude-personal claude'
FIG 2 · THE ONE ALIAS YOU ADD

One alias, for the account that isn't the default. Bare claude goes on reading ~/.claude exactly as it always has, so nothing you already type changes.

Make the folder with mkdir ~/.claude-personal, and the first run of claude-personal prompts an OAuth login and writes the token there. That is the second account, finished — and completely useless.

A SECOND FOLDER, ONE COMMAND OLD ~/.claude-personal/ .claude.json ITS OWN LOGIN — THE ONLY REAL DIFFERENCE nothing else. no skills, no settings, no plugins.
FIG 3 · THE SECOND FOLDER, JUST CREATED

An empty folder means no skills, no settings, no plugins. This is where the symlink does the work. A symlink is a file that points at another file — open it and you get whatever it points at. Make the personal folder's skills a symlink to ~/.claude/skills and there is still exactly one copy of your skills on disk — both accounts just walk to the same place to read it.

THE SYMLINKS — WHERE THE SHARING HAPPENS in ~/.claude-personal/ the real file, in ~/.claude/ skills/ skills/ CLAUDE.md CLAUDE.md settings.json settings.json settings.local.json settings.local.json plugins/ plugins/ A SYMLINK — OPEN IT AND YOU GET THE FILE IT POINTS AT. ONE COPY ON DISK, READ BY BOTH.
FIG 4 · WHAT A SYMLINK ACTUALLY DOES

Five of those and the second account has your whole setup.

zsh · 96 columns
$ cd ~/.claude-personal
$ ln -s ~/.claude/skills skills
$ ln -s ~/.claude/CLAUDE.md CLAUDE.md
$ ln -s ~/.claude/settings.json settings.json        # hooks, permissions, env
$ ln -s ~/.claude/settings.local.json settings.local.json
$ ln -s ~/.claude/plugins plugins
FIG 5 · MAKING THE FIVE SYMLINKS

A skill you add later shows up in both accounts on its own, and a hook you edit once takes effect in both. There is no second copy to keep in step.

Get your MCP servers into the personal account

Here's the surprise, and it turns up the first time you run claude-personal. Your MCP servers and your folder trust don't live in ~/.claude at all. They live in ~/.claude.json — a file sitting next to the folder, not inside it.

WHERE EACH COMMAND LOOKS FOR .claude.json ~/ .claude.json 7 SERVERS · 31 TRUSTED FOLDERS .claude/ THE WORK ACCOUNT — UNTOUCHED .claude-personal/ THE FOLDER YOU JUST MADE .claude.json WRITTEN BLANK ON FIRST RUN skills/ → SYMLINK, ALREADY DONE claude NO VARIABLE SET READS THE FILE IN ~/ claude-personal CLAUDE_CONFIG_DIR SET READS THE FILE INSIDE THAT FOLDER EXISTS, BUT EMPTY — THE MERGE BELOW IS WHAT PUTS YOUR SERVERS IN IT
FIG 6 · TWO PLACES ONE FILENAME CAN MEAN

So claude-personal looks for that file inside its own folder, finds nothing, writes itself a blank one, and starts up with no servers at all. A symlink won't fix it: that same file holds oauthAccount, which is the account identity, so pointing it at the real one would sign both accounts into the same login. Copy over only the mcpServers part and the identity stays put.

zsh · 96 columns
$ jq --slurpfile mcp <(jq '.mcpServers' ~/.claude.json) \
     '.mcpServers = $mcp[0]' ~/.claude-personal/.claude.json \
     > /tmp/merged.json && mv /tmp/merged.json ~/.claude-personal/.claude.json
FIG 7 · COPYING THE SERVERS, NOT THE LOGIN

Make --resume find the right account

Each account keeps its own transcripts, at <folder>/projects/<project>/<session-id>.jsonl. Resume a personal session under your work account and Claude Code says it doesn't exist, because from where it's looking that's true.

THE SAME PROJECT, IN TWO SEPARATE HISTORIES ~/.claude/projects/ -Users-you-code-api/ 4f2a9c3e-71bd….jsonl A WORK SESSION ~/.claude-personal/projects/ -Users-you-code-api/ 0b8e1d47-3ca9….jsonl A PERSONAL SESSION $ claude --resume 0b8e1d47-3ca9… looks for that id in ~/.claude/projects/ not there looks for it in ~/.claude-personal/projects/ found — sets CLAUDE_CONFIG_DIR, hands off THE FOLDER NAME IS THE SAME IN BOTH. THE SESSION ID ONLY EVER EXISTS IN ONE.
FIG 8 · THE SAME PROJECT, IN TWO SEPARATE HISTORIES

A session id only ever exists in one of those two folders, so the right account can be worked out from the id alone. Doing that before Claude Code starts is the whole job of a shim, and nothing ever calls it by name. You keep typing claude exactly as you always did — what changes is which file the shell finds under that name.

THE SAME COMMAND, BEFORE AND AFTER YOU ADD THE SCRIPT BEFORE $ claude --resume 0b8e1d47-3ca9… the shell walks PATH and takes the first claude it finds the real claude WHEREVER command -v claude POINTS TODAY Claude Code starts against ~/.claude. The session is not in there. SO IT TELLS YOU THE SESSION DOES NOT EXIST — WHICH, FROM ~/.claude, IS TRUE AFTER $ claude --resume 0b8e1d47-3ca9… THE SAME COMMAND the shell walks PATH and takes the first claude it finds ~/bin/claude NEW — THE SCRIPT YOU ADDED. EARLIER IN PATH. the real claude UNTOUCHED. REACHED ONLY BY exec. the script decides, before Claude Code exists at all 1 is CLAUDE_CONFIG_DIR already set? YES — YOU SAID WHICH. IT STOPS. 2 read the session id out of the arguments 3 is that id under ~/.claude-personal/projects/ ? no change nothing yes export CLAUDE_CONFIG_DIR=~/.claude-personal 4 exec the real binary, same arguments Claude Code starts against the folder it was handed. The session is there. exec REPLACED THE SCRIPT — NOTHING OF IT IS STILL RUNNING
FIG 9 · THE SAME COMMAND, BEFORE AND AFTER THE SCRIPT

Those four decisions are the whole script.

~/BIN/CLAUDEBASH
#!/bin/bash
# routes --resume to the account that owns the session
REAL=/opt/homebrew/bin/claude   # wherever `command -v claude` pointed before

if [[ -z "$CLAUDE_CONFIG_DIR" ]]; then
  id="" prev=""
  for arg in "$@"; do
    if [[ $prev == "--resume" || $prev == "-r" ]]; then id=$arg; break; fi
    if [[ $arg == --resume=* ]]; then id=${arg#--resume=}; break; fi
    prev=$arg
  done
  if [[ -n $id ]] && compgen -G "$HOME/.claude-personal/projects/*/$id.jsonl" >/dev/null; then
    export CLAUDE_CONFIG_DIR="$HOME/.claude-personal"
  fi
fi
exec "$REAL" "$@"
FIG 10 · THE SCRIPT THAT PICKS THE ACCOUNT

It only gets a look at the command because the shell finds it first, so it goes in a folder that sits ahead of the real binary in PATH. Read REAL off command -v claude before you install it — afterwards that command answers with the script.

Put the script somewhere the real binary is not. A Homebrew install answers /opt/homebrew/bin/claude; the native installer answers ~/.local/bin/claude, and writing the script over that one would replace the very binary it needs to exec.

zsh · 96 columns
$ command -v claude      # do this FIRST — whatever it prints is REAL
$ mkdir -p ~/bin         # any folder REAL is not already sitting in
$ $EDITOR ~/bin/claude   # paste the script, set REAL to that path
$ chmod +x ~/bin/claude
$ export PATH="$HOME/bin:$PATH"   # in ~/.zshrc, ahead of REAL's folder
FIG 11 · PUTTING THE SCRIPT IN FRONT OF THE REAL ONE

After that you type the id and let the script work out which folder it lives in.

zsh · 96 columns
$ claude --resume 0b8e1d47-3ca9…   # personal session — switches account for you
$ claude --resume 4f2a9c3e-71bd…   # work session — nothing to do, stays put
$ claude-personal --resume 0b8e1d47-3ca9…   # you said which — passes straight through
FIG 12 · THE SAME COMMAND, EITHER ACCOUNT

The aliases and the script are two separate controls, and they don't fight. A shell expands an alias before it ever searches PATH, so claude-personal becomes CLAUDE_CONFIG_DIR=~/.claude-personal claude — and that claude still lands on the script, which finds the variable already set and gets out of the way. Naming an account always wins. The script only decides when nobody did.

WHO PICKS THE FOLDER, AND WHEN YOU TYPE THE SHELL ENDS UP RUNNING claude ~/bin/claude the script decides, from the id claude-personal CLAUDE_CONFIG_DIR=~/.claude-personal ~/bin/claude ~/.claude-personal — you already said so a cron or launchd job ~/bin/claude the script decides, from the id THE SCRIPT CHOOSES YOU CHOSE — THE SCRIPT STEPS ASIDE AN ALIAS IS EXPANDED BEFORE PATH IS SEARCHED, SO EVERY ROW ENDS AT THE SAME FILE.
FIG 13 · WHO PICKS THE FOLDER, AND WHEN

So the id is the only thing you have to remember, from either account.

Move a conversation to the other account

You hit the usage limit halfway through a conversation — that's the case that started all of this. A session is a transcript on disk, so continuing it on the other account is a copy and a resume. This one goes next to the shim in ~/bin, made executable the same way.

~/BIN/CLAUDE-PICKUPBASH
#!/bin/bash
# claude-pickup <session-id> — continue a work session as personal
set -euo pipefail
id=${1:?usage: claude-pickup <session-id>}
src=$(ls ~/.claude/projects/*/"$id".jsonl 2>/dev/null | head -1)
[[ -n $src ]] || { echo "session $id not found" >&2; exit 1; }
proj=$(basename "$(dirname "$src")")
mkdir -p ~/.claude-personal/projects/"$proj"
cp -n "$src" ~/.claude-personal/projects/"$proj"/
CLAUDE_CONFIG_DIR=~/.claude-personal exec claude --resume "$id"
FIG 14 · MOVING ONE SESSION ACROSS

The work account keeps its copy as history, and the shim routes later resumes to the personal one.

A few things stay separate on purpose: the logins, the transcripts, folder trust, and any server with its own OAuth token, which needs /mcp run once in the new account. Everything else is shared, and that is the whole surface of it.

zsh · 96 columns
$ claude              # work (default, unchanged)
$ claude-personal     # second subscription, same brain
$ claude --resume X   # figures out the account itself
$ claude-pickup X     # mid-conversation wallet swap
FIG 15 · THE FOUR COMMANDS

Twenty minutes, once. Bare claude still opens the same account with the same skills, the same servers and the same history, so nothing about the way you already work changes. The only thing that changes is which subscription pays.