Skip to content

Instantly share code, notes, and snippets.

@harryf
Created March 8, 2026 12:35
Show Gist options
  • Select an option

  • Save harryf/7ef9d3339a545b68587dc3e7eec70cf5 to your computer and use it in GitHub Desktop.

Select an option

Save harryf/7ef9d3339a545b68587dc3e7eec70cf5 to your computer and use it in GitHub Desktop.
Switching Anthropic Accounts

How to Switch Between Work and Personal Anthropic Accounts with Claude Code and Claude Desktop

A guide for running two Anthropic accounts (e.g., work Team/Enterprise + personal Pro/Max) on the same macOS machine, with one-command switching for both Claude Code (CLI) and Claude Desktop.

Platform: macOS (uses macOS Keychain; Linux would need adaptation) Claude Code Version: 2.x Claude Desktop: Electron-based (1.x+)


Table of Contents


Overview

Anthropic doesn't offer built-in multi-account support. Both Claude Code and Claude Desktop assume a single authenticated user per machine. This guide works around that with:

  • Claude Code: Two ~/.claude profile directories, a symlink pointing to the active one, and macOS Keychain token rotation.
  • Claude Desktop: Swapping account-specific files (~5MB) inside ~/Library/Application Support/Claude/ while leaving shared data (8GB+ of caches and VM bundles) in place.

Both are scripted into single commands: claude-switch <work|personal> and claude-switch-desktop <work|personal>.


Part 1: Claude Code

How Claude Code Stores Auth

Claude Code stores a single OAuth token in the macOS Keychain:

Service:  "Claude Code-credentials"
Account:  <your macOS username>
Contents: JSON with accessToken, refreshToken, subscriptionType, scopes

The token JSON looks like:

{
  "claudeAiOauth": {
    "accessToken": "sk-ant-oat01-...",
    "refreshToken": "sk-ant-ort01-...",
    "expiresAt": 1772984739060,
    "scopes": ["user:inference", "user:profile", "..."],
    "subscriptionType": "team",
    "rateLimitTier": "default_claude_max_5x"
  }
}

Note: the token does not contain your email. Claude Code resolves the email by calling the API with the access token. The subscriptionType field is the best way to distinguish tokens (team/enterprise for work, pro/max for personal).

All Claude Code configuration, settings, hooks, history, and project memory lives under ~/.claude/.

Strategy: Symlink Swap + Keychain Rotation

  1. Two profile directories: ~/.claude-work/ and ~/.claude-personal/
  2. ~/.claude is a symlink pointing to the active profile
  3. Each profile stores its OAuth token in .auth-token
  4. On switch: save current keychain token to the outgoing profile, swap the symlink, load the incoming profile's token into keychain

Why symlinks:

  • Fast — single filesystem operation, no copying
  • Transparent — any tool referencing ~/.claude sees the active profile
  • Safe — both directories always exist intact

Claude Code Setup

Step 1: Backup and Export Current Token

# Backup current ~/.claude
cp -a ~/.claude ~/.claude-backup-$(date +%Y%m%d)

# Export current keychain token (your currently-authenticated account)
security find-generic-password -s "Claude Code-credentials" -w > /tmp/claude-token-primary.json
chmod 600 /tmp/claude-token-primary.json

# Verify it looks right
cat /tmp/claude-token-primary.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
o = d.get('claudeAiOauth', {})
print(f'subscriptionType: {o.get(\"subscriptionType\", \"?\")}')
print(f'rateLimitTier: {o.get(\"rateLimitTier\", \"?\")}')
"

Step 2: Capture the Second Account's Token

IMPORTANT: Do NOT use claude auth logout — it revokes the token server-side, permanently invalidating the token you just exported. Use claude auth login directly instead, which overwrites the keychain without revoking the previous token.

# Login as the second account (overwrites keychain, does NOT revoke first token)
claude auth login
# → Complete the OAuth flow in your browser for the second account

# Verify
claude auth status

# Export second token
security find-generic-password -s "Claude Code-credentials" -w > /tmp/claude-token-secondary.json
chmod 600 /tmp/claude-token-secondary.json

Step 3: Restore Primary Token

security delete-generic-password -s "Claude Code-credentials" -a "$(whoami)" >/dev/null 2>&1 || true
security add-generic-password -s "Claude Code-credentials" -a "$(whoami)" -w "$(cat /tmp/claude-token-primary.json)"

# Verify
claude auth status

Step 4: Create Profile Directories

Your current ~/.claude becomes the work profile (or whichever account was primary):

# Move current directory to work profile
mv ~/.claude ~/.claude-work

# Store token inside the profile
mv /tmp/claude-token-primary.json ~/.claude-work/.auth-token
chmod 600 ~/.claude-work/.auth-token

# CRITICAL: Immediately create symlink so nothing breaks
ln -s ~/.claude-work ~/.claude

# Verify
ls -la ~/.claude
claude auth status

Step 5: Create Second Profile

Option A: Clone and customize (recommended)

cp -a ~/.claude-work ~/.claude-personal
mv /tmp/claude-token-secondary.json ~/.claude-personal/.auth-token
chmod 600 ~/.claude-personal/.auth-token

Then customize ~/.claude-personal/:

  • Edit settings.json — remove work-specific MCP servers, adjust env vars
  • Edit CLAUDE.md — adjust instructions for personal context
  • Clear projects/ — remove work project memory
  • Clear history.jsonl — start fresh

Option B: Fresh minimal profile

mkdir -p ~/.claude-personal
mv /tmp/claude-token-secondary.json ~/.claude-personal/.auth-token
chmod 600 ~/.claude-personal/.auth-token

# Copy essentials from work profile
cp -a ~/.claude-work/CLAUDE.md ~/.claude-personal/
cp -a ~/.claude-work/settings.json ~/.claude-personal/
# Copy any other dirs you want (hooks/, skills/, etc.)
mkdir -p ~/.claude-personal/{projects,cache,debug}

Step 6: Install the Switch Script

Save the script below as claude-switch somewhere on your PATH (e.g., ~/bin/claude-switch or /usr/local/bin/claude-switch), then chmod +x it.

The Claude Code Switch Script

#!/usr/bin/env bash
set -euo pipefail

# Claude Code Profile Switcher
# Usage: claude-switch personal|work|status

PERSONAL_DIR="$HOME/.claude-personal"
WORK_DIR="$HOME/.claude-work"
LINK="$HOME/.claude"
KEYCHAIN_SERVICE="Claude Code-credentials"
KEYCHAIN_ACCOUNT="$(whoami)"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

usage() {
    echo "Usage: claude-switch <personal|work|status>"
    echo ""
    echo "  personal  Switch to personal Claude account"
    echo "  work      Switch to work Claude account"
    echo "  status    Show which profile is active"
    exit 1
}

get_current() {
    if [ -L "$LINK" ]; then
        local target
        target="$(readlink "$LINK")"
        case "$target" in
            *-personal*) echo "personal" ;;
            *-work*)     echo "work" ;;
            *)           echo "unknown" ;;
        esac
    elif [ -d "$LINK" ]; then
        echo "ERROR: ~/.claude is a real directory, not a symlink."
        echo "Run the one-time setup to convert it to a symlink."
        exit 1
    else
        echo "ERROR: ~/.claude does not exist."
        exit 1
    fi
}

get_profile_dir() {
    case "$1" in
        personal) echo "$PERSONAL_DIR" ;;
        work)     echo "$WORK_DIR" ;;
    esac
}

save_current_token() {
    local profile_dir="$1"
    local token
    token="$(security find-generic-password -s "$KEYCHAIN_SERVICE" -w 2>/dev/null || true)"
    if [ -n "$token" ]; then
        echo "$token" > "$profile_dir/.auth-token"
        chmod 600 "$profile_dir/.auth-token"
    fi
}

load_token() {
    local profile_dir="$1"
    local token_file="$profile_dir/.auth-token"
    if [ ! -f "$token_file" ]; then
        echo -e "${YELLOW}WARNING: No auth token found for this profile.${NC}"
        echo "Run 'claude auth login' to authenticate, then switch away and back."
        return
    fi
    local token
    token="$(cat "$token_file")"
    security delete-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" >/dev/null 2>&1 || true
    security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "$token"
}

check_claude_processes() {
    local pids
    pids="$(pgrep -f 'claude' 2>/dev/null || true)"
    if [ -n "$pids" ]; then
        echo -e "${YELLOW}WARNING: Claude Code processes detected. Restart them after switching.${NC}"
    fi
}

switch_profile() {
    local target_profile="$1"
    local target_dir
    target_dir="$(get_profile_dir "$target_profile")"

    if [ ! -d "$target_dir" ]; then
        echo -e "${RED}ERROR: Profile directory $target_dir does not exist.${NC}"
        echo "Run the one-time setup first."
        exit 1
    fi

    local current
    current="$(get_current)"
    if [ "$current" = "$target_profile" ]; then
        echo -e "${BLUE}Already on $target_profile profile.${NC}"
        show_status
        return
    fi

    check_claude_processes

    echo "Saving current auth token..."
    local current_dir
    current_dir="$(readlink "$LINK")"
    save_current_token "$current_dir"

    echo "Switching to $target_profile..."
    rm "$LINK"
    ln -s "$target_dir" "$LINK"

    echo "Loading $target_profile credentials..."
    load_token "$target_dir"

    echo ""
    echo -e "${GREEN}Switched to $target_profile profile.${NC}"
    show_status

    echo ""
    echo -e "${YELLOW}NOTE: Restart any running Claude Code sessions for changes to take effect.${NC}"
}

show_status() {
    local current
    current="$(get_current)"
    local target_dir
    target_dir="$(readlink "$LINK")"

    echo -e "  Profile:   ${GREEN}$current${NC}"
    echo "  Directory: $target_dir"

    # Read subscription type from keychain token directly.
    # Do NOT use `claude auth status` here — it queries the running process's
    # in-memory state and may show stale info after a switch.
    local token sub_type
    token="$(security find-generic-password -s "$KEYCHAIN_SERVICE" -w 2>/dev/null || true)"
    if [ -n "$token" ]; then
        sub_type="$(echo "$token" | python3 -c "
import sys, json
print(json.load(sys.stdin).get('claudeAiOauth',{}).get('subscriptionType','unknown'))
" 2>/dev/null || echo "unknown")"
        echo "  Token:     $sub_type"
    else
        echo -e "  Token:     ${YELLOW}no keychain token found${NC}"
    fi
}

case "${1:-}" in
    personal|work) switch_profile "$1" ;;
    status)
        echo "Claude Code Profile Status"
        echo "=========================="
        show_status
        ;;
    *) usage ;;
esac

Usage

claude-switch status     # Show active profile
claude-switch work       # Switch to work
claude-switch personal   # Switch to personal

After switching, restart any running Claude Code sessions.

Claude Code Gotchas

claude auth logout revokes tokens server-side. Never use it during setup. Use claude auth login directly to capture a second account's token — it overwrites the keychain without revoking the previous token.

claude auth status reads from the running process, not the keychain. After switching, claude auth status may show stale info from a still-running Claude Code process. The keychain swap is correct — just restart the session. The switch script reads the keychain directly to avoid this.

Token expiry. OAuth tokens expire. If a profile shows logged-out after switching, run claude auth login. The script captures the refreshed token next time you switch away.

MCP server port conflicts. If both profiles configure MCP servers on the same ports, kill old servers before switching: pkill -f "mcp-server" 2>/dev/null || true

Running sessions must restart. Claude Code loads config at startup and caches it in memory. A switch changes the filesystem and keychain, but running processes still have the old config. Always exit and restart.

Project memory is profile-specific. Each profile has its own projects/ directory. If you work on the same repo from both accounts, they'll have independent project-level memory and CLAUDE.md files.


Part 2: Claude Desktop

How Claude Desktop Stores Auth

Claude Desktop is an Electron app. It does not use the macOS Keychain for OAuth tokens. Instead:

Storage Contents
config.json Encrypted OAuth token in oauth:tokenCache, org-specific extension settings
Local Storage/ Account email, UUID, org UUID, subscription info (LevelDB)
Cookies Session cookies: __ssid, sessionKey (encrypted SQLite)
IndexedDB/ Web app storage for https://claude.ai (LevelDB)
Session Storage/ Conversation state, editor state (LevelDB)
blob_storage/ Conversation attachments

All of these live under ~/Library/Application Support/Claude/.

The oauth:tokenCache is encrypted with Electron safeStorage, whose key is stored in the Keychain as "Claude Safe Storage". This key is per-machine, not per-account, which is what makes swapping possible — encrypted tokens from either account can be decrypted on the same machine.

The directory also contains ~8GB of shared data (vm_bundles/, Cache/, Code Cache/, claude-code/) that doesn't need to be swapped.

Strategy: Selective File Swap

Instead of symlinking the entire 8GB+ directory, swap only the ~5MB of account-specific files:

config.json                    # Encrypted OAuth token + org settings
Local Storage/                 # Account metadata (LevelDB)
Cookies + Cookies-journal      # Session cookies (SQLite)
IndexedDB/                     # Web app storage (LevelDB)
Session Storage/               # Conversation state (LevelDB)
blob_storage/                  # Attachments
WebStorage/                    # Web storage quota
local-agent-mode-sessions/     # Agent sessions
shared_proto_db/               # Protocol buffer DB
ant-did                        # Anonymization ID
DIPS                           # Bounce tracking
Trust Tokens                   # Trust tokens
Network Persistent State       # HTTP/QUIC state

Profile snapshots are stored in ~/.claude-desktop-profiles/work/ and ~/.claude-desktop-profiles/personal/.

Claude Desktop Setup

Step 1: Capture the First Account

Claude Desktop must be fully quit before any file operations (it holds locks on the LevelDB and SQLite files).

# Quit Claude Desktop
osascript -e 'tell application "Claude" to quit' 2>/dev/null
sleep 2

CLAUDE_DIR="$HOME/Library/Application Support/Claude"
PROFILES_DIR="$HOME/.claude-desktop-profiles"

mkdir -p "$PROFILES_DIR/work"
mkdir -p "$PROFILES_DIR/personal"

# Copy account-specific files
SWAP_FILES=(
    "config.json" "Local Storage" "Cookies" "Cookies-journal"
    "IndexedDB" "Session Storage" "blob_storage" "WebStorage"
    "local-agent-mode-sessions" "shared_proto_db"
    "ant-did" "DIPS" "Trust Tokens" "Network Persistent State"
)

for f in "${SWAP_FILES[@]}"; do
    [ -e "$CLAUDE_DIR/$f" ] && cp -a "$CLAUDE_DIR/$f" "$PROFILES_DIR/work/$f"
done

echo "Work profile captured."

Step 2: Sign In as Second Account and Capture

# Launch Claude Desktop
open -a Claude

# In the app: click profile icon → Sign out → Sign in with second account
# Verify you see the correct account, then quit

osascript -e 'tell application "Claude" to quit' 2>/dev/null
sleep 2

for f in "${SWAP_FILES[@]}"; do
    [ -e "$CLAUDE_DIR/$f" ] && cp -a "$CLAUDE_DIR/$f" "$PROFILES_DIR/personal/$f"
done

echo "Personal profile captured."

Step 3: Restore First Account

for f in "${SWAP_FILES[@]}"; do
    if [ -e "$PROFILES_DIR/work/$f" ]; then
        rm -rf "$CLAUDE_DIR/$f"
        cp -a "$PROFILES_DIR/work/$f" "$CLAUDE_DIR/$f"
    fi
done

# Track which profile is active
echo "work" > "$PROFILES_DIR/.active"

echo "Work profile restored."

Step 4: Install the Switch Script

Save as claude-switch-desktop on your PATH, then chmod +x it.

The Claude Desktop Switch Script

#!/usr/bin/env bash
set -euo pipefail

# Claude Desktop Profile Switcher
# Usage: claude-switch-desktop personal|work|status

CLAUDE_DESKTOP_DIR="$HOME/Library/Application Support/Claude"
PROFILES_DIR="$HOME/.claude-desktop-profiles"
ACTIVE_FILE="$PROFILES_DIR/.active"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

SWAP_FILES=(
    "config.json"
    "Local Storage"
    "Cookies"
    "Cookies-journal"
    "IndexedDB"
    "Session Storage"
    "blob_storage"
    "WebStorage"
    "local-agent-mode-sessions"
    "shared_proto_db"
    "ant-did"
    "DIPS"
    "Trust Tokens"
    "Network Persistent State"
)

usage() {
    echo "Usage: claude-switch-desktop <personal|work|status>"
    echo ""
    echo "  personal  Switch to personal Claude account"
    echo "  work      Switch to work Claude account"
    echo "  status    Show which profile is active"
    exit 1
}

get_current() {
    if [ -f "$ACTIVE_FILE" ]; then
        cat "$ACTIVE_FILE"
    else
        echo "unknown"
    fi
}

save_desktop_profile() {
    local profile="$1"
    local dest="$PROFILES_DIR/$profile"
    mkdir -p "$dest"
    for f in "${SWAP_FILES[@]}"; do
        if [ -e "$CLAUDE_DESKTOP_DIR/$f" ]; then
            rm -rf "$dest/$f"
            cp -a "$CLAUDE_DESKTOP_DIR/$f" "$dest/$f"
        fi
    done
}

load_desktop_profile() {
    local profile="$1"
    local src="$PROFILES_DIR/$profile"
    if [ ! -d "$src" ]; then
        echo -e "${YELLOW}WARNING: No Claude Desktop profile for $profile.${NC}"
        echo "Launch Claude Desktop, sign in, then switch away and back to capture it."
        return 1
    fi
    if [ ! -f "$src/config.json" ]; then
        echo -e "${YELLOW}WARNING: Profile $profile has no config.json — may not be captured yet.${NC}"
        return 1
    fi
    for f in "${SWAP_FILES[@]}"; do
        if [ -e "$src/$f" ]; then
            rm -rf "$CLAUDE_DESKTOP_DIR/$f"
            cp -a "$src/$f" "$CLAUDE_DESKTOP_DIR/$f"
        fi
    done
}

quit_claude_desktop() {
    if pgrep -xq "Claude"; then
        echo "Quitting Claude Desktop..."
        osascript -e 'tell application "Claude" to quit' 2>/dev/null || true
        local i=0
        while pgrep -xq "Claude" && [ $i -lt 10 ]; do
            sleep 0.5
            ((i++))
        done
        if pgrep -xq "Claude"; then
            echo -e "${RED}Claude Desktop didn't quit. Kill it manually or run: killall Claude${NC}"
            return 1
        fi
        echo "Claude Desktop quit."
    fi
}

show_status() {
    local current
    current="$(get_current)"

    echo -e "  Profile:   ${GREEN}$current${NC}"

    if [ -f "$CLAUDE_DESKTOP_DIR/config.json" ]; then
        local has_token config_file="$CLAUDE_DESKTOP_DIR/config.json"
        has_token="$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print('yes' if 'oauth:tokenCache' in d else 'no')" "$config_file" 2>/dev/null || echo "no")"
        if [ "$has_token" = "yes" ]; then
            echo "  Token:     present (encrypted)"
        else
            echo -e "  Token:     ${YELLOW}missing${NC}"
        fi
    else
        echo -e "  Config:    ${YELLOW}no config.json found${NC}"
    fi
}

switch_profile() {
    local target_profile="$1"
    local current
    current="$(get_current)"

    if [ "$current" = "$target_profile" ]; then
        echo -e "${BLUE}Already on $target_profile profile.${NC}"
        show_status
        return
    fi

    quit_claude_desktop || exit 1

    if [ "$current" != "unknown" ]; then
        echo "Saving current ($current) desktop profile..."
        save_desktop_profile "$current"
    fi

    echo "Loading $target_profile desktop profile..."
    load_desktop_profile "$target_profile" || exit 1

    echo "$target_profile" > "$ACTIVE_FILE"

    echo ""
    echo -e "${GREEN}Switched Claude Desktop to $target_profile.${NC}"
    show_status

    echo ""
    echo "Launch Claude Desktop with: open -a Claude"
}

case "${1:-}" in
    personal|work) switch_profile "$1" ;;
    status)
        echo "Claude Desktop Profile Status"
        echo "=============================="
        show_status
        ;;
    *) usage ;;
esac

Usage

claude-switch-desktop status     # Show active profile
claude-switch-desktop work       # Switch to work
claude-switch-desktop personal   # Switch to personal

After switching, launch Claude Desktop with open -a Claude.

Claude Desktop Gotchas

Claude Desktop must be fully quit first. It holds file locks on LevelDB databases and the SQLite Cookies file. Swapping files while the app is running will corrupt the databases. The script handles quitting automatically.

Do not delete the "Claude Safe Storage" keychain entry. This is the Electron safeStorage encryption key shared by both profiles. Deleting it invalidates both profiles' encrypted OAuth tokens and you'll need to re-authenticate both.

Session cookies may expire. If you switch to a profile and find yourself logged out, just sign in again through the app. The script captures the new session next time you switch away.

Conversation history is server-side. Switching profiles doesn't affect conversation access — you'll see whichever account's conversations match the active token.

Claude Code inside Claude Desktop uses Desktop's auth. If you use the terminal/agent feature in Claude Desktop, it uses Desktop's OAuth token, not the Claude Code keychain token. Switching Claude Code's profile doesn't affect embedded sessions.


Quick Reference

Command What It Does
claude-switch status Show active Claude Code profile
claude-switch work Switch Claude Code to work account
claude-switch personal Switch Claude Code to personal account
claude-switch-desktop status Show active Claude Desktop profile
claude-switch-desktop work Switch Claude Desktop to work account
claude-switch-desktop personal Switch Claude Desktop to personal account
claude auth status Verify Claude Code auth (restart session first)
claude auth login Re-authenticate if token expired

Directory Layout After Setup

~/
├── .claude -> .claude-work/                        # Symlink to active Claude Code profile
├── .claude-work/                                   # Claude Code: work profile
│   ├── .auth-token                                 # Work OAuth token (600 perms)
│   ├── CLAUDE.md                                   # Instructions
│   ├── settings.json                               # Config (MCP servers, permissions, env)
│   ├── projects/                                   # Work project memory
│   ├── history.jsonl                               # Work conversation history
│   └── ...
├── .claude-personal/                               # Claude Code: personal profile
│   ├── .auth-token                                 # Personal OAuth token (600 perms)
│   ├── CLAUDE.md                                   # Instructions (customized)
│   ├── settings.json                               # Config (customized)
│   ├── projects/                                   # Personal project memory
│   ├── history.jsonl                               # Personal conversation history
│   └── ...
├── .claude-desktop-profiles/
│   ├── .active                                     # Tracks current Desktop profile
│   ├── work/                                       # Desktop: work profile snapshot (~5MB)
│   │   ├── config.json                             # Encrypted work OAuth token
│   │   ├── Local Storage/                          # Work account metadata
│   │   ├── Cookies                                 # Work session cookies
│   │   └── ...
│   └── personal/                                   # Desktop: personal profile snapshot (~5MB)
│       ├── config.json                             # Encrypted personal OAuth token
│       ├── Local Storage/                          # Personal account metadata
│       ├── Cookies                                 # Personal session cookies
│       └── ...
└── Library/Application Support/Claude/             # Claude Desktop data dir (active, files swapped in place)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment