cyberine
UsageRun recipes

Which Claude account a worker bills — oauthToken (k8rhz)

CLAUDECODEOAUTHTOKEN is a long-lived subscription credential that decides WHICH SEAT a claude worker bills. It used to reach every spawned worker by plain inheritance: T603's least-privilege env

CLAUDE_CODE_OAUTH_TOKEN is a long-lived subscription credential that decides WHICH SEAT a claude worker bills. It used to reach every spawned worker by plain inheritance: T603's least-privilege env scoping only recognized *_API_KEY / *_TOKEN_KEY names, and this one is neither, so the account a fan-out wave spent was whichever the launching shell happened to export — invisibly, and differently depending on how the session was started.

It is now credential-shaped (isProviderCredentialEnvKey also matches *_OAUTH_TOKEN) and is NOT on the claude allowlist, so buildScopedChildEnv strips it from every spawned worker. With no token in the child env the worker falls back to the keychain login — the same account claude uses interactively. To pin a specific seat, pass it explicitly:

import { makeClaudeAdapter } from './src/adapters/CliAgentAdapter.ts';

// no oauthToken: the worker sees no token at all and uses the keychain login
const defaultWorker = makeClaudeAdapter({ model: 'claude-sonnet-5' });

// explicit seat: injected via envSet, applied AFTER the credential strip
const pinnedWorker = makeClaudeAdapter({
  model: 'claude-sonnet-5',
  oauthToken: process.env['MY_SECOND_SEAT_TOKEN'] ?? '',
});

The scoping is covered with no network call by:

npm run selftest:claude-oauth-token-scoping

Known limitations and deferred work:

  • There is deliberately no env default. Reading CLAUDE_CODE_OAUTH_TOKEN from process.env inside the factory would re-create the ambient inheritance this change removes, one layer up. The caller names the seat or the worker uses the keychain.
  • One seat per dispatch is still all the fleet can pick. oauthToken is the seam only; selecting a different account per worker across one fan-out is kgjhf, and what a seat looks like when its quota runs out has not been measured (kzdc7).
  • The --bg worker is out of reach of this scoping. A claude --bg job is owned by the CLI's daemon, so it inherits the DAEMON's environment rather than the one Cyberine builds — the same reason that path refuses to run at all when a metered key is reachable (assertMeteredEnvReachable). Its account is decided by the daemon's own environment.
  • aider / qwen / crush still inherit it, along with every other provider credential: their credential cannot be determined from the command alone, so they keep full inheritance by T603's directive (breaking a worker's auth is worse than an over-broad env). Unchanged here, and asserted by the smoke so it stays a known gap rather than an accident.