Skip to content

fix(runtime): let a subscription ClaudeAgent use CLAUDE_CODE_OAUTH_TOKEN on headless hosts (#1266) - #1294

Merged
aviggiano merged 2 commits into
unstablefrom
fix/1266-claude-oauth-token-headless
Oct 14, 2026
Merged

aviggiano merged 2 commits into
unstablefrom
fix/1266-claude-oauth-token-headless

Conversation

@mrthankyou

@mrthankyou mrthankyou commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1266.

Problem

On a headless host (container, CI box) Claude Code's documented non-interactive login is a long-lived token from claude setup-token, exported as CLAUDE_CODE_OAUTH_TOKEN. Ultrafuzz dropped it at two layers, so every auth = "subscription" Claude task failed the engine preflight with Claude Code is not logged in:

  1. Launch. agentEnvironmentVariableNames gave the controller only each agent's api_key_env; subscription mode has none, so the token never reached the controller.
  2. Allowlisted, it was blanked anyway. The name contains TOKEN, so an allowlisted copy counted as a credential and was blanked for every model child. The Claude route only restores credentials under its prefixes (ANTHROPIC_, CLAUDE_CODE_USE_, AWS_, …), which this name does not match. A plain resume would also have removed it as a sensitive allowlisted value.

Fix

The token now follows the path stock API keys already use.

  • agentAuthenticationEnvironmentVariableNames (provider-credential-environment.ts) names the variables an agent's adapter reads for its configured auth: its api_key_env (plus MOONSHOT_API_KEY for Kimi), or CLAUDE_CODE_OAUTH_TOKEN for a subscription ClaudeAgent. start-run.ts uses it for both the controller environment and the provider-credential list, replacing two copies of the api-key logic. As a listed provider credential the token survives resume and is blanked for every other route.
  • environment.tsx adds CLAUDE_CODE_OAUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR to the built-in provider credentials, so they are blank for every model child by default (DeepSeek already blanked both).
  • claude.tsx: subscription mode passes the token back to Claude only, and carries it into the command environment, which Smithers applies last when it spawns ({...process.env, ...opts.env, ...command.env}). Putting it in opts.env alone was overridden by the blanked default, which the new adapter test caught. api-key mode never receives it.
  • validate warns with AGENT_ENV_ALLOWLIST_CREDENTIAL_BLANKED when ULTRAFUZZ_AGENT_ENV_ALLOWLIST names a credential that no selected agent uses, since every agent would receive it blank. For CLAUDE_CODE_OAUTH_TOKEN it says to use auth = "subscription". This covers the issue's "make a stripped credential visible" point without touching the engine's preflight message.
  • Docs: the headless claude setup-token option next to auth = "subscription" in docs/config.md; docs/security.md and CHANGELOG.md.

Like an API key, the token is visible to the agent process, including its Bash tool; that is the existing trust model for agent credentials. The secret gate recognizes it by name (*_TOKEN) and by format (sk-ant-oat01-). Raw transcripts in report bundles are tracked separately in #1289.

Testing

All in a Linux container.

Suite Result
runtime supporting suite (every file but runtime.test.ts) 1002 / 1003 — the one failure (pinned source proof) fails identically on unstable
runtime.test.ts: #1266, all startRun, credential, environment, provider-home and resume tests 95 / 95
Bun adapter contracts 48 / 48

New tests:

  • Bun adapter: the generated subscription Claude adapter's command carries the token; Codex, Kimi and OpenRouter children get it (and its file-descriptor variant) blank, even when it is allowlisted.
  • Launch: a subscription Claude run's controller carries the token and names it as a provider credential; a Codex run gets nothing.
  • validate: the warning fires for unowned allowlisted credentials, with the Claude hint, and not for the token when a subscription Claude agent is selected.
  • One existing assertion now expects CLAUDE_CODE_OAUTH_TOKEN in a subscription Claude run's provider-credential names (a name, not a value).

End-to-end with a fake claude (not committed). A fake claude on PATH answered claude auth status as logged in only when the token was in its environment, and logged what each invocation received. Both runs used an empty HOME, the token exported, ClaudeAgent on claude-opus-4-8, and --audit-profile smoke, through the real engine, preflight and process launch:

unstable this PR
token in the engine preflight's claude auth status absent present
engine preflight api_key_valid=fail: Claude Code is not logged in (the #1266 error) passed
Claude agent processes none launched with the token, --model claude-opus-4-8

Optional reviewer check with a real token: claude setup-token, then on a host without a ~/.claude login, export CLAUDE_CODE_OAUTH_TOKEN and run ultrafuzz run --audit-profile smoke with a subscription ClaudeAgent profile. #1266 already showed that Claude Code reports loggedIn: true with the token set; this PR is what gets it to the agent.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new actionable issues were found.

Summary

Subscription-mode ClaudeAgent receives CLAUDE_CODE_OAUTH_TOKEN on headless hosts. Other model children keep it blank.

  • Subscription Claude agents can use headless setup tokens.
  • Validate warns when an allowlisted credential has no selected agent.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Operator environment] --> B[Selected agent authentication]
  B --> C[Controller environment]
  C --> D[Blank provider credentials]
  D --> E[Subscription Claude restores its token]
  D --> F[Other agents keep token blank]
Loading

Reviews (2) · Last reviewed commit: "fix(runtime): warn when an allowlisted b..." · Reviewed by Greptile

…KEN on headless hosts (#1266)

Claude Code's documented non-interactive login is a long-lived token from
`claude setup-token`, passed as CLAUDE_CODE_OAUTH_TOKEN. Ultrafuzz never
gave it to the controller, and when allowlisted it was blanked for every
agent because no route owned it, so on a headless host every
subscription-mode Claude task failed the engine preflight with "Claude Code
is not logged in".

A subscription ClaudeAgent now consumes the token the way an api-key agent
consumes its key: the controller receives it and names it as the Claude
route's provider credential, which keeps it through resume and blanks it
for every other model child. The built-in provider credential list blanks
the token and its file-descriptor variant by default, and the Claude
adapter restores it only in subscription mode. The adapter carries it into
the command environment, which Smithers applies last when it spawns.

`ultrafuzz validate` now warns with AGENT_ENV_ALLOWLIST_CREDENTIAL_BLANKED
when ULTRAFUZZ_AGENT_ENV_ALLOWLIST names a credential that no selected
agent uses, since every agent would receive it blank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrthankyou
mrthankyou requested a review from a team as a code owner October 13, 2026 15:50
Comment thread packages/runtime/src/validate.ts Outdated
…no agent (#1266)

The validate warning skipped any allowlisted credential under a selected
route's prefix, so subscription-mode Codex with OPENAI_API_KEY allowlisted
gave no warning. A route never restores a built-in provider credential from
the allowlist (workflowControlChildEnvironment blanks them, and subscription
Codex blanks OPENAI_API_KEY itself); only the provider's own configured
authentication does. Built-in credentials no longer count as route-owned for
the warning.

The built-in provider credential list now lives in
provider-credential-environment.ts, and the agent environment template loads
it from @ultrafuzz/runtime with the classification functions it already
loads, instead of keeping its own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aviggiano
aviggiano merged commit 1e6eb30 into unstable Oct 14, 2026
17 checks passed
@aviggiano
aviggiano deleted the fix/1266-claude-oauth-token-headless branch October 14, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No headless-Linux path for Claude subscription agents: CLAUDE_CODE_OAUTH_TOKEN never reaches them, even when allowlisted

2 participants