Skip to content

Agent harness credentials

Zimmer keeps a pool of vendor accounts and rotates between them when one hits its rate limit. That’s the feature. Underneath it is the most fragile machinery in the project, and it’s fragile for a reason that isn’t Zimmer’s fault.

ClaudeAccount is the pool for both runtimes, discriminated by a runtime column (claude_code | codex). The naming is a leftover.

Everything goes through RuntimeAuthProvider.for(runtime)ClaudeAuthProvider or CodexAuthProvider.

ClaudeCodex
Token endpointplatform.claude.com/v1/oauth/tokenauth.openai.com/oauth/token
Client ID9d1c250a-e61b-44d9-88ed-5944d1962f5e (the CLI’s public ID)app_EMoamEEZ73f0CkXaXp7hrann
Files~/.claude.json (identity), ~/.claude/.credentials.json (tokens)~/.codex/auth.json
Token TTLfrom expiresAt (~8h, inferred)24h, inferred — auth.json has no expiry field
RotationAccountRotationService, 5-minute intervalinline in the provider, 24h
Identity check on captureemail must matchnone

RefreshRuntimeAuthTokensJob runs every 5 minutes:

The refresh threshold is 15 minutes on a 5-minute cron — three chances to catch a token before it expires.

Refresh tokens are single-use and rotating

Section titled “Refresh tokens are single-use and rotating”

Every refresh returns a new refresh token and invalidates the old one — and invalidates the sibling access token. Two consequences the code has to defend against:

  1. The new pair must be persisted atomically. A crash between “got new tokens” and “wrote them” bricks the account. refresh_token! writes both in one update!.
  2. A future expiresAt is not proof a token is live. If someone else refreshed, your still-unexpired access token is already dead. The code does not enforce this — token_expired? still keys purely off expiresAt. The defense is the completeness invariant.

A credential set with no refresh token is a dead end. ClaudeAccount.complete_claude_oauth? refuses to persist or adopt one, because the CLI sometimes rewrites .credentials.json without the claudeAiOauth block at all, and adopting that blindly would brick the whole pool.

Here is the structural problem the marker solves.

In the deployment shape this code was written for, ~/.claude.json (identity) is container-local while ~/.claude/.credentials.json (tokens) is a shared bind-mount. So any code that reads the local identity file to decide who owns the shared tokens gets a confidently wrong answer on the wrong container. That is the root cause of the 2026-06-11 cross-account token-contamination outage.

The fix: a marker file, ~/.claude/.ao-credentials-owner.json, written next to the shared tokens, recording which account they belong to. filesystem_credentials_owned_by_self? gates the sync.

When an account hits its rate limit, Zimmer rotates to the next one by priority:

  1. Sync the outgoing account’s tokens off disk.
  2. Snapshot its quota state.
  3. mark_quota_exceeded!.
  4. activate_next_account — which validates the candidate by calling refresh_token! before activating it, so a broken account is skipped before it can brick the pool.
  5. Write the new account’s config and credentials to disk, stamp the owner marker.
  6. Record an AccountRotationEvent.

QuotaResetCheckerJob (every 15 min, Claude only) restores quota_exceeded accounts when either window’s reset time has passed, or utilization drops below 100%.

AuthRecoveryService watches the transcript for "Not logged in · Please run /login" (matched by /not logged in|please run\s*\/login/i), re-injects credentials to disk, and re-spawns the process. Bounded by MAX_RECOVERY_ATTEMPTS; returns :unrecoverable if no account is available.

The “Authenticate” button drives a PTY-screen-scraping flow:

RuntimeLoginAttempt is being used as a cross-container message bus: the web process writes pasted_code, the worker process reads it. That’s why poll_state must use RuntimeLoginAttempt.uncached — ActiveRecord’s per-request query cache would otherwise hide the write. It’s documented at length in the code, and it’s a landmine.