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+)
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
~/.claudeprofile 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>.
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/.
- Two profile directories:
~/.claude-work/and~/.claude-personal/ ~/.claudeis a symlink pointing to the active profile- Each profile stores its OAuth token in
.auth-token - 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
~/.claudesees the active profile - Safe — both directories always exist intact
# 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\", \"?\")}')
"IMPORTANT: Do NOT use
claude auth logout— it revokes the token server-side, permanently invalidating the token you just exported. Useclaude auth logindirectly 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.jsonsecurity 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 statusYour 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 statusOption 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-tokenThen 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}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.
#!/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 ;;
esacclaude-switch status # Show active profile
claude-switch work # Switch to work
claude-switch personal # Switch to personalAfter switching, restart any running Claude Code sessions.
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.
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.
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 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."# 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."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."Save as claude-switch-desktop on your PATH, then chmod +x it.
#!/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 ;;
esacclaude-switch-desktop status # Show active profile
claude-switch-desktop work # Switch to work
claude-switch-desktop personal # Switch to personalAfter switching, launch Claude Desktop with open -a Claude.
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.
| 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 |
~/
├── .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)