@efoo/ccprofile
v0.1.1
Published
Per-directory Claude Code account routing via CLAUDE_CODE_OAUTH_TOKEN, direnv, and macOS Keychain
Downloads
309
Maintainers
Readme
ccprofile
Per-directory Claude Code account routing via CLAUDE_CODE_OAUTH_TOKEN, direnv, and the macOS Keychain.
ccprofile lets you run multiple Claude Code accounts in parallel — one per terminal, one per project — with zero manual switching. It never touches Claude Code's own Keychain entry, so there is no global "active account" to corrupt.
🤖 Install with an AI agent
Paste this prompt into Claude Code, Cursor, or any coding agent:
Install and configure ccprofile by following the instructions here:
https://raw.githubusercontent.com/efoo-team/ccprofile/main/docs/install-for-agents.mdThe agent will check prerequisites (direnv, hooks), install the CLI, ask whether you want shell completion, and walk you through registering accounts — only the browser OAuth step needs your hands.
Why
Claude Code stores its OAuth credentials in a single macOS Keychain entry, shared across every CLAUDE_CONFIG_DIR profile (#20553). Switcher-style tools work around this by swapping that entry in place — which breaks down the moment two sessions with different accounts run at the same time (in-session token refresh writes the old account back).
ccprofile takes the declarative route instead:
- Each account's long-lived OAuth token (
claude setup-token, valid ~1 year) is stored in the Keychain under ccprofile's own namespace — one entry per profile, no sharing, no swapping. ccprofile linkwrites a self-contained.envrcthat exportsCLAUDE_CODE_OAUTH_TOKENstraight from the Keychain. direnv activates it when you enter the directory. No node/npx in the hot path.CLAUDE_CODE_OAUTH_TOKENoutranks the stored login in Claude Code's documented auth precedence, so linked directories route to their account and everywhere else falls back to your normal/login.
Auth state lives in each process's environment — parallel sessions cannot interfere with each other by construction.
Requirements
- macOS (tokens are stored in the Keychain)
- Claude Code with a Pro / Max / Team / Enterprise subscription (
claude setup-tokenrequires one) - direnv —
brew install direnv+ the shell hook (fish:direnv hook fish | source) - Node.js >= 20 (only for running ccprofile itself)
Install
npm install -g @efoo/ccprofile # provides the `ccprofile` command
# or run ad hoc:
npx @efoo/ccprofile --helpQuick start
# 1. Register an account (launches `claude setup-token`, stores the token in the Keychain)
ccprofile add work --email [email protected]
# 2. Route a project to it — run inside the project directory…
cd ~/src/my-project
ccprofile link work
# …or point at a directory from anywhere:
ccprofile link work ~/src/my-project
# 3. Done — any claude launched in that directory (and below) runs as "work"
claude # /status shows "Auth token: CLAUDE_CODE_OAUTH_TOKEN"Repeat with ccprofile add personal etc. Different terminals in different directories run different accounts concurrently.
Commands
| Command | Description |
| --- | --- |
| ccprofile add <name> | Register a profile. Flags: --email, --expires-at <iso>, --token <token>, --force |
| ccprofile list [--json] | Profiles with token presence and expiry countdown |
| ccprofile link <name> [dir] | Write the managed .envrc block and direnv allow |
| ccprofile unlink [dir] | Remove the managed block (deletes .envrc if nothing else remains) |
| ccprofile token <name> | Print the stored token to stdout (for scripting — handle with care) |
| ccprofile remove <name> | Delete the profile and its Keychain entry |
| ccprofile doctor [dir] | Diagnose overriding env vars (ANTHROPIC_API_KEY etc.), apiKeyHelper, expiry, token liveness, broken links. --offline skips the server probe |
| ccprofile completion <shell> | Print a completion script for fish, zsh, or bash |
Shell completion
Subcommands, flags, and registered profile names are all tab-completable:
# fish
ccprofile completion fish > ~/.config/fish/completions/ccprofile.fish
# zsh (place _ccprofile somewhere in $fpath, then restart zsh)
ccprofile completion zsh > "${fpath[1]}/_ccprofile"
# bash
echo 'eval "$(ccprofile completion bash)"' >> ~/.bashrcccprofile link <TAB> completes profile names by calling the hidden ccprofile _profiles helper, which only reads ~/.ccprofile/config.json (never the Keychain).
How it works
~/.ccprofile/config.json profile metadata: email, expiry, keychain ref (no secrets)
macOS Keychain service "ccprofile", one entry per profile (the secrets)
<project>/.envrc managed block, generated by `ccprofile link`:
# >>> ccprofile managed >>>
# profile: work
export CLAUDE_CODE_OAUTH_TOKEN="$(security find-generic-password -w -s 'ccprofile' -a 'work' 2>/dev/null)"
# <<< ccprofile managed <<<Notes:
- Tokens are written to the Keychain via
security -i(stdin), so secrets never appear inpsoutput. - The
.envrcblock is self-contained: direnv re-evaluates it on every directory entry, and it must stay fast and dependency-free. ccprofile is only needed for CRUD operations. - Add
.envrcto your project's.gitignore— it is machine-local.
Limitations — the price of parallel accounts
ccprofile is built on claude setup-token, whose long-lived tokens are deliberately scoped to inference only ("for security reasons", per Claude Code's own /doctor). Several conveniences of a normal /login session are therefore unavailable in linked directories:
- No account identity introspection. The token cannot answer "whose token is this?" — the OAuth profile endpoint rejects it (
user:profilescope missing, see #11985). The--emailyou record is a self-declared label, not verified. Verify identity once, at registration time: make sure the browser is logged into the intended claude.ai account beforeclaude setup-token, then send a couple of prompts from a linked directory and confirm on claude.ai (web) that the intended account's usage moved. /status→ Usage tab shows no plan utilization in token-authenticated sessions (same scope restriction). Check usage on claude.ai instead.- Remote Control is unavailable in token-authenticated sessions; it requires a full-scope login token.
- Tokens last up to 1 year but can die earlier (password change, logout-all). The recorded expiry is a hint, not a guarantee —
ccprofile doctorprobes the server and tells live tokens apart from revoked ones. claude --baredoes not readCLAUDE_CODE_OAUTH_TOKEN.- Subscription accounting: from June 15, 2026,
claude -p/ Agent SDK usage on subscription plans draws from a separate monthly Agent SDK credit. - direnv only sees shell-launched processes. Apps started outside a hooked shell (GUI launchers) bypass the routing.
- Higher-precedence auth wins silently.
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,apiKeyHelper, and Bedrock/Vertex/Foundry env vars all outrank the token —ccprofile doctorflags them. - macOS only for now (the token store is the macOS Keychain).
Development
pnpm install
pnpm build # tsc → dist/
pnpm test # vitest
node dist/index.js --helpLicense
MIT
