Routing runs across clausona accounts
clausona keeps several Claude Code and Codex accounts on one machine. A route is a named
group of those accounts and a rule for picking one of them by how much of its plan quota is
used. clausona run --route <name> starts the tool on the account the rule picks right now.
(csn is an alias for clausona that only the user's interactive shell has. Run clausona.)
Run clausona route --help once before anything else. It lists every command, option and exit
code of the installed version. Where it differs from this skill, it wins.
clausona route with no arguments opens the Routes screen, a TUI meant for a person at a
terminal. Do not run it; use the subcommands below. (In a shell without a terminal it only prints
the help.) A user who would rather look through routes on that screen can run it themselves.
What decides the pick
- An account's usage is the higher of its 5H and 7D windows: 0% 5H with 99% 7D is 99%. That number, and nothing else, is ranked and shown.
- When the tool's arguments name a model (
claude --model opus,codex -m gpt-5.3-codex-spark, also afterexec), the account's limit on that model joins the max: Claude's is weekly, and a Codex one can have a 5-hour and a weekly window, of which the higher counts (named in the note andusage.windowasGPT-5.3-Codex-Spark 5Hor… 7D). A model falls under a limit when the limit's name (Opus,GPT-5.3-Codex-Spark) is a word of the model's name, in any case:opus,claude-opus-4-5,opus[1m]fall underOpus;gpt-5.3-codexdoes not fall underGPT-5.3-Codex-Spark. A run that names no model counts no model's limit, even if the account's default model has one: pass--modelwhen the work needs a model with its own limit.-c model=…, a Codex config profile andopusplanare not read. - clausona stops at the first stage that finds someone:
- pool:
frommembers undermaxUsage(default 80), chosen by the strategy. - fallback: the first
fallbackmember, in listed order, undermaxUsage. - overflow: every member is at
maxUsageor over it, so the one with the most left (lowest usage) is taken, pool or fallback, at 90% or 99% alike. - nobody: every member is at 100% or skipped. Nothing is launched, exit code 75.
With
--wait, it waits for the soonest reset instead and starts again at 1 (see Waiting).
- pool:
- Strategies:
round-robin(default; the account picked longest ago, a never-picked one first),headroom(lowest usage),expiring(lowest usage among accounts whose weekly limit resets within 24 hours; with none, asheadroom). - Signed-out, expired and unreadable accounts are skipped at every stage, and so is one whose stored sign-in is another account's.
- An API profile in a route (named exactly; no pattern matches one) is ranked by its spend as a
share of its limit: a clausona budget (
clausona route budget), else the limit set on its provider key. One with no limit, whose provider reports no spend, or whose spend could not be read is skipped. It is billed per use. - Resumed runs (
-c/--continue,-r/--resume,--from-pr,codex resumeorfork) use only accounts that share sessions: the primary account and profiles with merged sessions. If none of them can take it, the account that holds the session has to be run by name.
Looking before acting
bashclausona route list --json # routes, their settings with defaults, their members, dir rules clausona route explain <name> --json # every member's usage and status; launches and records nothing clausona route explain <name> --model opus --json # as a run of that model would rank it clausona route explain --json # what clausona run claude and codex would take in this directory clausona list --json # every profile, with its email and quota clausona route budget --json # every API profile: its limit, whose limit, spend and usage
explain --json gives:
settings: the route with its defaults.settings.toolisclaude,codexorall(a route over the accounts of both tools).outcome:{"kind": "picked", "id", "stage", "reason"}, or{"kind": "none"}withsoonest({"id", "at"}) when a reset time is known.explainexits 0 even when nobody could be picked, so readoutcome.kind.members, one per account:statusispicked,eligible(under the cut, not picked),over-limit(at or abovemaxUsage) orskipped. A skipped one hasskipReason:signed-out,expired,other-account(its stored sign-in is another account's),no-reading,not-registered,keeps-own-sessions, or for an API profileno-spend-limit,spend-not-reportedorspend-unread. Each also hasusage,fiveHour,sevenDayandlastPickedAt,scoped({"label", "usedPercent", "resetsAt"}, its limit on the--modelmodel, of a Codex limit with two windows the higher, or null) andbudget(an API profile's{"usedPercent", "spentUsd", "limitUsd", "source", "per", "resetsAt"}, or null).usage.windowis5H,7D, the name of that limit (its label for Claude,Opus; its label and window for Codex,GPT-5.3-Codex-Spark 7D), orbudget. The top level hasmodel.
route list --json gives each route's members, freeNow ({"free", "members"}) and next
(as outcome), and problems: what is wrong with each route in routes.json that cannot be
used, and with each dir rule or default that cannot be read. Such a route is left out, and only
commands that use it fail; tell the user, and point them to clausona route edit rather than
editing the file yourself. It also gives dirs ({"~/work": {"claude": "work"}}), defaults
({"claude": "main"}) and ruleWarnings: rules that name a route that is gone or is for the
other tool, which a run skips.
With --json, a failing route command prints {"version": 1, "error", "message"} on stdout.
error is usage, not-found (no such route) or failed; a routes.json, or a route named,
that cannot be used adds file (its path) and problems.
Answer "why did it pick X" from explain: outcome.stage and outcome.reason for the pick, and
each other member's status, skipReason and usage. explain takes the same field options as a
run (--strategy, --max-usage, …) and --resume, to show what a run like that would get.
Creating or changing a route for the user
route add and route set never ask, in a terminal or not. They write at once, and route add
then prints the route it made. So the asking is yours to do, before you write:
- Show the user which accounts the route would hold. Rank it unsaved and show them the result:
clausona route explain --tool <claude|codex|all> --from '<patterns>' [--exclude '<patterns>']. For a change, show the route as it is (route explain <name>) and as it would be. - Ask before including accounts that look like someone else's or shared: another person's name
or email,
*-share. Leave them out until the user says yes. *takes every subscription account of the tool, including ones added later. When the user described a subset ("my work accounts"), use names (work,work2) or an email pattern (*@example.com) instead.- Never put an API profile in a route unless the user names it: it is billed per use. A
pattern never matches one, so
*is safe; it joins only by its exact name (claude:glm). It also needs a spend limit:route addandroute setrefuse one without, namingclausona route budget <profile> --usd 20 --per month. Set a budget only with the amount and period the user gives. A budget stops new routed sessions, not a running one. Run an API profile by name (clausona run claude:glm) only when the user asks for it. - Wait for the user's OK, then write with the CLI. Never edit
~/.clausona/routes.jsonyourself, and do not useroute edit: it opens an editor for a person.
bashclausona route add <name> [--tool claude|codex|all] --from '<patterns>' [--exclude '<patterns>'] [--fallback '<patterns>'] [--strategy <s>] [--max-usage <n>] clausona route set <name> [--from …] [--exclude …] [--fallback …] [--strategy …] [--max-usage …] [--add …] [--drop …] [--no-fallback] clausona route rename <old> <new> clausona route remove <name>
--toolis the route's tool:claude(the default),codex, orallfor one route over the accounts of both tools. Without--tool,--fromentries that all carry one tool's prefix (codex:a,codex:b) make a route for that tool, and prefixes of both tools make anallroute. Checkclausona list --jsonfor which tools have accounts.route setcannot change the tool.--yesis accepted and does nothing. You do not need it.- Patterns are comma-separated profile names or globs (
team-*,claude:team-*). A pattern with an@matches account emails. Matching ignores case. Quote the list. - On an
allroute a name without a prefix matches in both tools:personalisclaude:personalandcodex:personal. Use the prefixed id to mean one account. - In
route set, each field option replaces its whole field:--exclude oldmakes the exclude list justold. To add to a list, pass its current entries too (fromroute list --json). --addand--dropchangefromone entry at a time.--dropremoves only an entry written infrom, so it cannot take one account out of*or*@example.com. Use--excludefor that.renameandremoveact at once. Ask before removing a route.renamealso rewrites the dir rules and defaults that name it;removealso removes them, and says which.- A route name starts with a letter or a digit, and the rest is letters, digits,
.,_and-. Names and patterns that look like an API key or a token are refused, and clausona does not print them back. Never put a key in either.
Running
bashclausona run --route <name> -- -p "<prompt>" # Claude Code, non-interactive clausona run --route <name> -- exec "<prompt>" # a Codex route clausona run codex --route <name> -- exec "<prompt>" # an all route: name the tool clausona run claude --from '<patterns>' -- -p "<prompt>" # an unsaved route clausona run --route <name> --strategy headroom -- -p "…" # change a field for this run only
- Routing options (
--route,--from,--exclude,--strategy,--max-usage,--fallback,--wait) go before the tool's own arguments. A--ends them, and everything after it goes to the tool untouched (--model,--permission-mode,--output-format json, …). - The field options need
--routeor--from. On their own they are refused with exit 1. - On an
allroute (settings.toolisall), put the tool word before--routewhenever you pass arguments:clausona run claude --route <name> -- -p "…". Only that tool's accounts are ranked. Arguments without a tool word are refused with exit 1, because Claude Code and Codex take different ones.route explainandroute picknarrow the same way with--tool. - Next to
--route,--exclude xleavesxout for this run on top of the route's own excludes. The other field options replace their field for this run. Nothing is saved. - Never put a profile right after routing options or a tool word. These are refused, because the
tool would get the name as its prompt:
clausona run --route main claude:work,clausona run claude claude:work. To run one account, name it alone (clausona run claude:work). If a profile is named like one of the tool's commands (exec,mcp), put--before the command:clausona run codex -- exec …. clausona run claude(orcodex) with no routing options takes a route from the user's dir rules and defaults: the deepest dir rule for the current directory, then the tool's default, then the active profile.clausona route explain --jsonwith no name says which. With no tool word and no--routeor--from, it is refused.- Naming a profile (
clausona run claude:work) always runs it, whatever its quota. - A routed run names the account it picked, and why, on stderr. stdout is the tool's own.
- Without a terminal an unknown
--routecreates nothing: it exits 1 with "Create it: clausona route add ". Treat that as a new route: show the members and ask first, as above. clausona run --route <name>with no tool arguments is an interactive session. Give that command to the user instead of starting it from your shell.
Waiting for a free account
--wait makes "nobody is free" a wait instead of an exit 75, for run and route pick alike:
bashclausona run --route <name> --wait -- -p "<prompt>" # waits up to 5h clausona run --route <name> --wait=2h -- -p "<prompt>" # or 30m, 1h30m: after `=`, never a space id=$(clausona route pick <name> --wait=1h --model opus) # an orchestrator's turn, waiting
- It sleeps until 30 s after the soonest reset of an account at 100%, reads quota again at
least every 30 minutes, and then ranks again. stderr says
waiting until 14:05 for claude:work (5H resets) …; stdout stays the tool's, orpick's id or JSON. - If the soonest reset is past the cap, or no reset time is known, it exits 75 at once, and the
message says the cap that would reach it (
--wait=48h). Ctrl-C exits 130; nothing is launched or recorded. - Several waits that wake together take different accounts, as picks always do.
- Your own shell tool call times out long before a wait of hours ends. Use
--waitin a script, in cron, or in a command you give the user; from your shell, only with a cap shorter than your tool's timeout, or run it in the background. Ask before starting a wait of more than a few minutes.
A cron or script recipe, for the user:
bashclausona run claude --route nightly --wait=4h -- -p "review yesterday's commits" > review.md case $? in 0) ;; 75) echo "no account came free within 4h" >&2 ;; *) exit 1 ;; esac
cron has a short PATH (give it one that reaches clausona, claude and node) and no
terminal, so an unknown route exits 1 instead of asking.
Dir rules and defaults
A dir rule gives a directory a route per tool: in that directory and below, a plain
clausona run claude (or codex) with no --route or --from runs on it. The deepest rule
for the current directory wins. A default does the same everywhere no rule holds. With neither,
the run takes the tool's active profile.
bashclausona route explain --json # here: what each tool's run takes, and why clausona route dir # every dir rule and default clausona route dir ~/work work # in ~/work and below, run claude takes route work clausona route dir ~/work any --tool codex # an all route fills both tools unless --tool says one clausona route dir ~/work --clear [--tool codex] clausona route default main # elsewhere, run claude takes route main clausona route default --clear --tool claude # --tool is required to clear (claude, codex or all)
- These change what every plain run in that directory does, the user's own included. Say what
will change (
route explainfrom that directory shows what runs now) and wait for the user's OK before you set or clear one. - A route fills the slot of its own tool. An
allroute fills both, or the one--toolnames. A route of the other tool is refused. The route must exist first. - A path may be relative, absolute or start with
~; it is stored with~when it is under home. A rule matches its directory through symlinks, and ignoring case on macOS and Windows. route explain --jsonwith no name gives{"directory", "runs": [...]}, one entry per tool (--toollimits it to one). A routed entry is theexplain --jsonof its route, plustoolandskipped. ItsresolvedByisdir:<path>(the rule's path as stored, with~) ordefault. An entry with no route is{"tool", "route": null, "resolvedBy": "active", "profile", "skipped"},profilenull when the tool has no active profile.skippedlists the rules passed over as{"rule", "reason"},rulebeingdir:<path>ordefault.- A rule whose route is gone, cannot be used or is for the other tool is skipped: the run says so
on stderr and goes on to the next rule. A rule that cannot be read at all is named by
route list(inproblems); a run only says that there is one. Ifroutes.jsoncannot be read at all, a plain run says so in one line and runs the active profile; a run with--routefails as before.
Several workers at once
Take one turn per worker on a round-robin route, and start each worker on its own pick:
bashid=$(clausona route pick main); code=$? # one profile id on stdout; records the turn clausona run "$id" -- -p "$(cat <brief file>)" # start this worker in the background
Do both for each worker in turn, and check code every time: 0 starts the worker on $id; 75
means nobody is free, so start no more workers (next section); anything else is an error to show
the user.
- Each pick records the turn, so the next pick takes the next account under the cut. Never start
every worker on one
pickresult, and never on ids copied fromexplain, which records nothing. - With fewer accounts under the cut than workers, later picks come back to an account already in
use. Once every account is over the cut, a pick (stage
overflow) takes the one with the most left, not the next turn. Tell the user which workers share an account. - One
clausona run --route main -- -p …per worker also takes its own turn. Usepickwhen you want the id before you start. - On an
allroute a pick can be an account of either tool. Pick with--tool claude(orcodex) so every id is of the tool your worker command is written for. - On a
headroomorexpiringroute, picks made close together land on the same account (readings are cached for 5 minutes). Add--strategy round-robinto eachpick, or use a round-robin route. pick --jsonprints{"profile", "route", "stage", "usage", "reason"}. When nobody is free it still prints JSON, with"profile": nullandsoonest({"id", "at"}, or null), and exits 75.- When workers run a model with its own limit, pick with
--modeltoo (clausona route pick main --model opus), so a spent Opus limit is not picked for an Opus worker. - To wait for a free account instead of stopping at 75, pick with
--wait(above).
When nobody is free (exit 75)
Nothing was launched. Every account of the route is at 100% of a window, or was skipped. The
message on stderr lists each account with its usage and when it resets, or why it was skipped,
and marks the soonest reset (with --json, soonest.at). Tell the user which account frees up
first, and when, and which skipped ones clausona login <profile> would bring back. Workers
already started keep running. If the work can wait, offer the same command with --wait (or the
longer --wait=<n>h the message names) for the user to run.
A higher --max-usage does not help: past the cut, any account under 100% is already taken. Do
not run an account by name to get past the limit unless the user says so. If they do, name the
risk: that account is at its limit, so the session may stop at once.
When a run stopped on a usage limit
If it was a limit on one model (Claude's Opus limit, say), start it again with that model named
(-- --model opus), so the account whose limit on it is spent is not picked again.
Otherwise start it again on the route. The spent account is now at or above the cut, so another one is
picked. But readings are cached for up to 5 minutes, and with every account over the cut, a
cached reading under 100% can pick the spent one again: if explain would still pick it, or
shows it eligible, add --exclude with it for this run. The route's own excludes still apply.
To carry on the same session, pass -c or --resume after --; that run only goes to accounts
that share sessions.


