Switch between Claude Code accounts on macOS β for real.
claude-switch lets you keep multiple Claude Code (Anthropic) accounts side by
side and switch between them with one command. Unlike directory-only profile
switchers, it also swaps the OAuth token stored in macOS Keychain, so each
profile is genuinely a different logged-in account β no manual re-login.
$ claude-switch list
π Claude Profiles:
βοΈ legacy π€ alice@example.com
- work π€ bob@example.com
- personal [login required]
I started with a small zsh function that swapped ~/.claude/ between profile
directories via a symlink. It looked like it worked β until I noticed that
every profile was still logged in as the same account.
Why? Claude Code on macOS doesn't store its OAuth token inside ~/.claude/.
It stores it in macOS Keychain under the service name
Claude Code-credentials. The directory swap was rotating settings, history,
custom skills/agents, and per-project state β but the active login was
system-wide, shared across every profile.
So a "real" account switcher has to swap two things at once: the directory
and the Keychain entry. That's what claude-switch does.
(Along the way, the original zsh function turned out to have a latent bug too:
[ "$x" == "$y" ] is fine in bash, but zsh's EQUALS option tries to resolve
the literal == as a path lookup for a command named =, producing the
mysterious claude-switch:40: = not found. Fixed by moving to [[ ... ]].
That fix is what made me look closer at what the function was really doing.)
For each profile <name>:
| What | Where |
|---|---|
| Config / history / skills | ~/.claude-profiles/<name>/ |
| OAuth token (backup slot) | macOS Keychain: Claude Code-credentials-<name> |
| Account cache (oauthAccount) | ~/.claude-profiles/<name>/.account.json |
Full ~/.claude.json backup |
~/.claude-profiles/<name>/.claude-root.json |
On claude-switch use <name>:
- Snapshot the current profile's state:
- active Keychain token β
Claude Code-credentials-<current> oauthAccountfrom~/.claude.jsonβ<current>/.account.json- whole
~/.claude.jsonβ<current>/.claude-root.json
- active Keychain token β
- Restore the target profile's token from
Claude Code-credentials-<name>into the active Keychain slot (or clear it if the target has no saved token β Claude Code will prompt to log in). - Restore the target profile's
~/.claude.jsonsnapshot back to~/.claude.json(or remove the file entirely if the target has no snapshot β Claude Code recreates it on next launch). - Repoint the
~/.claudesymlink to~/.claude-profiles/<name>/.
Why step 3 matters: Claude Code reads ~/.claude.json on launch for
oauthAccount, /status, /usage, and other account-scoped state. If
only the Keychain token is swapped, /status and /usage still show the
previous account's cached identity until the next API refresh. Swapping
the whole file keeps every account-scoped surface consistent.
When you launch Claude Code next, it reads the new token from Keychain,
the new ~/.claude.json cache, and the new config from ~/.claude/ β
a different account, no login needed.
- macOS (uses the
securityCLI and the system Keychain) jqβ only used to display emails inlist/status. Everything else still works without it. (Pre-installed on most setups; otherwisebrew install jq.)bashβ the system/bin/bash(3.2) is fine. The script uses[[ ]]but no other bash 4+ features.
git clone <repo-url> ~/MySpaces/claude-switch
cd ~/MySpaces/claude-switch
./install.sh # installs to /usr/local/bin by default
# or specify a target:
./install.sh ~/.local/bininstall -m 0755 claude-switch /usr/local/bin/claude-switchNote: manual install skips the automatic profile setup. Run
./install.shonce to let it migrate~/.claude, then copy the binary wherever you need it.
claude-switch helpIf a previous version of claude-switch exists as a shell function in your
~/.zshrc (the inspiration for this project), remove that block β a function
defined in .zshrc shadows the binary in PATH.
claude-switch create <name> Create a new empty profile
claude-switch list List profiles + account info
claude-switch status Show the active profile + account info
claude-switch use <name> [--force] Switch to profile <name>
claude-switch rename <old> <new> Rename a profile
claude-switch delete <name> Delete a profile (directory + token)
claude-switch help Show usage
The installer handles this automatically. When you run ./install.sh, it:
- Moves
~/.claudeβ~/.claude-profiles/legacy - Creates the
~/.claudesymlink - Snapshots your active token into the
legacyprofile
Confirm everything looks right after installing:
claude-switch status# Quit Claude Code (the macOS app or the CLI) first
claude-switch create work
claude-switch use work
# β Active Keychain entry is cleared
# β ~/.claude points to the empty 'work' directory
# Launch Claude Code and log in with the second account
# When you switch away later, claude-switch will save 'work's token automaticallyclaude-switch use legacy
# 'work's token is saved, 'legacy's token is restored, ~/.claude swaps$ claude-switch status
βοΈ Profile legacy
π Token active
π€ Account alice@example.com
π’ Org Personal (admin)
$ claude-switch list
π Claude Profiles:
βοΈ legacy π€ alice@example.com
- work π€ bob@example.com~/.claude β symlink to one of ~/.claude-profiles/*
~/.claude.json Claude Code's top-level cache. Restored
from the active profile's snapshot on
every `claude-switch use`.
~/.claude-profiles/
βββ legacy/
β βββ .account.json oauthAccount snapshot β used by
β β `claude-switch list` / `status`
β βββ .claude-root.json full ~/.claude.json snapshot β restored
β β into ~/.claude.json on switch
β βββ settings.json
β βββ settings.local.json
β βββ history.jsonl
β βββ projects/ per-project session state
β βββ sessions/
β βββ skills/ commands/ agents/ plugins/
β βββ ... (whatever else Claude Code writes)
βββ work/
βββ personal/
In Keychain Access.app, search for "Claude":
Claude Code-credentials β active, read by Claude Code
Claude Code-credentials-legacy β backup slot for the 'legacy' profile
Claude Code-credentials-work
Claude Code-credentials-personal
When claude-switch writes to Keychain it uses
security add-generic-password -A, which means "any application may read
this item without prompting." The trade-off:
- β Claude Code reads its credential silently across switches; no recurring permission dialogs.
β οΈ Any process running as your user can read the OAuth token via thesecurityCLI. For credentials on your own machine this matches the reality that~/.claude.jsonand~/.claude/are already user-readable.
If you prefer per-binary ACLs, edit kc_save() in the script and replace
-A with explicit -T paths, e.g.:
security add-generic-password -s "$service" -a "$USER" -w "$password" \
-T /usr/bin/security -T "$(which claude)"Note that the path of the claude binary varies by install method
(/opt/homebrew/bin/claude on Apple Silicon Homebrew, /usr/local/bin/claude
on Intel Homebrew, npm-installed paths, etc.).
- The script refuses to
usea profile while Claude Code is running (it greps the process list forclaudeandClaude.app). Pass--forceto override; the running session keeps its in-memory state but any new writes will land in the newly-linked profile directory. - Switching never touches the contents of any profile directory β only the
~/.claudesymlink moves. - Per-profile backup tokens stay in Keychain after a switch. Switching back restores them; no need to log in again.
claude-switch delete <name>removes the profile directory and its backup Keychain entry. There is no trash recovery.legacyis a reserved profile name and cannot be deleted.
= not found from the old zsh function β that's zsh's EQUALS option
misreading [ "$x" == "$y" ]. Either move the test to [[ ... ]] or switch
to this tool (which already uses [[ ... ]]).
list doesn't show emails β install jq. Without it the script still
works, it just can't read .account.json.
Keychain prompts during a switch β happens once when the existing
Claude Code-credentials entry has a restrictive ACL inherited from a
previous install. Switch to any other profile and back; the delete-then-add
inside kc_save rewrites the entry with -A and silences future prompts.
Profile shows no email β the account info hasn't been captured yet. Switch away from that profile and back; the auto-save on switch will populate it.
~/.claude exists and is not a symlink β you have a real ~/.claude
directory left over from an older install. Move it into the profile
structure first:
mkdir -p ~/.claude-profiles
mv ~/.claude ~/.claude-profiles/legacy
ln -s ~/.claude-profiles/legacy ~/.claude- macOS only. Linux uses
libsecretor a different keystore β the Keychain commands here don't translate. A Linux port would need a separate backend. ~/.claude.jsonis snapshot-on-switch, not live-synced. Claude Code rewrites this file every launch with the active account's data;claude-switchcaptures it at switch time. Between two switches, any state Claude Code writes belongs to whichever profile is currently active β that's the intended isolation.- No mid-session migration. A
--forceswitch while Claude Code is running won't relocate the running session β only future launches see the new profile. - Switching to a profile with no saved snapshot clears
~/.claude.json. Claude Code rebuilds the file on next launch (you may re-see the onboarding banner once). - One active login at a time. macOS Keychain only stores one
Claude Code-credentialsentry. Two Claude Code sessions cannot use two different accounts in parallel via this tool (use two different macOS user accounts for that).
The whole tool is a single bash script (claude-switch). Conventions:
- Bash 3.2 compatible (no associative arrays, no
mapfile). - All user-facing strings in English; emojis are language-neutral status icons.
- Keychain access funneled through
kc_read/kc_save/kc_delete/kc_existsso the ACL strategy is in one place. - Account-info display is a separate concern from token swapping β
account_stashandaccount_fieldare no-ops whenjqisn't installed.
Syntax-check before committing:
bash -n claude-switchMIT β see LICENSE.