Changed
- Claude settings now follow your live configuration across account switches; only authentication comes from the selected profile (#115). Before,
caam activate clauderestored the profile's savedsettings.json, so any Claude Code setting changed since that backup was reverted. Vault activation, isolated profiles and shallow profiles now share one policy. Permissions, auto-mode rules, hooks, MCP servers, plugin enablement, model and effort choices and other non-auth settings come from your current~/.claude/settings.jsonand~/.claude.json(or the files in$CLAUDE_CONFIG_DIR), and a rule you delete there is also gone after the next activation. Authentication settings such asapiKeyHelperandforceLoginMethod, and the wholeenvobject, stay with each profile. Each account also keeps its own project history and session IDs in.claude.json. The shallow-profile settings sync used to never delete anything; deletions now carry over there too, and a shallowsettings.jsonthat links to your user settings is replaced with a private file before launch. If you want each account to keep its own settings, set"claude_settings": {"mode": "per-profile"}in~/.config/caam/config.json.profile_keysmakes individual keys per-account andshared_env_keysshares individual environment variables. Malformed settings now stop the activation or launch instead of being copied. Details are indocs/CLAUDE_SETTINGS.md. - Desktop alerts are on by default and are now delivered. caam never read the
alertssection of~/.caam/config.yamlbefore, and bothalerts.enabledandalerts.notifications.desktopdefault to true. After upgrading,caam runposts a desktop notification when it switches accounts after a rate limit. The daemon posts one when a refresh fails, a provider rejects a login, a credential check fails, or a Cursor session is expiring or has expired. Identical daemon alerts are suppressed for an hour. Turn desktop alerts off withcaam config set alerts.notifications.desktop false, or send alerts to Slack or Discord withalerts.notifications.webhook.caam notify testsends a test alert through each enabled channel. caam serveand the local API now default to port 7892. The old default, 7891, is the local auth agent's port, so the two could not run on the same machine. Update clients and bookmarks that assume 7891, or pass--port.- API backups no longer replace an existing profile unless the request asks to. A backup request to the local API for a profile name that already exists now returns "profile already exists" unless the JSON body includes
"overwrite": true.caam backupon the command line is unchanged. caam runreruns headless commands after a rate limit, using your retry settings.--max-retrieswas accepted but never read, and thewrapsettings in~/.config/caam/config.json(max_retries,initial_delay,max_delay,backoff_multiplier,jitter,cooldown_duration, plus per-provider overrides) were not applied to native headless runs such asclaude -porcodex exec. Both now apply to headless reruns and to interactive handoffs. The defaults are three retries (the flag used to show 1), a 30-second first delay, a five-minute maximum delay and jitter, and aRetry-Afterthe CLI prints is honored. Each retry runs the whole command again, and anything an earlier attempt did stays done. Input redirected from a file (or/dev/null) is captured and replayed on each attempt, up to 16 MiB. A pipe is passed straight through, so a pipe that stays open never blocks the launch, and a retry after the CLI has read piped input is refused. Per-provider0andfalsevalues now take effect, and invalid settings stopcaam runbefore it activates an account.- Scheduled vault backups have new file names, and retention only deletes archives the daemon recorded. Scheduled archives are now named
caam_auto_backup_<time>_<id>.zip. Retention used to delete anycaam_export_*.zipin the backup location, including files made by hand withcaam bundle export. It now prunes only automatic backups whose size and SHA-256 still match the daemon's record. Scheduled archives made by earlier versions are kept, so delete oldcaam_export_*.zipfiles yourself when you no longer need them. - Rotation applies cooldown and policy checks even when only one profile is left.
caam next --forceandcaam activate --auto --forceoverride the cooldown only. System snapshots such as_originaland_backup_*are never rotation candidates. - Distributed recovery setups made before this release need
caam setup distributedrun again. Earlier setups could not work end to end (see Fixed). Setup now writes a per-host token into~/.config/caam/coordinator.json(mode 0600) and into the agent config. It also writes"auth_timeout": "5m", up from 60s.caam update --remoteskeeps a host's existingcoordinator.json, so an old 60s value stays until you re-run setup or edit the file. Re-running setup keeps existing tokens unless you pass--rotate-tokens. - The auth agent signs in from its own persistent Chrome profile. With
chrome_profileunset (the setup default), the agent used a throwaway temporary profile, so Google sessions never carried over between flows. It now uses~/.local/share/caam/auth-agent-chrome($CAAM_HOME/data/auth-agent-chromewhenCAAM_HOMEis set). Runcaam auth-agent signinonce to sign that profile in, thencaam auth-agent accounts add <email>...to list the accounts to rotate through. Without a list, every recovery uses whichever account Google or Claude offers first. - Dependencies: bubbletea 1.3.10, bubbles 1.0.0, glamour 1.0.0, cobra 1.10.2, pflag 1.0.10, fsnotify 1.10.1, sftp 1.13.11, modernc.org/sqlite 1.60.1, and
golang.org/x/crypto0.57.0,x/sys0.48.0 andx/term0.46.0. chromedp stays at 0.14.2 on purpose: versions 0.17 to 0.20 redesign its API, and caam drives OAuth through it.
Added
caam keepaliverenews idle Claude and Grok logins in their live homes (#119). Claude and Grok renew OAuth tokens only through their own CLIs, so an idle account could expire and drop out of usage-aware routing. When a token is within 2 hours of expiry,caam keepaliveruns a minimal native command in that account's live home and checks the credential afterward. For Claude that is a one-word Haiku prompt with no tools, hooks or MCP servers, which uses a small amount of Claude usage; for Grok it isgrok models. It never renews from a saved vault copy, whose refresh token may already be spent, and it reports a Claude login held in the macOS keychain as blocked because it cannot verify it. Afterward, a strictly newer credential for the same account is copied into the matching vault profile. Options are--dry-run,--json,--ttl,--min-gapand--timeout, and--print-systemdprints a user service and a timer that runs every 30 minutes.caam limits grok|cursor --profile NAME --source livereads the quota of the login the CLI is using now (#114). A vault snapshot goes stale once the native CLI rotates its login. The live read first checks that the live credential belongs to the named account, using the Grokuser_idor Cursor JWTsub(email only as a fallback), and fails if the account changes during the fetch. Grok quota reads use a copy with refresh tokens removed, so they cannot spend the CLI's refresh token. Neither credential source is written.- Vault profiles take tags and descriptions.
caam tag add|remove|list|clearandcaam profile describeused to work only on isolated profiles and failed with "profile not found" on the vault profiles thatcaam lslists. They now work on both, and the labels live in the profile'smeta.jsonand carry over when the profile is backed up again.caam lsshows each profile's description and tags,caam ls --tagfilters on them,--jsonaddsdescriptionandtags, andcaam addaccepts--description/-dand--tag. The TUI detail panel shows a Tags row, and profile search matches tags. caam lsshows each profile's type (#110). A new TYPE column readsvault, orvault+isowhen an isolated profile of the same name exists, and isolated-only profiles are listed after the table.--jsonaddstype,isolatedandisolated_onlyand keeps the same rows. When you pass a vault profile tocaam execorcaam login, they now say so and name both ways forward, instead of printing "profile X not found".- The activity log records refreshes, refresh failures, rate limits and automatic switches. It only received activate and deactivate events before, so history and per-profile error counts never showed them. A switch made by
caam runcounts as an activation of its target, so rotation's "last activated" signal includes it. caam daemon statusshows scheduled backups, andcaam daemon auto-backupconfigures them. Status reports the schedule, the last backup and archive, the next run, the count and the last error, also in--json.caam daemon auto-backupshows the schedule or changes it with--enable,--disable,--interval(at least 1h),--keep(at least 1) and--location. Restart a running daemon after changing it.caam robot docsprints machine-readable CLI documentation.caam robot docs [all|commands|exit-codes|errors|schemas]emits every command with its flags, examples and subcommands, the exit codes, the robot error codes, and JSON Schemas (draft 2020-12). The schemas cover the robot envelope, the data of each robot command, andstatus,lsandactivate --json.caam robot docs schemas lsprints a single schema.- The web dashboard in
web/dashboardruns on the local caam API. It used to show hard-coded numbers and buttons that did nothing. It now shows logged-in tools, saved profiles, profiles that need attention, coordinator health, and recorded activity and cooldowns. It can also activate a profile or save the current login after you confirm.caam serve --dashboard-url http://localhost:3000prints a link that carries the API address and token in the URL fragment; the dashboard keeps the token in tab memory and removes it from the address bar. A newGET /api/v1/activityreturns recent events with secret-like fields removed./api/v1/usagereports recorded caam activity (activations, errors and session time), not provider API calls./api/v1/coordinatorsreports the configured coordinators instead of an empty list. - New commands manage the distributed recovery agent.
caam auth-agent service install|uninstall|statusruns the agent at login (launchd on macOS,systemd --useron Linux).caam auth-agent signinopens a visible Chrome window on the agent's profile to add Google accounts and sign in to Claude.caam auth-agent accounts [add|remove] <email>...lists and edits the rotation, showing when each account was last used and whether it is held at its limit; changes restart an installed agent service. caam setup distributed --hostsets up coordinator hosts without a WezTerm config. Setup only discovered WezTermssh_domains, so tmux users could not use it.--host [user@]host[:port]is repeatable and accepts~/.ssh/configaliases.caam update --remotesupgrades every coordinator to the local caam version, with rollback. It backs up each host's binary and systemd unit, redeploys, and restores both if the upgraded coordinator fails its status check.--rollbackrestores the previous binary and unit and verifies them, and--dry-run,--forceand--jsonare available. Setup from a machine of another platform (for example a Mac deploying to Linux) now installs the release that matches the local version instead of reusing whatever caam the host had.- The daemon can announce new caam releases (opt-in). Set
daemon.update_check.enabled(withinterval, at least 1h, andchannel) and the daemon announces each new version once through your alert channels, with the commands to update this machine and the coordinators. It never installs anything. caam doctorchecks distributed auth recovery end to end. When an agent config exists, doctor reports on the config, Chrome, whether the agent's profile is signed in to Google, whether the agent runs as a service, and the account rotation (a warning when no accounts are listed). It also lists accounts held at their usage limit and when each hold ends, and checks that each coordinator is reachable over SSH, accepts its token and runs the same caam version. Each problem names the command that fixes it. JSON output has this section underdistributed.- Recovery skips accounts at their usage limit and stops when all of them are. On a limit, the coordinator reads which Claude account the pane is signed in with and the reset time from the banner, and sends both with the login request. The agent holds that account until the reset (5 hours when no reset is shown, at most 8 days) and does not pick it in the meantime. When every listed account is held, no browser opens. The coordinator closes Claude Code's login screen and starts no login in that pane until the first hold ends.
/statusandcaam auth-coordinator statusshow each pane's limited account, its reset and the hold. - Coordinators retry a failed pane login. A pane whose login failed used to stay failed until someone typed
/login. The coordinator now retries up to 2 times per rate-limit episode, then leaves the pane for manual recovery./statusshows each pane's retries, and/healthand/statusreport the coordinator's caam version.
Fixed
- Codex quota reads ask about the right ChatGPT workspace (#111). caam did not send the
ChatGPT-Account-Idheader that the Codex CLI sends, so a login with several workspaces could get figures for a different one. One Pro login read 86% used where Codex showed 43%, and rotation treated it as nearly spent. caam now takes the id from the access token'schatgpt_account_idclaim. - Cursor's "cursor models" window no longer reads 100% once included spend is used up (#112). caam computed it from included spend, which stops at the limit while Cursor keeps serving its own models from bonus credit. It now uses
autoPercentUsed, the figurecursor-agentshows as "Auto", when the response has a valid one. - A freshly reset Grok billing period reads as 0% used (#116). Grok leaves the usage fields out when they are zero. caam now reads that as zero only when every usage field is missing and the period bounds are consistent and contain the fetch time; malformed or partial data stays unknown. Routing and
--precheckcan then switch to the freshly reset account. - Cursor session logins show when they expire, and caam warns before they lapse (#118). caam now parses the expiry of Cursor session logins, which cannot be refreshed and must be logged in again when they run out. The deadline shows in
caam ls,status,doctor,verifyand robot output, with a warning seven days ahead and the login command to run for native, vault and isolated profiles. The daemon warns once per credential and no longer tries to refresh Cursor credentials. Stored Cursor API keys are treated as renewing themselves.caam runchecks the live credential before an interactive launch and stops an expired session with a "log in again" message; a renewable login whose access token has lapsed still launches. - Refreshes no longer replay a stale Codex refresh token, and skipped refreshes are reported (#117). Before it announces or attempts a refresh, caam checks the current credentials, and it skips a Codex vault token when the same account has a newer live login. Providers caam cannot refresh, OpenCode included, return an explicit "skipped" result instead of a false success. This applies to the daemon, the auth pool,
caam refresh(including--forceand--dry-run) and the TUI. - Robot commands reject profiles that hold no usable credentials (#113). Robot activation and routing now check the saved credentials, without writing, before restoring anything. Claude snapshots that carry only identity, or are incomplete, null or malformed, are rejected before live credentials, settings or the keychain change. Such profiles are left out of robot recommendations, and
caam robot act refreshreports unsupported, skipped and failed results as such. The CLI uses the same saved-profile check. - Account switches keep the live login.
activate,next, robot commands, workspaces,caam runprechecks and handoffs, the API and the TUI now share one switch operation, which checks the incoming snapshot before writing. A token the native CLI rotated is saved back to its profile only after the account and freshness match. Activating an older snapshot of the current account keeps the newer live credential and reportskept_livein JSON. A switch that fails partway reports an error instead of success. - Health and rotation follow the current credential. Saved health is recomputed from the vault, so replacing a credential no longer leaves a stale expiry or session deadline, and accounts that need a new login are excluded from every rotation strategy and cooldown override. The auth pool picks up added and deleted vault profiles before each refresh, and a manual pool refresh reports real failures instead of leaving accounts stuck as refreshing.
- Codex and Gemini API keys and OAuth logins no longer mix. For a profile set to use an API key, health, validation, rotation, the pool and the daemon use that key's state. Leftover OAuth files on disk are not probed or refreshed, and an API key from the environment is not used to rescue an expired browser login. Refreshes are tied to the credential they started from: one replaced mid-refresh is skipped, and the result is written only to copies that have not changed. The daemon can renew an expired access token that still has a refresh token.
caam backupreplaces a saved profile only with a complete, validated snapshot, and keeps the old one. Credential files that no longer exist are left out of the new snapshot, so an old OAuth login cannot linger after you move a profile to an API key or helper. The previous directory stays recoverable at theprevious_snapshotpath in the newmeta.json, and descriptions and tags carry over. This also works when the vault is on another mounted volume.- Imports, clones and discovery no longer lose accounts.
caam auth import --force,caam initandcaam profile clone --forcevalidate before replacing a profile, keep the previous one, and print where it is.caam watch(and--once) updates a named profile only with a newer, complete credential for the same account, saves logins it cannot match as separate profiles, and recovers when a watched directory is missing or replaced. Clones copy credential files as private files instead of links, and a Gemini Vertex ADC profile includes its gcloud credential directory. - Claude switches no longer leave the previous account's login behind. When the target profile has no OAuth file, keychain item or Desktop token cache, the outgoing one is removed instead of being left as a fallback; Desktop preferences such as theme stay. Settings, credentials and the keychain item are staged together and rolled back when a step returns an error. If a recovery copy has to be kept (
*.rollback.*, mode 0600, contains credentials), the error names it. This is recovery from returned errors, not crash-safe switching. - On macOS, Claude activation works without a login keychain. caam now tells an unavailable login keychain apart from one with no Claude item, and falls back to file-backed credentials only in the first case. Denied keychain access still fails.
caam execchecks the next account again after a retry wait. An account chosen before the backoff could expire during it.caam run --quietno longer crashes on its first account switch.caam runhandoffs submit the login command. The login command (/login,/auth,codex login) was typed with a line feed, which Claude Code, Codex and Gemini read as Ctrl+J rather than Enter, so it sat unsubmitted. It now ends with a carriage return. For Claude, the handoff first sends Esc to close the usage-limit menu, so the Enter cannot pick a paid "usage credits" option.- caam keeps the rest of Codex's
config.tomlintact when it sets file credential storage. It matchedcli_auth_credentials_storeonly in double quotes and also inside nested tables. It now finds the top-level setting in any quote style, replaces only its value, and refuses to write a file it cannot read unambiguously. - Distributed auth recovery works end to end. Setup gave the agent coordinator URLs the coordinator did not listen on, provisioned no token, wrote a systemd unit with an unexpanded
~, and deployed over a second SSH connection that only read~/.ssh/id_ed25519. Codes were fire-and-forget, and a later response could overwrite an accepted one. Now the coordinator keeps the first valid code and acknowledges it, and agents redeliver until it does, so each code is pasted once.caam robot status --include-coordinatorsandcaam auth-coordinator statusquery the configured coordinators with their tokens and SSH tunnels instead of a hardcodedlocalhost:7890. - Coordinators keep working after boot and logout. The
autobackend now follows whichever of WezTerm or tmux answers, instead of deciding once at startup. The systemd unit carries your login shell's PATH, so a multiplexer in~/.local/binor/optis found, and setup reports a missing multiplexer or disabled lingering with the fix. Uploaded binaries are checked against their SHA-256 before they replace the installed one; a cut-short upload used to install a truncated binary. Hosts without systemd are refused before anything is installed. Re-running setup restarts an installed agent service, and deployment has its own 15-minute time limit. - The coordinator recognizes every Claude Code usage-limit banner, but only on a session that has stopped. It now matches "Claude usage limit reached", "5-hour / Weekly / Opus limit reached", Claude Code 2.1's named limits ("You've hit your session limit · resets 3pm", and the weekly, Opus, Sonnet, Fable, usage credit and spend limits) and "You're out of usage credits", still requiring the reset on the same line. The newest message on screen decides a pane's state, so old scrollback no longer hides a fresh banner, and a session showing its working spinner is never treated as stopped.
caam wezterm recover,caam wezterm login-alland thecaam rundetector use the same patterns. - Keystrokes typed into panes do what they should. Each line ends with a carriage return (Enter) instead of a line feed, which Claude Code treats as a newline inside its input, so
/login, the method choice, the code and the resume prompt used to sit unsubmitted. Esc closes the usage-limit menu before/login, so Enter cannot pick "Switch to usage credits". Ctrl+E Ctrl+U clears the prompt line before/loginand before the resume prompt, so a prefilled "continue" is not sent along with them (Ctrl+Y restores it). The login method is chosen with the digit alone, because Claude Code acts on the keypress and the extra Enter landed in the code prompt. - OAuth URLs reach the agent whole. tmux capture joins soft-wrapped lines, and URLs that WezTerm or Claude Code's own layout split across lines are joined back together. The newest URL on screen is used, without a trailing ANSI escape, and
claude.comhosts are accepted. - Logins finish and sessions resume. "Login successful" and "Successfully logged in" count as a finished login, and the "Press Enter to continue" screen is dismissed before the resume prompt is typed (also in
caam wezterm recover --auto). A restarted coordinator picks up panes already at the login menu or the code prompt. A lost login-method keystroke is resent, at most 3 times. A login request that no agent has fetched now waits for the agent (for example a sleeping laptop) instead of expiring after 60 seconds and abandoning the pane; the auth timeout runs from when the agent claims the request. - The compaction reminder fires on Claude Code 2.1. Its notice, "Conversation compacted (ctrl+o for history)", did not match the pattern.
- The auth agent takes the authorization code from the OAuth callback. It scraped pages for short uppercase codes, so it could miss Claude Code's long
code#stateor send a token-like string from a Google page into the session as a bogus code. The callback URL is now the source, and page text is read only on Claude's code pages. - The browser flow no longer waits out its 90-second deadline on a missing selector. Every recovery on Google's account chooser, and on consent pages without a submit button, used to fail this way. An expired claude.ai session now continues through Google, and a flow that ends on a sign-in page names the page and points at
caam auth-agent signin. - The auth agent rotates Claude accounts. Claude's authorize page approves whichever Claude account the browser is signed in to, so every recovery re-approved the same account, often the one at its limit. The agent now reads the account the page names and, when it is not the chosen one, clears only the claude.ai cookies and signs in through Google with the chosen account. Browser flows run one at a time (concurrent flows on one Chrome profile failed), and an account is marked used as soon as its flow ends, so back-to-back requests rotate.
--strategy randomnow picks at random; it always took the first account. - Chrome is closed cleanly, so its sessions are saved. Ending a flow killed Chrome, which lost cookies it had not yet written, such as a new Claude session or Google's rotated session cookies.
- A hung coordinator no longer delays polling of the others. One unreachable host stretched the 2-second poll of every coordinator to that host's SSH timeout.
- Sync pushes onto existing remote profiles succeed. Pushing a file that already existed on an OpenSSH remote failed, because a plain SFTP rename does not replace its target. caam now uses the POSIX rename extension, which does.
Security
caam bundle importcannot write outside the vault, and--forceno longer skips integrity checks. A bundle with valid checksums could carry profile paths that escaped the configured vault, and--forcebypassed integrity failures. Profile names and every source and destination path are now checked before any preview or write. Symlink redirects, special or duplicate ZIP members, unchecked files and checksum failures stop the import in every mode, including--dry-runand--force. Replacement backups use private names, so replacingworkcannot overwrite a profile namedwork.bak.- The coordinator API requires a token off loopback, and deployment checks SSH host keys. The coordinator listens on 127.0.0.1 by default and refuses any other
--bindaddress without--auth-token. The deployer used to open a second SSH connection that skipped host key checks; setup now reuses its authenticated connection, and the agent's own SSH connection checksknown_hosts. Tokens are per host and are never included in API output. - The auth agent signs in only with listed accounts and clicks consent only on real consent pages. It clicked submit-like buttons on any page that mentioned "authorize" or "allow", so it could click "Create account" on a Claude sign-up page for a Google identity you never meant to use. It also picked the first account Google offered when the chosen one was missing. Consent is now clicked only on Claude's OAuth authorize page and Google's OAuth consent pages. When accounts are listed, the agent falls back only to other listed accounts, and it fails instead of using an unlisted Google account or approving a Claude session outside the list. With no list, it still uses the first account offered.
- The auth agent rejects non-loopback Host headers and non-Claude authorize URLs.