orbotodocs
Agents & AI

Login profiles for Claude Code, Codex, Gemini CLI and Kimi

Keep several accounts of the same coding harness on one machine, sign each one in from orboto, see which account every profile holds, and switch between them without losing the conversation.

A login profile is one captured account of a coding harness. With profiles you keep a private and a company subscription side by side, start a session under either one with --account, and let a session move to the other when the first one hits its usage limit. Profiles exist for orboto claude, orboto codex, orboto gemini and orboto kimi; orboto ollama keeps its own host-per-profile model.

Every harness has one config directory, never one per profile. A profile is a captured copy of the login that directory holds. orboto stores the profile name, the plan, the rate limit tier, the token expiry, the account identity and the last usage limit in ~/.orboto/accounts.json - never a token. The captured login itself is kept in a second, orboto-owned Keychain item on macOS, or in an AES-256-GCM encrypted file under ~/.orboto/accounts/ on Linux, keyed by a local secret in ~/.orboto/secret.

The commands

orboto claude accounts add work                     # capture the account that is logged in right now
orboto claude accounts add private --login          # sign in first, then capture that account
orboto claude accounts login work                   # sign in again and re-capture the profile
orboto claude accounts list                         # every profile with its identity, plan, limit and quota
orboto claude accounts refresh work                 # re-capture from the live login (tokens rotate)
orboto claude accounts remove work                  # delete the captured login; the live login stays
orboto claude --account private                     # start a session under a profile
orboto claude switch work                           # restart a running session under another profile

Added in v0.213.0 add --login and login run the harness's own sign-in in the foreground and capture the account that signed in. The sign-in command is what you would type yourself:

LauncherSign-in that --login and login run
orboto claudeclaude auth login
orboto codexcodex login --device-auth
orboto kimikimi login
orboto gemininone - Gemini CLI has no login command orboto can run

For Gemini CLI, sign in inside Gemini CLI itself and then run orboto gemini accounts add <name>. orboto gemini accounts add <name> --login and orboto gemini accounts login <name> stop with exit code 2 and say exactly that. If the sign-in exits with an error, nothing is captured and nothing is left behind.

The same verbs exist for the other launchers: orboto codex accounts list|add|remove|refresh|login, and likewise for gemini and kimi. orboto codex --account work starts under a profile and orboto codex switch second moves a running session.

Every profile knows its account

Added in v0.213.0 A capture records which account the login holds: the email address, or the account id when there is none. It is a plain field in the registry, never a token.

  • Claude Code's login does not carry an email address. orboto reads it from oauthAccount in the .claude.json file Claude Code keeps next to the login: ~/.claude.json for the default config directory, <dir>/.claude.json for any other.
  • Codex and Kimi carry it in the email claim of their token, then the account id.

You see the identity in three places:

  • orboto claude accounts list has an IDENTITY column, and --json returns it as identity per profile. Under the table, a live login: line names the account the live login holds and which profile holds it.
  • The start line of orboto claude --account work names it: orboto claude: login profile work (me@example.org).
  • Admin -> Agents -> Live shows it next to the profile name of a running session, because the session publishes it with its registration and heartbeat.

A profile captured before 0.213.0 shows - until its next accounts refresh or accounts login.

A switch swaps only the credential and leaves .claude.json on the previous account. So for the Claude Code main directory the active profile of the registry wins: a capture that is not preceded by a fresh sign-in records the active profile's identity, and warns on stderr when .claude.json names another account. A capture right after --login or login trusts .claude.json, which Claude Code has just rewritten.

One account, one profile

Added in v0.213.0 accounts add and accounts login print the account they captured (added claude profile private (me@example.org)) and refuse an account another profile already holds - recognised by its identity or by a byte-equal captured credential - unless you pass --force. accounts login <name> also refuses to record a different account under a name that already holds one. accounts refresh runs the same guard, with the same --force.

When the live credential is not the active profile's and .claude.json names an account no profile holds, the refusal says exactly that instead of calling it a duplicate. It names the live account, the active profile, and the two ways out: accounts add <name> --login to keep the account as a new profile, or accounts refresh <active> if the main directory still runs the active profile. add --login with no active profile and a live login prints, before the sign-in, that the live account is held by no profile and will be replaced, so you can stop with Ctrl-C and capture it first. Nothing is stored when a command refuses.

A rotated login is saved before a sign-in replaces it

Tokens rotate while a session runs, so the live login is usually newer than the captured copy. Before a sign-in replaces the shared main-directory login, add --login and login write the live login back into the active profile - the same first step a switch takes.

The write-back happens only when the live login is that profile's account: its identity matches, or the identity is unreadable and the credential is no other profile's. Otherwise orboto skips it and says so on stderr, for example write-back skipped, naming the live account, who holds it and the active profile. A live login that belongs to no profile is never written over a profile.

Two accounts at the same time: --parallel

Claude Code only. A normal profile switches the one config directory, so only one account runs at a time. --parallel gives a profile its own thin config directory, ~/.orboto/claude-dirs/<name>, so a private and a company account can run in two terminals at once.

orboto claude accounts add second --parallel            # capture the live login, then create the thin directory
orboto claude accounts add second --login --parallel    # sign in inside the thin directory

Added in v0.213.0 With --login --parallel the sign-in runs inside the thin directory, so the login of the main directory is not touched, and a failed sign-in removes the thin directory it had created. Without --login, orboto captures the login the main directory holds, builds the thin directory and puts the main directory back on the profile it ran before, when it has one on record; otherwise it says that the main directory still runs the new profile.

The thin directory links projects, sessions, plugins, skills, hooks, rules, shell-snapshots, file-history, statsig, todos, tasks and cache into the main directory, so every conversation is visible from both without copying anything. settings.json is a real copy that accounts refresh --parallel and a switch synchronise in both directions. The thin directory has its own Keychain item, named like the one Claude Code derives for that directory. --parallel on any other launcher stops with exit code 2.

Usage limits

A session can move to the next profile by itself when the harness reports a usage limit:

orboto claude --on-limit switch:work,private

--on-limit takes ask, stop, off or switch:<name,...>. Added in v0.230.0 switch without names is the same switch over every profile (see Switching on and off). A switch picks, from the order, a profile that has no active limit and is not logged out, on hold or quarantined; when the order is exhausted it asks. A switch restarts the harness in the same directory with the same conversation (--resume, or --continue without an id), so the conversation never moves. This is the reactive path: it acts when the harness shows its limit prompt. With switch: it is also the fallback of the proactive switch below, which acts before the prompt.

Switching on and off

Added in v0.230.0 With two or more profiles that are neither on hold nor --parallel, every supervised orboto claude start switches over all of them at the usage limit by default. You can turn that off for new starts and change the policy of a session that is already running, all from the command line and without a restart.

For new starts

orboto claude accounts config --auto-switch off    # a start without --on-limit no longer switches
orboto claude accounts config --auto-switch on     # back to the default: switch over every profile
orboto claude accounts config --json               # "autoSwitch": true or false, next to threshold and strategy

--auto-switch off stops the default: a start without --on-limit runs without a switch policy, and a plain claude session no longer has its login switched at its limit. --auto-switch on brings it back. The setting is stored as rotation.claude.autoSwitch in ~/.orboto/accounts.json, is on until you turn it off, and accounts config --reset puts it back on together with the threshold and the strategy. accounts config and accounts list print it, and their --json carries autoSwitch. An explicit --on-limit on a start always wins, and a session that already runs keeps its policy until you change it as shown next. Only orboto claude has this default: --auto-switch on another launcher exits with code 2.

For running sessions

orboto claude switching reads and changes the policy of the supervised sessions on this machine. Nothing restarts: the change applies at once, also in the middle of a turn and while a restart waits for the session to be idle.

orboto claude switching on|off|ask|stop|switch:<names>|status
    [--session <short id>|--all] [--threshold <percent>]
    [--strategy next-reset|most-quota|order] [--resume] [--json]
orboto claude switching status                         # every supervised session: its policy and who set it
orboto claude switching off --all                      # running sessions stop switching
orboto claude switching on --session 1a2b3c4d          # switch over every profile again
orboto claude switching switch:work,second --threshold 85 --strategy order --session 1a2b3c4d
orboto claude switching ask --session 1a2b3c4d
orboto claude switching stop --resume --all            # stop at the limit, continue at the reset
orboto claude switching status --json
ValueAt the usage limit
on (also switch)switch over every login profile; a profile added later joins, a disabled one leaves
switch:<a,b>switch over exactly these profiles, in this order; added profiles do not join
askask the operator, in the terminal and in Admin -> Agents -> Live
stopstop the session
offonly record the limit

--threshold (1 to 100) and --strategy tune a switch and go with on or switch:<names>; without them the values of accounts config apply. --resume goes with ask or stop and lets the session wait for the reset and continue; a switch always does. A combination that does not fit, such as --resume with on, exits with code 2, and so does a switch order that names a profile accounts list does not know. With one session running the command addresses it; with more than one a change needs --session <short id> or --all, and status lists them all. orboto claude --sessions shows each session's policy under its row, and the status line shows it next to the profile, for example profile work on-limit switch all at 90%.

Every session you address answers with one outcome:

OutcomeMeaning
appliedthe session runs the new policy from now on
refusedthe session kept its policy; the line names the reason, for example a profile it does not have
supersededa later request replaced yours; the line names the policy the session runs now
pendingno answer within 6 seconds, for example during a restart; the request stays, the session applies it as soon as it gets to it, and switching status shows the result
older-clithe session's supervisor runs an orboto older than 0.230.0 and cannot take a change; your request is taken back and its policy changes only with a new start

The newest request wins. When more than one request reaches a session before it serves them, the session applies the newest only, and every older requester sees superseded.

Exit codeMeaning
0every addressed session answered applied; status also exits 0
1at least one session answered refused, superseded, pending or older-cli
2usage error: an unknown value or flag, --session together with --all, a flag that does not fit the value, a threshold outside 1 to 100, an unknown profile, no supervised session, or several sessions and neither --session nor --all

--json prints one row per session: session, cwd, outcome, detail and limitPolicy for a change, and session, cwd, terminal, account and limitPolicy for status.

A policy change withdraws what the old policy queued. A switch before the limit that waits for the running turn to end, or a resume armed at the limit, belongs to the policy that queued it. When the new policy no longer wants it - off, ask, stop without --resume, or an order without that profile - the session withdraws it and says so, for example queued switch to work withdrawn by the policy change; a resume the new policy still wants is planned again. A switch or a restart an operator requested, with orboto claude switch, orboto claude --restart or in Admin -> Agents -> Live, stays.

Accounts added or disabled while sessions run. A policy over every profile is resolved at each decision, not once at the start. orboto claude accounts setup third adds a profile, and every running session that switches over every profile takes it into its rotation at its next decision, without a restart. accounts disable <name> takes a profile out of every rotation at once; accounts enable puts it back. A --parallel profile never joins the order.

A start with one profile carries the default. A supervised start with a single login profile runs the every-profile policy as well. The status line shows it as switch all (one profile) and switching status adds waits for a second login profile. It acts as soon as a second profile exists, also while the session runs.

From the browser, another machine and scripts

Admin -> Agents -> Live shows a session's policy in its panel under At the usage limit: what happens at the limit, who set it and when (the start's default, the start's --on-limit, orboto claude switching or this panel or the API). The form below it switches over every profile or over chosen profiles in the order you tick them, asks, stops or turns the switch off, with the percent, the strategy and Wait for the reset and continue, and applies it to the running session without a restart. It appears for the session's account, its owner, an admin with admin:agents:write and a person the session is shared with at Operate level, while nobody else holds the takeover and when the session's CLI takes the change.

On the command line the same change is orboto live switching <session> with the same values and flags, for a session on any machine; status prints the policy the session published. It takes the session by the id or prefix orboto session list prints, answers when the instance has accepted the change, and the session confirms it as a status line in its transcript (orboto live transcript <session> --follow). A session whose CLI is older than 0.230.0 takes no change, and the command exits with code 1.

orboto live switching 0c4f6a10 status
orboto live switching 0c4f6a10 off
orboto live switching 0c4f6a10 switch:work,second --threshold 85

Scripts and agents send the live input set_limit_policy; MCP clients reach it through orboto_api_search and orboto_api_call:

orboto post /agents/live-sessions/<id>/input '{"kind":"set_limit_policy","policy":{"mode":"switch","profiles":["work","second"],"threshold":85}}'

mode is off, ask, stop or switch. profiles (one to 20 names), threshold (1 to 100) and strategy belong to switch, and without profiles it switches over every profile; resume belongs to ask and stop. A field that does not fit the mode, or a profile the session did not publish, is a 400; a session that does not take the kind answers 409. The route follows the rules of the other live controls: the session's account, its owner, admin:agents:write or an Operate share may change a policy, a person who may only watch gets 403, anyone else 404. It takes or renews your takeover, and 409 names another holder. Every change through this route is written to the audit log as agent.live.input with the new and the previous policy. A session publishes its policy as limitPolicy on GET /agents/live-sessions/<id>: mode, everyProfile, profiles, threshold, strategy, resume, active, source (default, flag, cli or live), changedAt and changedBy.

Which sessions switch

Supervised orboto claude sessions take everything above. Sessions a runner owns and starts with --on-limit (agent-runner, codex-runner and live-session run, for Claude Code and Codex) switch when the harness reports its limit, take the live change too and publish their policy, but have no start default, no switch before the limit and no orboto claude switching; agent-runner, codex-runner and live-session run --daemon accept API-key profiles only. The interactive orboto codex, orboto gemini, orboto kimi and orboto agy sessions never switch on their own: for the first three the login changes only with orboto <harness> switch <name>, orboto agy has no login profiles at all, and Ollama keeps no session to switch. What each launcher provides lists it per launcher.

Usage and proactive switching

Added in v0.215.0 orboto reads how much of each account's quota is used, shows it, and moves a session to a better profile before the limit prompt appears.

Quota per profile

orboto claude accounts list             # asks the usage endpoint for every profile
orboto claude accounts list --cached    # the last stored read, no request
orboto claude accounts list --json      # the same as data for scripts

list prints one row per profile with the 5-HOUR and 7-DAY utilisation and the reset time of each, and a STATE column: active, parallel, hold or quarantined. Under the table you find one line for every further window the account reports - the per-model weekly windows, for example work: Fable weekly 81% (resets ...) next to the account-wide ones -, a quarantine, and the effective rotation settings. --json carries the same: usage per profile (state known, unknown or not_reported, and windows with window, model, usedPercent, resetsAt, checkedAt, error, backoffUntil), hold, quarantine, plus active and the effective rotation.

--cached prints the stored read without a request. A read younger than a minute is reused anyway.

After a failed read the row says unknown (42% 25m ago): the last good value with its age, never the old number as if it were current. The status line and the switch decision ignore a stale read.

Where the numbers come from

Each harness has an endpoint it polls for its own usage display. orboto calls it with the profile's own login:

HarnessWhat orboto reads
Claude Codethe usage endpoint Claude Code itself polls: the 5-hour and 7-day windows and every per-model weekly window the account carries
Codexthe usage endpoint of the ChatGPT login: a primary and a secondary window; an API-key login has no plan quota
Kimithe usage endpoint of the Kimi login: the 5-hour and 7-day limits
Gemini CLI, Ollamanothing - the row says not reported, and their switch stays reactive (--on-limit)

None of these endpoints is a documented public API, and each can change or disappear without notice. orboto treats every read as best effort. A failed read shows unknown, never fails list and never blocks or delays a start. A rate-limit answer (429) backs off for at least six minutes, longer with each repeat and never shorter than the server's Retry-After, up to 30 minutes; any other failure waits one minute, doubling up to 30. The backoff is stored on the profile, so list, rotate and every supervisor on the machine respect it together.

An expired Claude Code token of a profile that is not the live login is renewed with the same refresh grant Claude Code uses, and the new tokens go back into that profile's captured login. The live login is never refreshed by orboto - Claude Code owns it.

Hold and quarantine

orboto claude accounts disable <name> puts a profile on hold: no switch, proactive or at a limit, lands on it until accounts enable <name>.

When the server rejects a profile's refresh token (invalid_grant), the profile is quarantined: list shows it and no switch lands on it. accounts login <name> - any new capture of the profile - releases it. If the shared config directory's live login holds that very account, the rejected token only means the captured copy is stale; the profile is then not quarantined, and the next switch or accounts refresh stores the current login.

The proactive switch

orboto claude --on-limit switch:work,private
orboto claude --on-limit switch:work,private --on-limit-threshold 85 --on-limit-strategy order

With --on-limit switch:<order>, the orboto claude supervisor polls the quota of the profile the session runs on: every minute while a turn runs, every five minutes when idle, and less often while a backoff runs. When the window that counts reaches the threshold, the supervisor reads the profiles of the order and restarts the session under the one the strategy picks - before the limit prompt appears. The switch is the same one a reactive switch takes: the conversation is resumed, and the restart waits for the running turn to end.

SettingFlag on a startStored asDefault
Threshold, percent of a window--on-limit-threshold <percent> (1-100)rotation.<harness>.threshold90
Strategy--on-limit-strategy next-reset|most-quota|orderrotation.<harness>.strategynext-reset

Both flags need --on-limit switch:. To store them for a harness's profile set, use accounts config:

orboto claude accounts config --threshold 85 --strategy most-quota
orboto claude accounts config --reset       # back to 90 and next-reset

The values live under rotation in ~/.orboto/accounts.json; accounts list prints the effective ones. A flag on a start overrides the stored value for that start.

StrategyPicks among the profiles that qualify
next-reset (default)the one whose tightest window resets soonest - consume what comes back first
most-quotathe one with the lowest utilisation in its tightest window
orderthe first in the switch: order

Ties fall back to the order.

The window that counts. Claude accounts carry the account-wide 5-hour and 7-day windows and, besides them, a weekly window per model. The threshold and the strategy use the tighter of the account-wide windows and the weekly window of the model the session runs. A session on a model without a window of its own is judged by the account-wide windows only.

orboto finds the model in this order: the model Claude Code reports in its status line for the session; the launcher's --model; ANTHROPIC_MODEL; then model in the project's .claude/settings.local.json and .claude/settings.json, in Claude Code's config directory settings.json, and in the user settings. A model matches a window when the model id or alias contains the window's name or the other way round: claude-fable-5-1, fable and Fable 5.1 all match a Fable window.

It never flip-flops. A target must be below the threshold and better than the current profile on the same comparison. Profiles that are on hold, quarantined, logged out, limited, or whose quota could not be read are skipped. After any switch there is a cooldown: no proactive switch within five minutes of the last one. When no target qualifies, the session stays where it is and says so once. At the limit itself the reactive --on-limit path still acts, and it ranks the profiles it can switch to by the same strategy; profiles without a known quota come after the known ones, in the order.

Where you see it

  • The status line of orboto claude shows profile <name> and the weekly row of the session's model under Claude Code's own 5-hour and weekly lines. It drops the numbers when the last read failed.
  • Admin -> Agents -> Live shows the utilisation per profile on the session, together with the hold and quarantine badges, or Usage unknown with the time of the last good read.

For cron and scripts: accounts rotate

orboto claude accounts rotate --once --json

rotate --once reads every profile, decides the same way for the profile the shared config directory is on, and switches it once. If a supervised orboto claude session runs on that profile - one started without --account counts as running on the active profile - the switch goes through that session's supervisor: it waits for the running turn, resumes the conversation and starts the cooldown. With more than one such session rotate refuses and names switch --session; with none, it switches the login in place.

Exit codeMeaning
0switched, or nothing to do (below the threshold, or in the cooldown) - the result field says which
1error, including an unknown quota of the active profile: nothing is switched on a guess
2usage error: an unknown flag, a threshold outside 1-100, an unknown strategy
3no viable target

--json prints result as switched, nothing-to-do, no-viable-target or error; the plain line carries the same word after accounts rotate:. --order a,b limits the candidates. Without it every profile except --parallel ones is a candidate, because a parallel profile runs in its own thin directory and is a target only when you name it in the order. --threshold, --strategy and --model override the settings for this run; without --model the model comes from the current directory's .claude/settings.local.json and .claude/settings.json, then Claude Code's config directory and the user settings. Without --once, rotate repeats the check every five minutes in the foreground until Ctrl-C.

Concurrent writes and macOS

Every write of a captured login (a capture by add, login or refresh, a switch, a token refresh, the write-back before a sign-in) takes one lock next to the registry, so a quota read and a switch never interleave. It waits up to 90 seconds. A switch that runs while list is renewing many expired profiles waits instead of failing.

On macOS the supervisor reads the live Claude Code login from the Keychain once per poll - every 60 seconds while a turn runs; before 0.215.0 it was read only at a start and a switch. If macOS asks whether security may read that item, answer "Always Allow".

See also Launchers and the uniform flags, Update Claude Code, Codex and the other harnesses and the CLI daily workflow.

On this page