Repository navigation
fix(runtime): let a subscription ClaudeAgent use CLAUDE_CODE_OAUTH_TOKEN on headless hosts (#1266) - #1294
Merged
Conversation
…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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 asCLAUDE_CODE_OAUTH_TOKEN. Ultrafuzz dropped it at two layers, so everyauth = "subscription"Claude task failed the engine preflight withClaude Code is not logged in:agentEnvironmentVariableNamesgave the controller only each agent'sapi_key_env; subscription mode has none, so the token never reached the controller.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 plainresumewould 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: itsapi_key_env(plusMOONSHOT_API_KEYfor Kimi), orCLAUDE_CODE_OAUTH_TOKENfor a subscriptionClaudeAgent.start-run.tsuses 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 survivesresumeand is blanked for every other route.environment.tsxaddsCLAUDE_CODE_OAUTH_TOKENandCLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTORto 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 inopts.envalone was overridden by the blanked default, which the new adapter test caught.api-keymode never receives it.validatewarns withAGENT_ENV_ALLOWLIST_CREDENTIAL_BLANKEDwhenULTRAFUZZ_AGENT_ENV_ALLOWLISTnames a credential that no selected agent uses, since every agent would receive it blank. ForCLAUDE_CODE_OAUTH_TOKENit says to useauth = "subscription". This covers the issue's "make a stripped credential visible" point without touching the engine's preflight message.claude setup-tokenoption next toauth = "subscription"indocs/config.md;docs/security.mdandCHANGELOG.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.
runtime.test.ts)unstableruntime.test.ts: #1266, allstartRun, credential, environment, provider-home and resume testsNew tests:
CLAUDE_CODE_OAUTH_TOKENin a subscription Claude run's provider-credential names (a name, not a value).End-to-end with a fake
claude(not committed). A fakeclaudeonPATHansweredclaude auth statusas logged in only when the token was in its environment, and logged what each invocation received. Both runs used an emptyHOME, the token exported,ClaudeAgentonclaude-opus-4-8, and--audit-profile smoke, through the real engine, preflight and process launch:unstableclaude auth statusapi_key_valid=fail: Claude Code is not logged in(the #1266 error)--model claude-opus-4-8Optional reviewer check with a real token:
claude setup-token, then on a host without a~/.claudelogin, exportCLAUDE_CODE_OAUTH_TOKENand runultrafuzz run --audit-profile smokewith a subscriptionClaudeAgentprofile. #1266 already showed that Claude Code reportsloggedIn: truewith the token set; this PR is what gets it to the agent.🤖 Generated with Claude Code

Confidence Score: 5/5
The PR appears safe to merge; no new actionable issues were found.Summary
Subscription-mode
ClaudeAgentreceivesCLAUDE_CODE_OAUTH_TOKENon headless hosts. Other model children keep it blank.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]Reviews (2) · Last reviewed commit: "fix(runtime): warn when an allowlisted b..." · Reviewed by Greptile