Multi-Account Rotation¶
Two or more personal Claude subscriptions can be wired into Agentic and rotated transparently. The user still experiences ONE account with more capacity — no UI surface exposes the switching, and existing single-account installations behave exactly as before (no accounts.toml = no rotation = today's flow, byte-for-byte).
Why¶
A single Claude Max account hits the 5-hour and weekly limits during heavy autonomous-runner sessions. Linking a second subscription doubles available capacity without changing the user experience: when one account is rate-limited the runner (and chat) silently fail over to the next, and resumes on the original account once its limit window resets.
Setup¶
1. Seed each account's credential directory¶
Each Claude account needs its own CLAUDE_CONFIG_DIR — a directory where .claude.json, projects/, sessions/, and backups/ live. You seed it by running claude /login once with that directory set:
mkdir -p ~/.config/craft-easy-agentic/account-A/.claude
CLAUDE_CONFIG_DIR=~/.config/craft-easy-agentic/account-A/.claude claude /login
# follow the OAuth/login prompts in your browser
mkdir -p ~/.config/craft-easy-agentic/account-B/.claude
CLAUDE_CONFIG_DIR=~/.config/craft-easy-agentic/account-B/.claude claude /login
# log in with the second Anthropic account
After login each directory contains .credentials.json plus the regular Claude state.
2. Register the accounts in accounts.toml¶
Create ~/.config/craft-easy-agentic/accounts.toml:
[[accounts]]
name = "primary"
claude_config_dir = "/home/patrik/.config/craft-easy-agentic/account-A/.claude"
[[accounts]]
name = "secondary"
claude_config_dir = "/home/patrik/.config/craft-easy-agentic/account-B/.claude"
| Field | Notes |
|---|---|
name |
Free-form label used in logs. Must be unique. |
claude_config_dir |
Absolute path to the directory that holds the account's .claude.json and .credentials.json. |
home |
Deprecated — still accepted for backwards compatibility but logs a warning at startup. See Migration below. |
Restart the Agentic service after editing the file:
The startup log shows how many accounts were registered and warns about any deprecated home fields it found.
How rotation works¶
Acquire / release per spawn¶
Every Claude spawn (chat turn, runner task) goes through the AccountResolver:
- Acquire — picks the account with the lowest
in_use_countthat is not currently rate-limited. Tie-break: order inaccounts.toml. The account'sin_use_countis incremented and a handle returned. - Spawn — the subprocess is started with
CLAUDE_CONFIG_DIR=<account's dir>set. RealHOMEis unchanged so other CLIs (gh,az,kubectl,git pushover SSH) still see the user's regular credentials. - Release — when the subprocess exits,
in_use_countis decremented.
State is persisted to ~/.local/share/craft-easy-agentic/account-state.json and protected with a flock so the bash runner subcommands and the in-process chat thread share state without races.
Rate-limit handling¶
When a Claude spawn exits non-zero and the output matches an Anthropic rate-limit pattern, the runner:
- Marks only that account as limited (
limited_until_unix+limit_reasonwritten toaccount-state.json). - The inner retry loop in
run_taskcallsaccount acquireagain — picks the next available account and retries the same task without bumping the regular attempt counter. - If all accounts are rate-limited the acquire returns exit-code 2 → the runner falls back to the classic pause-file flow (writes
.pause-backlog+.pause-backlog-resume-atset to the earliest reset across accounts) → dashboard shows the rate-limit pill with countdown.
When the reset timestamp passes, the account is automatically marked available again on the next acquire.
Chat session continuity across accounts¶
Claude's ~/.claude/sessions/ directory is CLAUDE_CONFIG_DIR-tied — --resume <session-id> only works on the account that originally adopted the ID. If the chat hits a rate-limit mid-conversation and rotates to a different account:
SessionMeta.claude_sessions: BTreeMap<account_name, session_id>records the per-account binding.- On the first turn under the new account,
render_history_for_replaybuilds a markdown transcript of prior user/assistant turns and prepends it to the next prompt. --session-id <new-uuid>is used so the new account adopts a fresh ID.- On subsequent turns under the same account, normal
--resumecontinues working.
The user sees no break in the conversation — the switch is invisible.
CLI¶
craft-easy-agentic account exposes the resolver to bash callers (the runner uses this internally). Useful for diagnostics:
# Show current state of all accounts
craft-easy-agentic account snapshot
# Force-acquire (returns JSON with claude_config_dir, name, etc.)
craft-easy-agentic account acquire
# Release a previously acquired account
craft-easy-agentic account release --name primary
# Mark an account rate-limited until a unix timestamp
craft-easy-agentic account mark-limited --name primary \
--until-unix 1746090000 --reason rate_limit_5h
# Clean up stale in_use_count after a crash
craft-easy-agentic account housekeep
Distinct exit codes let bash branch without parsing the JSON: 0 = ok, 2 = no available account, 1 = error.
State file: account-state.json¶
{
"accounts": {
"primary": {
"in_use_count": 1,
"limited_until_unix": null,
"limit_reason": null
},
"secondary": {
"in_use_count": 0,
"limited_until_unix": 1746117600,
"limit_reason": "rate_limit_5h"
}
}
}
flock-protected so concurrent acquire/release calls from bash and Rust never race.housekeep()runs at server startup to resetin_use_count(otherwise crashes leave it stuck at >0).- Safe to delete manually if it gets corrupted — Agentic recreates it from
accounts.toml.
Per-spawn isolation: CLAUDE_CONFIG_DIR, not HOME¶
Earlier iterations of multi-account routing overrode HOME per spawn. That worked for Claude itself but broke every other HOME-tied tool — gh, az, kubectl, ssh-agent all suddenly saw an empty config and looked logged out inside the chat panel.
The fix (CA-T013) is to override only CLAUDE_CONFIG_DIR, which the Claude CLI respects for its own state but no other tool reads. Real HOME is left alone, so everything else inside chat- and task-spawns continues to see the user's regular credentials.
Migration from home to claude_config_dir¶
If your accounts.toml still uses the legacy home field, Agentic:
- Logs a deprecation warning at startup.
- The first time the rotation picks up that account, runs a one-shot idempotent migration:
<home>/.claude.json→<home>/.claude/.claude.json. - Continues to work — the legacy field is mapped internally to
claude_config_dir = <home>/.claude.
To silence the warning, edit accounts.toml:
# old
[[accounts]]
name = "primary"
home = "/home/patrik/.config/craft-easy-agentic/account-A"
# new
[[accounts]]
name = "primary"
claude_config_dir = "/home/patrik/.config/craft-easy-agentic/account-A/.claude"
Troubleshooting¶
"Permission denied" reading .credentials.json¶
The credential file is owned by the user that ran claude /login. If the systemd service runs as a different user, run the login while logged in as the service user.
Account never rotates back from "limited"¶
Check account-state.json — limited_until_unix should be a future unix timestamp. If it's stuck in the past, run craft-easy-agentic account housekeep to clear stale entries.
Chat replays history every time it opens¶
Replay only happens when the chat picks up an account that doesn't yet have a session ID for the current SessionMeta. If you see replays mid-conversation, look in the chat session NDJSON — the same account should keep the same session ID across turns.
Single-account installations¶
If accounts.toml doesn't exist (or has zero entries), Agentic uses the user's default Claude config (the directory claude would normally use without overrides). Behaviour is identical to pre-CA-T011 builds.