The Friction of AI Identity: Managing Multiple Claude CLI Accounts
The Friction of AI Identity: Managing Multiple Claude CLI Accounts
You are deep in the zone. The architecture diagram is finally clicking together in your head. You tab over to your terminal to ask Claude to scaffold the boring boilerplate, only to realize you are authenticated under your personal account. Worse, you just hit your hourly usage cap.
Now you have to manually log out, trigger a magic email link for your client's Teams account, switch to your browser, wait for the email, click the link, and jump back to the terminal. By the time the authentication succeeds, your mental model of that architecture is completely gone.
Context switching destroys human productivity. We compartmentalize our code environments with Docker and nvm, yet we treat our AI CLI tools as rigid, single-tenant monoliths.
I got tired of the login dance. If I am jumping from a personal Pro account to a startup Teams workspace or an Enterprise client, the CLI should keep up.
Hacking the Config State
Most CLI tools store their auth state in a local JSON file or a hidden directory. Claude Code is no different. Instead of fighting the official auth flow every time I switch hats, I decided to build a state manager that swaps the underlying configuration files on demand.
The idea is simple: sync the current OAuth token state to a named profile, and then symlink or overwrite the active config when a switch is requested.
Here is how I approached the core switching logic in TypeScript:
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
interface ClaudeProfile {
id: string;
email: string;
workspace: string;
plan: 'Pro' | 'Teams' | 'Enterprise';
}
const CONFIG_DIR = path.join(os.homedir(), '.claude_cli');
const ACTIVE_CONFIG_PATH = path.join(CONFIG_DIR, 'auth.json');
const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
async function switchAccount(targetEmail: string): Promise<void> {
try {
const availableProfiles = await fs.readdir(PROFILES_DIR);
const targetProfile = availableProfiles.find(p => p.includes(targetEmail));
if (!targetProfile) {
console.error(`Profile for ${targetEmail} not found. Did you sync it?`);
return;
}
const profilePath = path.join(PROFILES_DIR, targetProfile);
// Overwrite the active auth state with the stored profile state
await fs.copyFile(profilePath, ACTIVE_CONFIG_PATH);
console.log(`Successfully swapped context to: ${targetEmail}`);
// Optional: Read and print the current state
const rawData = await fs.readFile(ACTIVE_CONFIG_PATH, 'utf-8');
const session: ClaudeProfile = JSON.parse(rawData);
console.log(`Active Workspace: ${session.workspace} (${session.plan})`);
} catch (error) {
console.error('Failed to switch profiles:', error);
}
}
// Usage example:
// switchAccount('jordan@example.invalid');The Usage Limit Edge Case
I originally tried wiring this up via slash commands and CLI hooks (!cc-sync-oauth and !cc-switch). It felt elegant. You type a command, and the agent swaps its own brain out.
There is a catch, though. Because of how the agent's lifecycle is managed in memory, swapping the tokens under its feet causes unexpected behavior if the current session hits a usage limit. I initially thought it was a token expiration issue, but debugging proved it happens specifically when you exceed the message cap. The internal hook throws a fit because the active process still holds references to the exhausted session.
I am still looking for a cleaner way to handle the in-memory agent lifecycle when a limit is hit. For now, the most bulletproof method involves killing the process, then swapping the config files via a standalone script before restarting the CLI.
Wrapping Up
Your terminal must adapt to your workflow. Building small, aggressive automations to remove daily friction is the highest ROI work you can do as a developer. If you find yourself doing the same manual setup more than three times a week, script it.