Skip to content

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ccswitch

Switch between multiple Claude Code accounts with one command — without re-running /login every time, and without splitting your setup into isolated copies.

$ ccswitch list
  ACCOUNT      EMAIL                          STATUS               RELOGIN
* work         you@example.com                ready                24d
  personal     you@example.net                ready                19d
  client       you@example.org                BROKEN - re-login    -

$ ccswitch personal
switched to personal  (you@example.net)  valid 6h 40m

$ claude          # runs as personal

Why

Claude Code stores one signed-in account at a time. The usual workaround is to give each account its own CLAUDE_CONFIG_DIR, but that isolates everything — your MCP servers, plugins, skills, agents, slash commands, session history and CLAUDE.md all get duplicated per account, and each copy drifts.

ccswitch takes the opposite approach. One ~/.claude, shared by every account. Only the credential and the identity it belongs to are swapped.

Separate CLAUDE_CONFIG_DIR ccswitch
MCP servers duplicated per account shared
Plugins & marketplaces duplicated per account shared
Skills, agents, commands duplicated per account shared
Session history duplicated per account shared
settings.json, CLAUDE.md duplicated per account shared
Accounts active at once many one

That last row is the trade-off. If you need two accounts running simultaneously in different terminals, use CLAUDE_CONFIG_DIR instead — ccswitch changes which account claude runs as, globally.

Install

With npm, if you already have Node around:

npm install -g @2hmad/ccswitch

Or with no Node at all:

curl -fsSL https://raw.githubusercontent.com/2hmad/ccswitch/main/install.sh | bash

Or from a checkout:

git clone https://github.com/2hmad/ccswitch
cd ccswitch && ./install.sh

The npm package is scoped because the unscoped ccswitch name belongs to an unrelated project. The installed command is still ccswitch.

Requires bash, python3, and Claude Code. Node is only a delivery mechanism — nothing at runtime uses it. Linux is supported; macOS is experimental — see Platform support.

Update

ccswitch update            # fetch and install the latest release
ccswitch update --check    # just report what is available

It knows how it was installed and won't fight your package manager — an npm copy is told to run npm update -g @2hmad/ccswitch rather than overwriting itself. Before replacing anything it checks the download is really ccswitch, of the expected version, and parses as valid bash, then swaps it in by rename so the running script is never written through.

ccswitch list mentions a newer version when one exists. That line is printed from a cache written by the scheduled refresh run, so list itself never touches the network. Silence it with CCSWITCH_NO_UPDATE_NOTICE=1.

Usage

Sign each account in once:

ccswitch login work        # opens Claude Code; run /login, then /exit
ccswitch login personal
ccswitch login client

Already signed in as one of them? Capture it without a fresh login:

ccswitch add work

Signed in with /login inside Claude Code instead? That changes the live account without telling ccswitch, so point it at the right slot afterwards:

ccswitch save work        # adopt the live credential into 'work'

A bare ccswitch save writes into the active account, and refuses if the live credential belongs to a different one — it names the account it actually belongs to.

Then switch whenever you like:

ccswitch personal         # shorthand
ccswitch use personal     # same thing
claude                    # runs as personal

All commands

Command Description
ccswitch <name> Switch to an account
ccswitch use <name> [--force] Same, explicit form
ccswitch login <name> Sign a new account in and store it
ccswitch add <name> Store the account you're already signed in as
ccswitch list Accounts, emails, token status
ccswitch current Print the active account name
ccswitch save [name] Write the live token back to its account
ccswitch sync Capture the live token into the account it belongs to
ccswitch autosync install Capture every rotation automatically
ccswitch refresh [name|--all] [--force] Renew a token as its refresh token nears expiry
ccswitch rm <name> Forget an account
ccswitch rename <old> <new> Rename an account
ccswitch backup [file] Archive the vault
ccswitch restore <file> Restore a vault archive
ccswitch doctor Diagnose the setup
ccswitch update [--check] Update ccswitch itself
ccswitch completion bash|zsh Print a completion script

Keeping parked accounts alive

Claude Code holds two tokens: a short-lived access token (~8h) and a refresh token used to mint new ones. The refresh token rotates — every renewal consumes the old one and issues a replacement. Present a consumed token and the server answers invalid_grant, at which point Claude Code marks it dead and blanks the credential on disk, so the next thing you type asks you to sign in again.

Each rotation also pushes the refresh token's expiry back out to roughly 28 days from that moment, so an account you actually use never lapses. The 28-day clock is not what costs you a login. Losing a rotation is. If the vault holds a snapshot taken before a rotation, restoring it hands the server a token that has already been spent.

That is what ccswitch autosync install is for: a systemd path unit watches the credential file and captures every rotation as it is written, so the vault can never fall behind. Without it the vault only catches up when you happen to run a ccswitch command, which leaves a window.

The access token expiring is a non-event — Claude Code renews it on use — so ccswitch list does not report it. ccswitch doctor shows it if you want the detail.

That matters for a tool that snapshots and restores credentials. ccswitch keeps the vault in step by syncing the live credential into the active account's slot on every run, not only when you switch — a token Claude Code rotated mid-session, or a /login you typed inside Claude Code, would otherwise never reach the vault, and restoring that stale snapshot later would hand the server a consumed token. ccswitch list reports a slot whose tokens have been cleared as BROKEN - re-login, and its RELOGIN column counts down the refresh token's real remaining life.

An account you haven't switched to in a few weeks can lose its refresh token, which forces a full ccswitch login <name> with a browser round-trip. ccswitch refresh --all prevents that: it restores each stored account in turn, makes one tiny API call to exercise its token, saves the renewed credential back, and returns you to the account you started on.

Run it by hand whenever it occurs to you:

ccswitch refresh --all

Or schedule it daily. Because each renewal rotates the token, refresh does nothing until an account is within CCSWITCH_REFRESH_WINDOW_DAYS (default 7) of its refresh token expiring — so a typical run makes no API calls at all:

$ ccswitch refresh --all
skip    work  (you@example.com)  re-login in 24d - not due yet
refreshed personal  (you@example.net)  valid 8h 0m

systemd user timer (recommended)

Prefer this on anything that isn't a server running 24/7. Persistent=true catches up a run missed while the machine was off; cron silently skips it. A desktop that sleeps overnight will never fire a 4am cron job — the run is lost every night, without a single log line to say so. Pick a time the machine is usually awake anyway.

~/.config/systemd/user/ccswitch-refresh.service:

[Unit]
Description=Refresh parked ccswitch account tokens

[Service]
Type=oneshot
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=%h/.local/bin/ccswitch refresh --all

~/.config/systemd/user/ccswitch-refresh.timer:

[Unit]
Description=Daily ccswitch token refresh

[Timer]
OnCalendar=*-*-* 15:30:00
Persistent=true
RandomizedDelaySec=15m

[Install]
WantedBy=timers.target

Enable it:

systemctl --user daemon-reload
systemctl --user enable --now ccswitch-refresh.timer

systemctl --user list-timers ccswitch-refresh.timer   # when it next fires
journalctl --user -u ccswitch-refresh.service         # what happened last time

If you want it to run while you're logged out, also sudo loginctl enable-linger $USER.

cron

Only if the machine is genuinely always on. crontab -e, then:

PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin

30 4 * * * ccswitch refresh --all >> "$HOME/.cache/ccswitch-refresh.log" 2>&1

Two things that trip people up:

  • Set PATH. cron runs with a bare /usr/bin:/bin, and refresh needs both ccswitch and the claude binary. Run command -v claude and put that directory first — it's usually ~/.local/bin. Without this the job dies with the 'claude' command was not found on PATH.
  • Pick an hour the machine is actually on, and you are not working. refresh refuses to run while a claude process is alive. A job scheduled for a time you are always shut down never runs at all, and cron will not tell you.

How it works

Claude Code keeps two pieces of account state:

  • ~/.claude/.credentials.json — the OAuth access and refresh tokens
  • ~/.claude.json — a mixed file holding your sign-in identity and your MCP servers, per-project trust decisions, and other config

ccswitch stores per account, under ~/.config/ccswitch/accounts/<name>/:

credentials.json   copy of .credentials.json
identity.json      only the oauthAccount and userID keys from .claude.json

Switching writes those two back into place and leaves every other key in ~/.claude.json untouched — which is why mcpServers, projects and the rest stay shared.

Before switching away, ccswitch saves the live credential back into the account you're leaving. Claude Code refreshes tokens during a session, so without this you'd restore a stale token the next time around.

Notes and caveats

One account is active at a time. This is by design — it's the price of sharing everything. ccswitch use refuses to run while a claude process is detected; pass --force to override.

The vault holds live session tokens. ~/.config/ccswitch is created mode 700 and files mode 600. ccswitch backup produces an archive containing those tokens — encrypt it if you keep it anywhere but your own disk.

Tokens rotate, and a lost rotation costs you a login. Renewing consumes the old refresh token, so a snapshot taken before a rotation is worthless afterwards — the server answers invalid_grant and Claude Code blanks the credential on disk, which is why an account can suddenly demand /login mid-prompt. ccswitch syncs the live credential into the active slot on every run to stay ahead of this. An account it could not keep up with shows as BROKEN - re-login in ccswitch list, and needs one ccswitch login <name>.

Parked accounts still lapse. The access token (~8h) refreshes itself whenever the active account is used — Claude Code does that on its own. Accounts sitting in the vault aren't touched by anything, so their refresh token (~28 days) can expire outright. Run ccswitch refresh --all occasionally, or schedule it — see Keeping parked accounts alive. Because each renewal is itself a rotation, it acts only within CCSWITCH_REFRESH_WINDOW_DAYS (default 7) of the refresh token's expiry, so most runs make no API call at all. The RELOGIN column in ccswitch list is the number that matters.

CLAUDE_CONFIG_DIR takes priority. If it's set, ccswitch operates on that directory instead of ~/.claude. ccswitch doctor will warn you. If you're migrating from per-account config dirs, unset it first.

Project config is unaffected. .mcp.json, .claude/settings.json and CLAUDE.md inside a repo load from the repo regardless of which account is active.

Migrating from per-account CLAUDE_CONFIG_DIR

If you already have isolated config dirs, pick the one with the setup you want to keep, make it your ~/.claude, then capture each account's credential:

unset CLAUDE_CONFIG_DIR                    # and remove it from ~/.zshrc

# keep the best-configured directory as the shared one
mv ~/.claude ~/.claude.old
cp -r ~/.claude-accounts/work ~/.claude
cp ~/.claude-accounts/work/.claude.json ~/.claude.json

ccswitch add work                          # captures what's now live

# for each remaining account, drop its credential in and capture it
cp ~/.claude-accounts/personal/.credentials.json ~/.claude/.credentials.json
python3 - <<'EOF'
import json, os
h = os.path.expanduser("~")
src = json.load(open(f"{h}/.claude-accounts/personal/.claude.json"))
dst = json.load(open(f"{h}/.claude.json"))
for k in ("oauthAccount", "userID"):
    dst[k] = src.get(k)
json.dump(dst, open(f"{h}/.claude.json", "w"), indent=2)
EOF
ccswitch add personal

Then ccswitch list to confirm all of them are there.

Moving to another machine

The vault is portable, but an account cannot be live on two machines at once. Refresh tokens rotate: whichever machine renews first invalidates the other's copy, and the loser gets a forced browser sign-in. So this is a move, not a copy — decide which machine owns the accounts.

On the machine you are leaving:

ccswitch sync                              # capture the newest rotation first
ccswitch backup ~/ccswitch-move.tar.gz     # the whole vault

ccswitch sync matters. The backup is only as fresh as the vault, and a rotation that has not been captured yet would be left behind — you would carry an already-spent token to the new machine.

Copy it over a channel you trust, because it contains live session tokens:

scp ~/ccswitch-move.tar.gz you@laptop:~/

On the new machine:

npm install -g @2hmad/ccswitch     # or the curl installer
ccswitch restore ~/ccswitch-move.tar.gz
ccswitch autosync install
ccswitch list                      # every account should read 'ready'
shred -u ~/ccswitch-move.tar.gz    # it holds live tokens

Then on the old machine, stop using those accounts — ccswitch rm <name> for each, or delete ~/.config/ccswitch outright. Leaving them behind is what causes the two machines to fight over rotations.

What the vault does not carry

ccswitch stores only the credential and the identity it belongs to. Your MCP servers, plugins, skills, agents, commands, history and CLAUDE.md live in ~/.claude and ~/.claude.json, and are the same for every account — copy them separately if you want the same setup:

tar czf ~/claude-config.tar.gz -C ~ .claude .claude.json

Restore that before ccswitch restore, since it overwrites ~/.claude.json — which is also where the active account's identity lives.

On WSL

WSL behaves like Linux, and the file backend applies. One catch: ccswitch autosync and the scheduled refresh both need systemd, which WSL does not run as init unless you switch it on:

printf '[boot]\nsystemd=true\n' | sudo tee -a /etc/wsl.conf

Then wsl --shutdown from Windows and reopen. Without it, ccswitch autosync install will tell you so, and you can run ccswitch sync by hand after signing in instead.

Keep everything inside the WSL filesystem (~), not under /mnt/c. Windows drives do not carry Unix permissions, so the vault could not be locked down to mode 700.

Platform support

Platform Status
Linux Supported
macOS Experimental — works via the login keychain, but not yet confirmed on real hardware (#1)
Windows / WSL WSL behaves like Linux and should work; untested

On macOS, Claude Code keeps the OAuth credential in your login keychain rather than in ~/.claude/.credentials.json, so ccswitch reads and writes it there with security(1). It derives the same item Claude Code does:

Value
Service Claude Code-credentials, plus -<sha256(config dir)[:8]> when CLAUDE_CONFIG_DIR is set
Account $USER, or claude-code-user if that is unset or contains anything outside [a-zA-Z0-9._-]

This path has been exercised against a security(1) stand-in but not yet against a real Keychain, so treat it as experimental and report anything odd on #1.

ccswitch doctor prints what it resolved to. The first switch may raise a keychain prompt — macOS asks before letting a new binary read an item it did not create. Choose Always Allow if you would rather not be asked again.

The keychain is used only when there is no ~/.claude/.credentials.json; a file left by an older Claude Code still wins. Force it either way with CCSWITCH_BACKEND=keychain or CCSWITCH_BACKEND=file.

Contributing

Issues and pull requests welcome. Please run shellcheck bin/ccswitch before submitting; CI runs it on every push.

Changelog

See CHANGELOG.md.

License

MIT — see LICENSE.

About

Switch between multiple Claude Code accounts with one command — MCP servers, plugins, skills and session history stay shared.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages