Skip to content

Instantly share code, notes, and snippets.

@fortunto2
Last active April 20, 2026 09:35
Show Gist options
  • Select an option

  • Save fortunto2/b326e4727e32f9af1742f0710dcc5f75 to your computer and use it in GitHub Desktop.

Select an option

Save fortunto2/b326e4727e32f9af1742f0710dcc5f75 to your computer and use it in GitHub Desktop.
Claude Code multi-account auth switcher — switch between personal/work/API key profiles via macOS Keychain

Claude Code Multi-Account Setup on macOS

Two ways to run multiple Claude Code subscriptions (personal + work) on one machine, side-by-side, without a re-login every time you switch.

TL;DR

Use long-lived OAuth tokens + direnv. The keychain-swap approach originally in this gist has a subtle bug: Anthropic's short-lived OAuth credentials rotate on every refresh, so a snapshot you saved to disk goes stale within hours. Symptom — "please log in" every time you return to the other profile. Long-lived tokens (claude setup-token, 1-year TTL, no rotation) sidestep the problem entirely.


Recommended: long-lived tokens + direnv

Each subscription gets its own CLAUDE_CODE_OAUTH_TOKEN stored in a file. direnv swaps the env var by directory. No Keychain, no rotation races.

Setup

# 1. Generate personal token (run in any terminal, logged into personal account)
claude setup-token
# Save output → ~/.claude-personal-token
chmod 600 ~/.claude-personal-token

# 2. Generate work token (run in a shell logged into the work account)
mkdir -p ~/.claude-work
claude setup-token
# Save output → ~/.claude-work/token
chmod 600 ~/.claude-work/token

# 3. Personal default in ~/.zshrc:
cat >> ~/.zshrc <<'EOF'

# Claude Code multi-account via long-lived tokens + direnv
[ -f ~/.claude-personal-token ] && \
  export CLAUDE_CODE_OAUTH_TOKEN="$(cat ~/.claude-personal-token)"
eval "$(direnv hook zsh)"
EOF

# 4. Per-project override in ~/projects/work/.envrc:
cat > ~/projects/work/.envrc <<'EOF'
export CLAUDE_CONFIG_DIR=$HOME/.claude-work
export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$CLAUDE_CONFIG_DIR/token")"
EOF
direnv allow ~/projects/work

# 5. Clear stale Keychain OAuth (so Claude Code reads the env token cleanly)
security delete-generic-password -s "Claude Code-credentials" -a "$USER" 2>/dev/null

Open a fresh terminal. In ~/projects/work → work account. Anywhere else → personal. Both persist independently.

Why this works

claude setup-token emits sk-ant-oat01-... — a 1-year OAuth token tied to your Claude subscription. It counts against your Pro/Max quota (not API billing), and critically does not rotate, so file-based storage stays valid for the full year.

CLAUDE_CONFIG_DIR isolates per-project settings, history, and MCP state. Combined with direnv, each project gets its own Claude profile automatically — no wrapper function, no manual switching.

Caveats

  • Already-running sessions don't see new env vars. Restart your shell after editing .zshrc / .envrc.
  • ANTHROPIC_API_KEY takes precedence over CLAUDE_CODE_OAUTH_TOKEN if both are set. Don't mix them.
  • Third-party tools are off-limits. Since April 2026, Anthropic's ToS forbids feeding these tokens to Cursor, Cline, Windsurf, OpenClaw, etc. Claude Code and the official claude-code-action are fine.
  • Status line may display "API" instead of "Subscription". Cosmetic — billing still goes against your subscription quota.
  • One active long-lived token per subscription. Re-running claude setup-token invalidates the previous one.

Legacy: Keychain swap (API keys only)

The original scripts in this gist (claude-switch-auth, zshrc-wrapper.sh, dot-envrc) swap macOS Keychain entries between saved profiles. This works for API keys (sk-ant-api03-..., no rotation), but breaks with OAuth subscriptions: the refresh token rotates on every use, so the snapshot you saved for profile A is already invalid by the time you switch away and back. The wrapper silently overwrites the fresh Keychain entry with the stale file, logging you out.

Keep these files if you have mixed OAuth + API key setups and want per-directory API-key swapping. For OAuth-only subscriptions, use the recommended approach above.


Files

File Purpose
README.md this doc
dot-envrc-token .envrc template for the recommended token approach
dot-envrc .envrc template for the legacy Keychain approach
claude-switch-auth legacy Keychain-swap script (works cleanly only for API keys)
zshrc-wrapper.sh legacy .zshrc wrapper for directory-based Keychain swap

References

License

MIT

#!/bin/bash
# Switch Claude Code keychain credentials between saved profiles.
# Supports both OAuth ("Claude Code-credentials") and API key ("Claude Code") auth.
#
# Usage:
# claude-switch-auth save <name> — save current keychain to profile
# claude-switch-auth <name> — load profile into keychain
# claude-switch-auth status — show active profile
# claude-switch-auth list — list saved profiles
KEYCHAIN_ACCOUNT="${USER:-$(whoami)}"
CREDS_DIR="$HOME/.claude-auth-profiles"
# Detect which keychain service is active
get_service_name() {
if security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
echo "Claude Code-credentials"
elif security find-generic-password -s "Claude Code" >/dev/null 2>&1; then
echo "Claude Code"
else
echo ""
fi
}
# Read the service name stored with a profile (fallback to default)
get_profile_service() {
local profile="$1"
local meta="$CREDS_DIR/${profile}.meta"
if [[ -f "$meta" ]]; then
cat "$meta"
else
echo "Claude Code-credentials"
fi
}
switch_profile() {
local profile="$1"
[[ ! -f "$CREDS_DIR/$profile" ]] && echo "Profile '$profile' not found. Run: claude-switch-auth save $profile" && exit 1
local target_service
target_service=$(get_profile_service "$profile")
local current_service
current_service=$(get_service_name)
python3 -c "
import subprocess
pw = open('$CREDS_DIR/$profile').read().strip()
# Remove old service entry if switching between OAuth/API key modes
for svc in ['Claude Code-credentials', 'Claude Code']:
subprocess.run(['security', 'delete-generic-password', '-s', svc, '-a', '$KEYCHAIN_ACCOUNT'], capture_output=True)
# Add credentials under the correct service name
subprocess.run(['security', 'add-generic-password', '-s', '$target_service', '-a', '$KEYCHAIN_ACCOUNT', '-w', pw], check=True)
"
echo "Switched to: $profile (service: $target_service)"
}
case "$1" in
save)
PROFILE="$2"
[[ -z "$PROFILE" ]] && echo "Usage: claude-switch-auth save <name>" && exit 1
mkdir -p "$CREDS_DIR"
SERVICE=$(get_service_name)
if [[ -z "$SERVICE" ]]; then
echo "Error: No Claude Code credentials found in keychain"
exit 1
fi
security find-generic-password -s "$SERVICE" -a "$KEYCHAIN_ACCOUNT" -w 2>/dev/null > "$CREDS_DIR/$PROFILE"
echo "$SERVICE" > "$CREDS_DIR/${PROFILE}.meta"
chmod 600 "$CREDS_DIR/$PROFILE" "$CREDS_DIR/${PROFILE}.meta"
echo "Saved profile: $PROFILE (service: $SERVICE)"
;;
status)
SERVICE=$(get_service_name)
if [[ -z "$SERVICE" ]]; then
echo "No Claude Code credentials found"
exit 1
fi
echo "Service: $SERVICE"
security find-generic-password -s "$SERVICE" -a "$KEYCHAIN_ACCOUNT" -w 2>/dev/null | \
python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
o = d.get('claudeAiOauth', {})
if o:
print(f'Type: OAuth — {o.get(\"subscriptionType\", \"?\")} / {o.get(\"rateLimitTier\", \"?\")}')
elif d.get('claudeAiApiKey'):
print('Type: API Key')
else:
print('Type: Unknown')
except: print('Could not parse credentials')
"
;;
list)
if [[ -d "$CREDS_DIR" ]]; then
for f in "$CREDS_DIR"/*; do
[[ "$f" == *.meta ]] && continue
[[ -f "$f" ]] || continue
name=$(basename "$f")
service=$(get_profile_service "$name")
echo " $name ($service)"
done
else
echo "No profiles saved"
fi
;;
"")
echo "Usage: claude-switch-auth {save <name>|<name>|status|list}"
;;
*)
switch_profile "$1"
;;
esac
# .envrc — put in project root, requires direnv
# Isolates Claude Code config (history, settings) per project
export CLAUDE_CONFIG_DIR=$HOME/.claude-epiphan
# Place as .envrc in your per-account project root (e.g. ~/projects/work/.envrc).
# Then run: direnv allow .
#
# Assumes you generated a long-lived token via `claude setup-token`
# and stored it at $CLAUDE_CONFIG_DIR/token (chmod 600).
export CLAUDE_CONFIG_DIR=$HOME/.claude-work
[ -f "$CLAUDE_CONFIG_DIR/token" ] && \
export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$CLAUDE_CONFIG_DIR/token")"
# Add to ~/.zshrc — auto-switch Claude auth profile by directory
claude() {
if [[ "$PWD" == ~/projects/epiphan* ]]; then
claude-switch-auth epiphan 2>/dev/null
else
claude-switch-auth personal 2>/dev/null
fi
command claude "$@"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment