claudefob
v0.3.6
Published
Unofficial CLI to store multiple Claude Code OAuth tokens in your OS keystore and switch between them. Not affiliated with Anthropic.
Maintainers
Readme
claudefob
claudefob is an unofficial, community project. It is not affiliated with, endorsed by, or supported by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic.
A cross-platform CLI that stores multiple Claude Code OAuth tokens in your OS keystore (macOS
Keychain, Linux Secret Service, Windows Credential Manager) and activates exactly one of them as
CLAUDE_CODE_OAUTH_TOKEN — so switching accounts is claudefob use work instead of hand-editing
export CLAUDE_CODE_OAUTH_TOKEN=… in a shell rc file.
Install
npm i -g claudefobThen wire up shell integration. claudefob init writes the hook to a file it maintains
(~/.config/claudefob/hook.sh) and prints a three-line block that sources it — so upgrades
rewrite only that script and your startup file never needs editing again. It takes effect in any
terminal you open afterwards.
# >>> claudefob >>>
[ -f "$HOME/.config/claudefob/hook.sh" ] && . "$HOME/.config/claudefob/hook.sh"
# <<< claudefob <<<To activate it in the terminal you are already in, without opening a new one:
source ~/.config/claudefob/hook.shSource it — do not run it. The script is not executable on purpose: executing it would spawn a child shell that exits immediately, changing nothing in your session.
init prints that exact line for you. It is needed only once, at install time — a child process
cannot modify its parent shell, and with >> the block goes to your rc file rather than to the
shell.
Prefer everything in one place? claudefob init --inline emits the whole block instead and writes
no extra file — you then re-run it after each upgrade.
claudefob detects your shell from the parent process, so --shell is never needed. You only
choose which file to append to:
# macOS, zsh (the default shell since Catalina)
claudefob init >> ~/.zshrc
# Linux, bash (the default on most distros)
claudefob init >> ~/.bashrc
# macOS, bash — terminals are login shells here, so .bashrc is not read
claudefob init >> ~/.bash_profile
# fish, either platform
claudefob init >> ~/.config/fish/config.fish# Windows, PowerShell 5.1 or 7. The profile usually does not exist yet, so create it first.
if (!(Test-Path $PROFILE)) { New-Item -Type File -Path $PROFILE -Force }
claudefob init | Add-Content $PROFILEPass --shell posix|fish|powershell only to override detection — for example to read another
platform's instructions from your own machine.
Which rc file?
| Shell | File | Loaded for | Use it? |
|---|---|---|---|
| zsh | ~/.zshrc | interactive shells | Recommended. Fires for every terminal you type in. |
| zsh | ~/.zprofile | login shells | Works on macOS, where every Terminal tab is a login shell. Missed by a plain zsh subshell. |
| zsh | ~/.zshenv | every zsh, scripts included | Only if you need the token outside interactive terminals. Adds a keystore read to every script you run. |
| zsh | ~/.zlogin | login shells, after .zshrc | No reason to use it here. |
| bash | ~/.bashrc | interactive non-login shells | Recommended on Linux, where terminals are non-login. |
| bash | ~/.bash_profile | login shells | Recommended on macOS, where every terminal is a login shell. Most setups source .bashrc from here. |
| bash | ~/.profile | login shells, read by several shells | Fallback when .bash_profile does not exist. |
| fish | ~/.config/fish/config.fish | every fish session | The only file fish needs. |
| PowerShell | $PROFILE | every session of that PowerShell version | 5.1 and 7 have separate files — install into each if you use both. |
Windows notes: if Get-ExecutionPolicy reports Restricted, profiles never load and this setup
silently does nothing; fix it with Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
cmd.exe is not supported — it has no profile mechanism and no command substitution. Use PowerShell. Git Bash and WSL are bash, so use the bash rows above.
Run claudefob guide any time for platform-specific install steps, a scan of which rc files
already have the block installed, and removal instructions.
Quick start
claudefob add work --description "day job" # prompts for the token, echo disabled
claudefob # picker: choose a token to activate
claudefob status # confirm it's active
claudefob stop # deactivateCommand reference
| Command | What it does |
|---|---|
| claudefob add <name> [--description <text>] | Store a new token. Prompts for it (no argv, no echo). |
| claudefob list [--json] | List stored tokens. Metadata only — never touches the keystore or prompts for auth. |
| claudefob show [<name>] | Reveal a token after OS-level authentication. Prints to stderr only. |
| claudefob status [--json] | What's active, whether the shell hook is installed, and whether this shell is in sync. |
| claudefob / claudefob use [<name>] | Activate a token (interactive picker if no name given). |
| claudefob stop | Deactivate. Idempotent. Never touches ~/.claude.json. |
| claudefob remove [<name>] [--yes] | Delete a token from the keystore and metadata. |
| claudefob init [--shell <s>] | Print the shell hook block to stdout, for you to append to an rc file. Never writes a file itself. |
| claudefob guide [--shell <s>] | Read-only: install steps, rc file scan, removal instructions. |
| claudefob update | Update claudefob using whichever package manager installed it. |
| claudefob export --shell <s> | Internal — called by the shell hook at every shell startup. Not meant to be run by hand. |
ls and rm are accepted as aliases for list and remove. -d is short for --description,
-y for --yes.
--help and --version report on stderr, not stdout. stdout is reserved for the shell code
the wrapper function evaluates, so a version number printed there would be run as a command.
Environment variables
| Variable | Effect |
|---|---|
| CLAUDEFOB_NO_UPDATE_CHECK=1 | Disable the update check. Also honours NO_UPDATE_NOTIFIER and CI. |
| CLAUDEFOB_NO_HOOK_CHECK=1 | Suppress the "shell integration is not installed" warning. |
| NO_COLOR=1 | Disable colored output. |
| CLAUDEFOB_DEBUG=1 | Print diagnostics that are otherwise swallowed. |
| XDG_CONFIG_HOME | Where store.json lives (macOS/Linux). |
Update checks
claudefob checks npm for a newer version at most once every 24 hours, in a detached background
process, and prints a notice from cache on a later run. It never blocks a command, and it never
runs on the claudefob export path that your shell calls at startup. The request goes to
registry.npmjs.org and exposes your IP address to npm; disable it with
CLAUDEFOB_NO_UPDATE_CHECK=1.
How it works
A child process cannot change its parent shell's environment. So instead of trying to, claudefob
writes shell code to stdout and lets the shell eval it; every human-facing message — tables,
prompts, confirmations, the revealed token from show, errors — goes to stderr. That split is
what makes $(claudefob use work)-style activation safe: nothing but shell code ever appears on
stdout, and nothing you'd want to keep secret is ever captured by a stray $(...).
The hook block installed by claudefob init does two things: it evals claudefob export once at
every shell startup (so activation survives opening a new terminal), and it wraps the claudefob
command in a shell function so that running claudefob use ... from an already-open shell also
patches that shell's environment immediately.
Linux: make sure your keyring is persistent
Run claudefob guide on the machine for the full recipe. In short: on a headless Linux box — a
devbox reached over SSH, a container — there is often no unlocked login keyring. libsecret then stores secrets in the Secret Service session collection, which
lives in memory. Everything works until the machine reboots, and then every secret is gone, while
claudefob's metadata survives: claudefob status still names an active token and activating it
fails with "no longer in the keystore".
claudefob warns about this when you run claudefob add. To check by hand:
ls -la ~/.local/share/keyrings/ # a login.keyring or *.keyring means storage survives a rebootOn Ubuntu or Debian, create a login keyring that PAM unlocks automatically:
sudo apt-get install -y gnome-keyring libpam-gnome-keyring
# log out and back in so PAM creates and unlocks ~/.local/share/keyrings/login.keyringOver SSH with no desktop session, unlock one per session instead:
gnome-keyring-daemon --unlock --components=secretsKWallet works too — it exposes the same Secret Service API and keeps its own persistent store.
Security model
Protects against: secrets ending up in dotfiles, dotfile backups, Time Machine snapshots,
dotfile repos, or shell history; shoulder-surfing while a token is on screen; someone sitting down
at your already-unlocked terminal and reading a token out with show.
Does not protect against: any process already running as you. It can read the keystore
directly through the same OS API claudefob uses, and it can read an active token straight out of
its own environment. The show authentication gate is a speed bump against a person, not a
sandbox against code — there is no bypass, and there never will be one, but it was never designed
to stop malware running as your user.
Tokens are never written to store.json, argv, or a log file. Password prompts disable terminal
echo. store.json is written with mode 0600. show prints only to stderr.
Platform notes
- Linux needs a running Secret Service (gnome-keyring or kwallet). If
addfails because none is running, log in to a real desktop session, or bring one up headless:dbus-run-session -- bash -c 'echo -n "" | gnome-keyring-daemon --unlock --daemonize --components=secrets; exec $SHELL' - Windows PowerShell profiles live at two separate paths for 5.1 and 7+;
claudefob guideprints both. If the profile silently doesn't load, your execution policy is probablyRestricted— fix it withSet-ExecutionPolicy -Scope CurrentUser RemoteSigned. - cmd.exe is not supported. It has no real profile mechanism and no command substitution.
claudefob init --shell cmderrors and points you at PowerShell instead.
The ~/.claude.json onboarding fix
Claude Code prompts for interactive auth at startup whenever hasCompletedOnboarding is unset in
~/.claude.json — even with CLAUDE_CODE_OAUTH_TOKEN already set. When you activate a token
(claudefob use or bare claudefob), claudefob checks that file and, only if the flag is missing
or false, patches it to true with a minimal in-place edit. Before writing anything it verifies
the round trip: the only difference between the old and new file content must be that one flag. If
verification fails for any reason (comments, an unexpected value, duplicate keys, anything it
can't confidently reason about), it writes nothing and prints the one-line manual fix instead.
No backup file is ever created — the round-trip check is the safety net. claudefob stop never
opens this file at all.
Keeping terminals in sync
Environment variables are per-process, so a switch made in one terminal cannot reach into another one that is already open. The hook block handles this with a prompt hook: any open terminal picks up the change at its next prompt, with no restart and no command to run.
The check costs nothing on the unchanged path — [ -nt ] and : > are shell builtins, so
claudefob only actually runs on the first prompt after store.json changes.
Deactivation propagates too, but only for a token claudefob set. If you exported
CLAUDE_CODE_OAUTH_TOKEN yourself somewhere else, claudefob leaves it alone — it tracks its own
work with a CLAUDEFOB_APPLIED marker and clears the variable only when that marker is present.
One thing this cannot fix: a Claude Code session that is already running. Its environment was
copied when the process started, so it keeps the token it launched with until you exit and start
claude again. No tool can change a running process's environment.
If you installed the hook before this release, re-run claudefob init to pick up the sync block.
License
MIT — see LICENSE.
