Skip to content

Accounts and usage limits

For anyone running agents on a subscription plan. After this page you will know how to give a session its own credential identity, what happens when an agent hits its plan's ceiling, and the three ways out of that wall — retry, hand off, or resume automatically.

Accounts

An account is one of an agent's logged-in identities, held as a directory the agent CLI treats as its home. af never reads, stores, or forwards the credential itself: it decides which directory a session sees, and the agent's own login flow puts the material there.

Credential directories

Accounts are registered separately for Claude, Codex, and Gemini. The directory is under accounts/<agent>/<name> in the AF home; the same name for two agents does not share a login. af accounts add <agent> <name> prints the directory path, which has these per-agent meanings:

Agent Environment variable Directory shape
Claude CLAUDE_CONFIG_DIR The config directory itself
Codex CODEX_HOME The config directory itself
Gemini GEMINI_CLI_HOME A home-like root; Gemini appends .gemini/

Point Gemini at the printed root, never its nested .gemini directory. af recognizes either <root>/.gemini/oauth_creds.json or <root>/.gemini/gemini-credentials.json as a credential artifact.

Log in

Install each agent you want to log into on the machine running the daemon first. For example, Codex login needs codex on the daemon's PATH, even if your sessions normally use another agent. Finding it in your current shell is not enough if that shell's PATH changed after the daemon started. From a shell with the corrected PATH, run af daemon restart. If you use an installed autostart service, first rerun af daemon install from that shell to replace the service's captured PATH; restarting alone reuses that snapshot. Run the login command in an interactive terminal.

af accounts login claude work
af accounts login codex work
af accounts login gemini work
af accounts list

Each login registers the account if needed and opens the agent's own login in a tmux pane on the daemon host. Complete sign-in in a browser on the device you are using. The flows differ:

Agent What af launches What you do
Claude claude auth login with BROWSER=true, a no-op browser opener Open the printed URL and paste the authorization code into the pane if prompted
Codex codex login --device-auth Open the verification URL and enter the device code there while the CLI polls
Gemini gemini with NO_BROWSER=true Open the URL, sign in, then paste the authorization code at the terminal prompt

Afterwards, af reports whether a credential file exists. It checks presence, never reads the secret; presence is not proof that a token is still valid. af accounts login codex work --no-attach starts the flow and prints how to attach instead of taking your terminal.

To stop before signing in, press ctrl+c in the login pane: the account stays registered but af accounts list reports not logged in, and the login command reports that no credential was created. Re-run the login command when ready to finish. A named Codex account has separate credentials, history, and skills.

Codex registration seeds missing approval_policy · sandbox_mode · model from the operator's ~/.codex/config.toml, with the selected profile overriding root values; existing account settings, including selected-profile values, stand. For effective workspace-write mode it also seeds missing workspace options (network_access · writable_roots · exclude_tmpdir_env_var · exclude_slash_tmp). It skips model if either config selects model_provider at root or in its selected profile, and seeds nothing if the ambient config has neither approval policy nor sandbox mode. Credentials, provider configuration, profiles, and project trust are never copied. An unresolved ambient profile or an ambient approval, sandbox, or active workspace option that fails schema validation prevents all seeding; unreadable or unparseable ambient files also leave settings alone. Both af accounts add and af accounts login print a notice explaining the seeding policy and any refusal.

Gemini's registration pre-answers folder trust for the account directory and selects Google OAuth in that account's non-secret settings. Existing choices are preserved, so an account with a different authentication method can still show the picker. This touches only the account's own .gemini settings; it neither authenticates you nor writes a credential. Claude and Codex need no such settings to reach sign-in.

The equivalent manual setup is:

CODEX_HOME=$(af accounts add codex work) codex login --device-auth
CLAUDE_CONFIG_DIR=$(af accounts add claude work) BROWSER=true claude auth login
GEMINI_CLI_HOME=$(af accounts add gemini work) NO_BROWSER=true gemini

Use the managed login to get af's identity environment isolation too: it removes competing identity variables while preserving proxy and certificate settings. Explicit BROWSER or NO_BROWSER entries in session_env_passthrough override the browser-free defaults.

Pick an account

After completing sign-in, pin a session to that account and its agent (a Codex account cannot be used by the default Claude agent):

af sessions create --name spike --program codex --account work

In the TUI, press n to open the naming form, then ctrl+o to pick an account for the selected agent. In the web client, use New session · Program · Account. The picker lists that agent's accounts and shows the project default; changing the program changes the account list. Register and log in from the web config view's Accounts section using Log in. See the TUI guide and web guide.

af refuses an unproved fallback — a session either runs as the account you named or does not start — and by default never rotates accounts on its own. Rotating after a usage limit is opt-in and needs an explicit candidate list — see Opt-in auto-resume. A session created with --account is pinned and automatic switching never rotates it. The full command surface is in af accounts.

Scoping an account to a project

Typing --account on every create is the wrong unit of work when the answer is always the same for a whole repository. default_accounts says it once — per agent, and most usefully per project:

af projects add ~/work/monorepo                                 # once
af config set default_accounts.codex work --project ~/work/monorepo
af config set default_accounts.codex personal --project ~/side/hobby

Now a session created in that project — from the CLI, the TUI, the web client or a scheduled task — runs as work with nothing typed. The resolution order is explicit --account (or a picker choice) → the project's default → the global default → the agent's ambient login, and it is applied by the background service on the create, so every surface gets the same answer.

The default has these properties:

  • It is keyed by agent, because an account belongs to one. claude's work and codex's work are different identities in different registries, so default_accounts.codex never applies to a claude session — that session keeps the ambient identity until you scope it too.
  • The pickers show it. The TUI's ctrl+o field and the web's Account select preselect the project default and mark its row project default, so a scoped create is visible before you press Enter rather than discovered afterwards.
  • A default af cannot honour refuses the create, naming the key, the file it is set in, and how to clear it. It never falls back to the ambient identity: a session quietly running as someone else is the failure the whole feature exists to prevent.
  • A project's always-on root agent is deliberately not scoped by it. Its command comes from [root_agent] rather than an agent name, and a preference set about ordinary creates must not be able to stop a project's guaranteed session from starting. Scoping a root belongs beside the program it already names.

Set it globally (af config set default_accounts.codex work, no --project) for a default everywhere. It is rejected from a repository's checked-in .agent-factory/config.toml: identity policy is never checked in, and a committed account name means nothing to anyone else who clones the repository.

To clear one:

af config unset default_accounts.codex --project ~/work/monorepo  # drop the project's override
af config set default_accounts.codex ""                           # clear the global default
af config set default_accounts.codex "" --project ~/work/monorepo # opt this project out of the global default

An empty value is meaningful, and the three lines differ. unset removes the project's entry so the global default applies again; an empty value at either layer means "no default", and because the key merges per agent, an empty entry in a project's config wins for that agent and keeps its sessions on the ambient identity even when the global layer names an account.

What the account reaches

A pinned account applies to every process af starts for that session: the agent pane, each helper tab, and the VS Code tab's editor — its integrated terminals and anything an extension shells out to included. Each one gets the account's credential home injected and the agent's other identity variables removed, so an ANTHROPIC_API_KEY left in your shell cannot quietly outrank the account you selected. If the account cannot be resolved when a VS Code tab opens — it was removed or renamed since the session was created — af refuses to start the editor and the pane names the af accounts add that repairs it, rather than opening one on your ambient login.

One boundary af cannot hold for you: an interactive shell you open inside the editor reads your ~/.bashrc and /etc/bash.bashrc, and a line there exporting the agent's credential variable wins over the account — exactly as it would in any other terminal. af's own panes escape that because af chooses their command; a terminal profile inside the editor is yours, and it lives in the editor's own settings, which the editor can rewrite. If you pin sessions to accounts, keep agent credential variables out of your shell startup files.

Docker and backend boundaries

Account selection supports local and Docker sessions and requires tmux 3.2 or newer. Docker bind-mounts the selected credential directory read-write so token refreshes reach the real account. This differs from docker.mount_agent_credentials, which opts into mounting an ambient agent credential read-only. Account selection must establish the selected identity or refuse the create; it never silently uses an ambient login.

SSH, sandbox, and hook backends refuse account selection because af cannot ensure refreshed credentials are written back. Automatic account replacement also excludes Docker; see Opt-in auto-resume.

Account-scoped handoff

Local account-scoped sessions support both same-agent and cross-agent handoffs. Use af sessions handoff <session> --account <name> to choose another registered account of the current agent. Add --to <agent> to change agent and account together. The target account must belong to the incoming agent and must not be currently walled in the daemon's limit ledger.

An explicit --account selects a pinned identity: an existing pin moves to the chosen account, and an ambient session becomes pinned to that account. For an ambient session, --to <agent> without --account keeps the replacement ambient. A session already scoped to an account must specify a target account when changing to an agent whose resolved command supports accounts (claude, codex, gemini as shipped); omitting it does not bypass the pin. Handing it to a target whose resolved command has no account support drops the scope instead, and the response reports the drop on from_account. Capability follows the resolved command rather than the enum, so program_overrides moves a target between the two cases: aider redirected to codex requires a codex account, and codex redirected to aider drops the scope. A third case refuses outright: a target whose resolved command af cannot classify as an agent at all (a wrapper such as npx codex may launch an account-capable agent underneath) can neither be proven to carry the scope nor proven safe to drop it, so the handoff refuses rather than destroy a durable pin on an unproven answer — point the override at a literal agent command to make it classifiable. Because a scope drop restarts only the agent pane, a session with shell, process, or VS Code sibling tabs is refused until those tabs are closed — they would keep running under the dropped account's environment.

A scope drop is one-way: the session records the dropped name on the handoff's from_account and runs ambient from there, but a later handoff back to an account-capable agent does not restore it — name the account again with --account.

The session keeps its worktree and branch, and the new conversation receives the handoff brief. See Hand off to another account for examples, admission checks, and the TUI and web pickers.

Bug report redaction

Bug report bundles replace session account fields and registered account labels with a redaction marker, including labels embedded in collected config and log text. Credential directories and their secrets are not collected. The local operator log retains account names so login and switching remain diagnosable; label redaction happens when the bundle is assembled. If the registry cannot be read, collection still produces a bundle but reports that account labels could not be redacted. Review that collection error before sharing it.

Usage limits

Subscription-plan agents (claude, codex, devin) can hit a plan usage-limit wall: the CLI stops working and prints a banner like "Claude usage limit reached. Your limit will reset at 2pm (America/New_York)" or "You've hit your usage limit … try again at Jul 25th, 2026 5:55 PM". The agent is not dead — it is parked until its limit window resets.

Agent Factory detects that state, surfaces it, and can bring the session back on its own once the window elapses. This page covers the whole flow end to end.

What is detected

  • claude and codex — their usage-limit banners are recognized, and when the banner states a reset time it is parsed into an absolute instant. These are the plan-metered agents that stall at a dead prompt with a reset window, so they are the ones whose reset time schedules a timed auto-resume.
  • devin — detected too (#2411), but detect-only. Its exhaustion banner is inferred from the binary and docs rather than captured live, and it carries no reset time, so a devin limit gets the badge and the manual c retry but never a scheduled (timed) auto-resume — only the limit_retry_interval fallback if limit_auto_resume is on. Its healthy N% remaining / resets in … quota-status line is deliberately not treated as a limit.
  • gemini and aidernot detected. They are API-key-metered: a "limit" there is a transient HTTP 429 the CLI already retries, with no plan reset time to schedule against. They are surface-only in the sense that nothing special happens — no badge, no auto-resume.
  • opencodenot detected, deliberately. It is API-key-metered against your own Anthropic/OpenAI credentials, and its TUI reports spend ($0.10 spent) rather than a plan wall — there is no plan-reset banner to match and no reset window to schedule against, so af ships no usage-limit matcher for it.

Detection runs on captured pane content, so it needs no agent cooperation. You can tune the detection regex per agent with limit_patterns.

Claude reset times honor a parenthesized timezone accepted by Go's time.LoadLocation, including single-component names such as (UTC) and (Japan) and slash names such as (America/New_York) or (Etc/GMT+5). When several parenthesized groups appear, the first loadable zone wins. If the clock parses but no zone loads, one warning names the banner and all rejected candidates, and parsing falls back to the daemon's timezone. An unparseable clock does not warn or suppress a later warning. Repeated warnings are suppressed by a process-local cache of the last 64 distinct banner-and-candidate keys; an evicted banner can warn again. A banner without a zone also uses the daemon's timezone. The sidebar displays the resulting instant in your local timezone; auto-resume uses that same instant plus its grace buffer.

The [limit] badge

When the daemon's status poll sees a usage-limit banner for a claude, codex, or devin session, it marks the session LimitReached. In the sidebar the row shows a [limit] badge, and — when the banner carried a parseable reset time (not devin, which carries none) — when the limit resets:

▸ fix-auth-bug   [limit] resets 2:00 PM

[limit] means "parked at a usage-limit wall", distinct from working, idle (ready), or dead. The badge clears automatically once the session resumes work (whether you resume it, the daemon auto-resumes it, or the banner scrolls away on its own).

Manual retry

Resume a limit-blocked session immediately from any surface:

  • TUI: select it and press c.
  • Web: select it and click Retry limit.
  • CLI: run af sessions retry-limit <title> (with --repo when needed).

Every surface calls the same daemon recovery action. It:

  1. Re-spawns the agent if its tmux session exited while blocked (the rare case); a live stall just gets nudged.
  2. Re-delivers the pending prompt — a task-driven session re-sends its stored task prompt so it resumes its actual work; an interactive session with no stored prompt is sent a bare continue (which loses the agent's prior in-context state, an unavoidable limitation — see anthropics/claude-code#5977).
  3. Clears the [limit] badge so the poll re-resolves the real state.

The TUI's c binding is rebindable like any other via the [keys] table (action limit_retry); see configuration.md.

Opt-in auto-resume

By default a limit is surface-only: the badge plus the manual retry. Set limit_auto_resume = true in your global config to let the daemon resume a parked claude/codex session on its own once its limit window elapses (a reset-less banner — including every devin limit — resumes on the limit_retry_interval fallback below instead):

limit_auto_resume = true
limit_account_candidates = ["work", "personal"] # optional account-switch order
limit_retry_interval = "30m"   # fallback cadence when a banner states no reset time
  • Parsed reset time → the daemon resumes shortly after that time (a small grace buffer is added because limit windows are rolling and approximate). A reset time already in the past resumes promptly.
  • No parseable reset time → the daemon retries on the fixed limit_retry_interval cadence. Set it to empty or 0 to leave such a session surface-only.
  • Re-limit backoff → if a resumed session immediately hits the wall again, the daemon backs off exponentially (settling at one attempt every 5 minutes) rather than hammering an exhausted plan. Killing the session is always the off-ramp.
  • Optional account switch → before waiting for the old identity's reset, an unpinned local session may restart on the first registered account in limit_account_candidates that has no current limit observation. The ordered list is the opt-in; af never chooses from every registered account. If every listed account is limited or absent, the existing wait remains. Docker account-scoped creates remain supported, but automatic Docker replacement stays disabled until its complete provision plan and container cleanup identity can survive a daemon crash.
  • An explicit --account is pinned. It is a pin, so automatic switching never overrides it. Accounts selected by the scheduler may move again after they later hit their own limit.
  • No invented quota claim. Providers expose no quota API af can read. The scheduler uses the same honest evidence as af quota: a candidate is rejected when af currently observes one of its sessions at a limit; absence of that observation is not presented as a provider guarantee.
  • Visible in the session. The first prompt after replacement names the old and new identities before repeating the stored task prompt.
  • One credential boundary. A local swap stops every agent, shell, and process pane before committing the new identity, then restores the agent and shell panes with the selected account environment. A claude or codex agent keeps its conversation: af copies the transcript into the new account's home before committing the new identity, and the replacement resumes it. The copy stays in the new account's home, and the provider replays that history under the new account, so an automatic rotation moves the conversation's content to the candidate account as well as the work. If the conversation cannot be carried, the agent starts a fresh conversation, and the notice it receives says why. Other agents start a fresh provider conversation. New account-scoped terminal tabs remain interactive but skip shell startup files, because an rc file can otherwise replace the selected identity after af has established it. A resolved command that explicitly pins --continue, --resume, --session-id, or codex resume would take away af's choice of which conversation the replacement opens, so af names those arguments and keeps the existing wait instead.
  • Operator-only. limit_auto_resume, limit_retry_interval, and limit_account_candidates are rejected in in-repo configs. A save through af config set applies them to the running daemon at once — no restart. The candidate list may also be replaced in a machine-local personal-project config, so one project can use a narrower set without trusting checked-in repository config with identity policy.

Set the global ordered list with:

af config set limit_account_candidates work,personal

Or add --project <id-or-path> for a personal per-project override.

Full config reference: configuration.md.

Hand off to another account

To continue under another registered account of the same agent:

af sessions handoff fix-auth --account personal

The session keeps its identity, worktree, branch tip, and stored prompt. For claude and codex it also keeps the conversation. Each account has its own provider home (CLAUDE_CONFIG_DIR or CODEX_HOME), so after stopping the outgoing agent af copies that conversation's file into the new account's home: the transcript for claude, the rollout for codex. The new account then resumes the same conversation id. The copy only ever adds to the new account's home; nothing in the previous account's home changes.

The copied transcript is kept in the new account's home, and from then on the provider replays that history under the new account, whose credentials send it. Treat a handoff, manual or automatic, as moving the conversation's content to that account.

The new account starts a fresh conversation instead, and its brief says af tried to carry the conversation and why it could not, when:

  • the file is missing, or reached through a symbolic link;
  • the new account already holds a different version of it;
  • the conversation af recorded is no longer the newest one in this worktree, because a new one was started with /clear or /new;
  • a replacement already launched on the carried conversation stopped before it was confirmed working.

Other agents always start fresh with a brief. Use --brief to replace the prompt, or combine --to claude --account work to change both agent and account; changing the agent always starts a fresh conversation. The recorded handoff includes the outgoing and incoming accounts and branch tip.

If an agent or account handoff starts its replacement but cannot confirm whether the mission was submitted, af suppresses automatic redelivery because the first submission may already have landed. Inspect the replacement pane, then choose the explicit override when it is needed: press c in the TUI (shown as Retry), click Retry handoff in the web pane header, or run af sessions retry-limit <title>. The retry verdict is attached to that pending handoff mission; sending another prompt does not make the daemon resend it. If that explicit retry delivers the mission but cannot persist its final settlement, every client reports the retry as completed with a warning. The TUI shows the warning as a completion message, the web keeps it in the confirmed mutation notice, and CLI JSON includes a warning field; do not retry the already-delivered mission.

This is an operator-chosen account swap: the target must be registered for the incoming agent and have no current limit observation in the daemon's ledger. An unregistered or currently limited target is refused before stopping the old runtime. No limit_auto_resume or limit_account_candidates setting is needed. An explicit pin moves to the chosen account and remains pinned against automatic rotation. The same local account-swap path replaces all credential-bearing panes; its launch checks and restrictions, including VS Code tabs, still apply.

In the TUI, press F on the [limit] session and choose another account. The project default is preselected when it is another registered, logged-in account. In the web, use Handoff beside Retry, then choose the agent and account. Both pickers list accounts belonging to the selected agent.

Hand off to another agent

Waiting is not the only option. If the work should not sit until the window resets — a day, sometimes several — hand the session to a different agent:

af sessions handoff fix-auth --to claude

In the TUI, press F on the selected session, pick the agent, and confirm. The key is advertised on the status bar for a limit-blocked session, next to the c retry, because the two are the two answers to the same wall: c waits for this agent's window, F continues under another one.

What a handoff does, and does not do:

  • The session is the same session. Same worktree, same branch, same tabs, same task binding, same name. Only the agent process is replaced. Nothing is archived, nothing is re-cloned, and uncommitted work is untouched — it is simply still there, because the worktree never moved.
  • A different agent starts fresh, with a brief. Agent conversations are not portable between providers: claude cannot read codex's transcript and vice versa. So instead of a transcript, the incoming agent is told the session's goal, that it is continuing someone else's work, and where to look (git log/git diff on the branch). It is explicitly told not to start over.
  • The swap is recorded. af notes which agent handed off to which, and the branch tip at that moment. That tip is the attribution boundary: everything up to it is the outgoing agent's work, everything after is the incoming agent's. af cannot label the commits themselves — your agent writes them — so the recorded tip is what lets a reviewer check the split rather than take it on trust.

Use --brief when the stored goal is stale or too broad, which is common on a long-running session:

af sessions handoff fix-auth --to gemini --brief "just finish the retry test; leave the docs alone"

Any supported agent can be a target. Two things worth knowing rather than being blocked on:

  • Approval policy belongs to the target agent. A handoff starts the target's resolved command and configuration; it does not carry approval settings from the outgoing agent. See Agent approval behavior.
  • Local-worktree sessions only. A docker/ssh/hook session runs its agent inside a provisioned sandbox, where swapping the agent is a different lifecycle; those sessions refuse the handoff rather than half-perform it.

Handing off is reversible. Each agent's conversation history is stored per directory, so the outgoing agent's thread is still in the worktree — hand back to it once its limit resets and it picks up its own conversation.

There is no automatic handoff. A swap changes which agent is editing your branch, so it is always something you ask for.

Task runs: park, don't fail

A task (cron or watch) can fire while your plan is already exhausted. When a task-driven session hits a usage-limit wall as it starts up, or a task targets a session already marked [limit], Agent Factory parks the run instead of typing into the limited pane or reporting a false success:

  • The session is kept, not torn down, and marked [limit] (with its reset time) so the badge, the manual c retry, and auto-resume all apply to it.
  • The task's run status is recorded as parked: usage limitnot an errored/failed run. It shows in the task manager as waiting for the limit window, and no failure side-effects fire.
  • For a newly created task session, once the window resets the same resume machinery takes over: auto-resume (if limit_auto_resume is on) or your manual c retry re-delivers the session's stored task prompt, and the run proceeds to completion. A parked run becomes a completed one — never a failed one.

For a task with target_session, trigger kind determines what parking retains. A cron occurrence is skipped and recorded parked: usage limit; the first cron fire after reset runs normally, without replaying a burst of identical prompts. A watch event is distinct data, so it enters the durable FIFO and replays after the target leaves [limit]. Limit-held watch backlogs are not expired at the ordinary 72-hour outage boundary. Once the bounded queue reaches 500 events or 256KB, Agent Factory stops reading the watch command's stdout and lets pipe backpressure hold the producer until replay makes room, rather than dropping an event or growing managed queue storage without limit.

This automatic park is deliberately task-only. A manual af sessions send-prompt still delivers to a [limit] session, because typing may be needed to answer a credits or limit picker. Do not type into a timed auto-continue banner: Claude explicitly treats any typing as cancellation. In a Codex numbered picker, use the arrow keys to select the intended choice before sending Enter; digits are ignored and Enter accepts whichever choice is already highlighted.

Before this behavior, such a run spun a readiness timeout and was recorded as a failure even though nothing was actually wrong — you'd just hit your plan limit.

Scope summary

Agent Detected [limit] badge Manual c retry Auto-resume Task park Handoff target
claude
codex
gemini
aider
amp
opencode
devin fallback

Auto-resume is timed for claude/codex because their banners carry a parseable reset window. devin is detected but carries no reset window, so it has no timed auto-resume — only the limit_retry_interval fallback cadence when limit_auto_resume is on (the same path a claude/codex banner with an unparseable reset time takes). The remaining agents are not detected: they expose no plan-reset banner, or are API-key-metered (transient 429s the CLI retries) with no plan window to schedule against.

The last column runs the other way, and deliberately so. Detection answers "can af tell this agent hit a wall", which claude/codex/devin support — so only they can be handed from on a limit. Being handed to needs nothing from the agent at all: af stops one process and starts another in the same worktree, so every supported agent is a valid destination.

Custom detection patterns

If an agent reworded its banner, override the detection regex per agent with limit_patterns (the built-in reset-time parser is kept):

[limit_patterns]
claude = "Claude usage limit reached\\."
codex  = "You['’]ve hit your usage limit"

Keys must be a supported agent (claude, codex, aider, gemini, amp, opencode, devin); an override for an agent with no built-in matcher (aider/gemini/amp/opencode today) is ignored, and an uncompilable regex warns and falls back to the built-in default. A devin override replaces its inferred detect pattern (still detect-only — there is no reset parser to keep). See configuration.md.