Claude Code
Claude Code is fullsend's default runtime. Every role is supported, and nothing needs configuring to use it — this page is the operational detail once you are on it.
fullsend run triage --model opus --effort highChoosing between runtimes is in Agent runtimes. Selection, precedence and the config keys live there too.
Models
Pass an alias or a model id. fullsend resolves opus, sonnet and haiku itself and passes the id to --model, so the default does not depend on the pinned Claude Code version:
| Alias | Resolves to | Built-in fallback chain |
|---|---|---|
opus | claude-opus-5-5 | claude-opus-4-6 |
sonnet | claude-sonnet-5-5 | claude-sonnet-4-6 |
haiku | claude-haiku-5-5 | claude-haiku-4-5 |
fable | the current model of that tier, as the pinned Claude Code version defines it | none |
The same alias applies when the harness names no model and the agent definition's model: frontmatter does. By default all inference goes to Anthropic models on Vertex AI, on the fleet's WIF credentials. Vertex enables models per project, so the default is not always a model your project can serve. When it is not, the run falls back along the alias's chain (see Fallback chains below), so a project that has not enabled the 5.5 models keeps running on the previous default with no config change. A pinned id that the project does not serve fails at the first model call. The one other supported route is the inference gateway route, selected by a gateway/<model> model.
Per-repo alias overrides. Point an alias at a different model for this repo with models.aliases in .fullsend/config.yaml (sonnet: claude-sonnet-5); the run then passes that id to --model and --fallback-model, and the alias has no built-in fallback chain. A repo that pinned the 5.5 ids here before they became the default can drop the pin to get the fallback. Syntax, rules and what the plan block shows are on the pi page — it is the same block. It covers the run's own model only.
Sub-agents. Sub-agent model: frontmatter is resolved by Claude Code itself, with its own alias table for the pinned version. fullsend's defaults and fallback chain do not apply there, and fullsend does not set ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL: a pinned 5.5 id there would fail every sub-agent on a project that does not serve it. A sub-agent that needs a specific generation names the id.
When your project does not serve the model
A run on opus, sonnet or haiku retries on the next model when Vertex says the project does not serve the one it asked for. Nothing to configure:
| Situation | What happens |
|---|---|
| Alias on its default, project serves 5.5 | Runs on 5.5 |
| Alias on its default, project does not serve 5.5 | Falls back along the built-in chain (table above) |
FULLSEND_FALLBACK_MODELS=a,b set | That list replaces the built-in chain |
Alias retargeted in models.aliases | No fallback, unless FULLSEND_FALLBACK_MODELS is set |
Pinned id (claude-opus-5-5) or provider/id | No fallback: the run fails at the first model call |
Any other error (auth, quota, PERMISSION_DENIED) | No fallback: the run fails |
| Inference gateway route | No runner fallback |
What you see:
Model: opus → claude-opus-5-5 (built-in default) # plan block
Fallback models: claude-opus-4-6 (built-in for opus, when not served) # plan block
! model claude-opus-5-5 is not available (not served in this project); falling back to claude-opus-4-6metrics.json then records the model that answered as model, the one it left in fallback_from, and why in fallback_reasons (not_served). A retry happens only when the model produced nothing, and every attempt shares the run's timeout.
An explicit FULLSEND_FALLBACK_MODELS list is also passed to Claude Code as --fallback-model, on every route. Claude Code may then switch models itself within the session (overload, server errors, or a Vertex 404). That prints the same warning with Claude Code's trigger as the reason, ! model <id> is not available (Claude Code: <trigger>); falling back to <next>, and is recorded the same way, with claude_code:<trigger> in fallback_reasons. How the runner detects "not served" is in Runtime implementation.
At a glance
| Roles | All, including review and retro — they need sub-agents |
| Credentials | Vertex: WIF external_account + a refreshed OIDC token. Gateway route: a run-scoped placeholder for the forge OIDC token or the gateway API key. What the launch clears is listed under Environment the launch clears |
| Unattended | --dangerously-skip-permissions; hooks wired from the harness, never from agent-writable files |
| Artifacts | output.jsonl, transcripts, metrics.json with runtime: claude, and claude-debug.log with --debug |
| Effort | --effort low..max |
Inference gateway route
The runner can send Claude Code's requests through a self-hosted, Anthropic-compatible inference gateway (ADR 0137) instead of Vertex. The gateway holds the upstream key, and the sandbox only ever holds a placeholder.
Selecting it. Give the run a gateway/<model> model (harness model:, --model, an agents: entry, a models.aliases target, or the agent's frontmatter) and configure an inference.gateway block (config reference). Claude Code is given <model>: the prefix only selects the route, so gateway/claude-haiku-5-5 calls claude-haiku-5-5 and gateway/vendor/org/model calls vendor/org/model. Claude Code needs no model list, so the block's models and models_file are not read.
- With no block, a
gateway/model is an error. - In the
oidcmode the block applies only on a run with a forge OIDC endpoint, so a local run with agateway/model is an error. Useauth: api-keyfor a local run. - Other Claude models in a run with a block keep the Vertex route. The Vertex credential check is skipped only for gateway runs. A harness
host_filesmount of${GOOGLE_APPLICATION_CREDENTIALS}that is notoptionalis still required, so mark it optional, or remove it, when you move a Vertex harness to the gateway.
What the runner owns. For gateway runs, after the agent-writable .env (and the harness env.sandbox and host-file env files it sources), the launch clears the variables listed below and exports:
ANTHROPIC_BASE_URL: the block'surlorigin, with its path prefix if it has one;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1, without which Claude Code also dialsapi.anthropic.com;- the credential, per mode (below).
.env cannot override any of these. A variable that .env makes readonly stops the launch rather than reaching Claude Code. The placeholder is read from the sandbox before .env runs.
The repository's own .claude/settings.json and .claude/settings.local.json can carry an env block, which Claude Code applies after launch over the launch environment. The runner therefore repeats the route in the --settings document it passes, which ranks above both. On gateway runs that document goes inline on the command line, together with the security hooks, never as a path to a file in the sandbox. It sets ANTHROPIC_BASE_URL, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and, in the oidc mode, the helper and its TTL. It also sets every other variable in the cleared list to "", which Claude Code treats as unset. In the api-key mode it sets apiKeyHelper to "", so a repository helper adds no x-api-key value. In the api-key mode it also pins ANTHROPIC_AUTH_TOKEN to the placeholder, or ANTHROPIC_API_KEY when the block sets messages_auth_header: x-api-key (below). The runner reads that placeholder from the sandbox before the agent starts. It is only a placeholder, so the file exposes nothing, and the key does not rotate, so it holds for the whole run. The launch refuses to start if the sandbox hands out a different placeholder by then.
Credential per auth mode. By default the credential is sent as Authorization: Bearer in both modes, the header ADR 0137 sets for the route and the one pi sends, so a gateway reads one header whatever the runtime or mode. An api-key block can set messages_auth_header: x-api-key instead (ADR 0142):
| Mode | How Claude Code gets it | Header sent |
|---|---|---|
oidc (default) | an apiKeyHelper (command -p cat of a runner-seeded token file) in the inline --settings document, with CLAUDE_CODE_API_KEY_HELPER_TTL_MS=10000 | Authorization: Bearer, plus a copy in x-api-key (below) |
api-key | ANTHROPIC_AUTH_TOKEN set to the placeholder for FULLSEND_INFERENCE_GATEWAY_API_KEY | Authorization: Bearer |
api-key with messages_auth_header: x-api-key | ANTHROPIC_API_KEY set to the same placeholder | x-api-key only |
Outside that last row, ANTHROPIC_API_KEY, which Claude Code would send as x-api-key, is cleared and never set, so a .env value cannot add a second credential header. In that row the roles swap: ANTHROPIC_AUTH_TOKEN is cleared and pinned to "", and apiKeyHelper is "", so the request carries the credential in x-api-key and nothing in Authorization. Use it for a gateway that reads the caller's key from x-api-key on /v1/messages, the header Anthropic documents for API keys on that API. The gateway must then replace the caller's x-api-key with its own upstream key, never forward it; see Header custody for an x-api-key gateway. messages_auth_header: x-api-key with auth: oidc is a config error.
In the oidc mode Claude Code also sends the helper's value as x-api-key: it puts an apiKeyHelper value in both headers. The gateway authenticates the Authorization header and must strip both headers before it calls upstream, as ADR 0137 requires.
In the oidc mode the token is the job's forge OIDC token, which lives about 300 s. Each refresh pins a new placeholder generation, so Claude Code cannot keep one in an environment variable. Instead, the runner re-seeds the token file after every refresh, as it does for pi. It starts each refresh with at least the refresh work plus a safety margin left on the old token, so the new placeholder is handed over at least that margin (15 s) before expiry, typically about two minutes. Claude Code (checked on 2.1.296) re-runs the helper when a request is answered 401 or 403. Otherwise it re-runs it in the background once CLAUDE_CODE_API_KEY_HELPER_TTL_MS has passed, and sends its cached value meanwhile, however old.
Long gaps between model requests in the oidc mode. A model request after a gap that spans a hand-off and outlasts the old token still carries the old placeholder. Such a gap is typically one tool call of more than about 2 minutes. Claude Code recovers from it:
- Every placeholder generation of Claude Code's gateway provider is created with the run's deadline as its OpenShell expiry, not the token's
exp. The deadline is the run's time budget, with the same bound as theapi-keymode's provider (at least 24 hours). It is set once, when the generation is created, and never updated, because on OpenShell an expiry update mints a new generation. The provider is still deleted when the run ends. - So OpenShell still resolves the stale placeholder, to the old JWT, which has already expired. The gateway refuses an expired token, as ADR 0137 requires of every gateway. Claude Code then re-runs its helper and retries with the re-seeded generation. This needs the gateway to answer
401or403, not a5xx. agentgateway'sjwtAuthand Praxis both answer401, after a default 60 s leeway pastexp. - The JWT's own
expdoes not change, and the gateway'sexpcheck becomes the layer that fails closed. A gateway with clock-skew leeway (60 s by default on agentgateway and Praxis) accepts a stale token for that long afterexp. Before, OpenShell stopped resolving it atexp. With the token'sexpas the expiry, OpenShell would resolve the stale placeholder to nothing and answer500(credential_unavailable). Claude Code does not treat a 500 as an auth failure, so it would retry the same value until the run failed. - A fixed grace period past
expwould only move that cliff to a tool call longer than the grace. - The refresh schedule does not change: each token is still re-seeded before its own
exp. - If a hand-off fails closed (the new generation never reaches the sandbox), the refresher stops. The provider's expiry is not moved back. Once the token the agent holds expires, the gateway refuses it, and so it refuses every retry, because the token file is not re-seeded again.
pi does not need this, because it reads the token file on every request, so its provider keeps each token's exp. Codex caches its auth.command value for 30 s only, and then re-runs the command before the next request, so a long tool call does not leave it with a stale value.
Egress. The runner imports a per-host profile for the gateway, rendered for Claude Code. It allows POST /v1/messages and POST /v1/messages/count_tokens, from the binaries **/claude and **/claude.exe (the sandbox image's native binary). OpenShell also admits a process whose ancestor matches (egress binary identity), so the tools Claude Code starts share this access. Its credential metadata names Authorization: Bearer, or a plain x-api-key header with messages_auth_header: x-api-key. That rendering has its own profile id, so the two never overwrite each other on a shared OpenShell gateway. OpenShell resolves the placeholder only on requests to that host and path.
Limits.
- Sub-agents inherit the base URL and the credential. A sub-agent's
model:alias (sonnet,haiku) is resolved by Claude Code to its own Anthropic id and sent to the gateway as that id. Useinherit, or an id your gateway serves. - Model discovery and the interactive model picker are not used.
- The sandbox log shows refused DNS lookups for
github.comandraw.githubusercontent.comfrom Claude Code, even withCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1. This is expected and harmless: the run does not need them. fullsend inference gateway statusreports this route and its header.
Do not point Claude Code at an endpoint by hand. Setting ANTHROPIC_BASE_URL through harness env.sandbox with a provider that supplies ANTHROPIC_API_KEY reaches Claude Code on non-gateway runs (see below), but it is discouraged:
- It needs a long-lived API key stored somewhere. The Vertex route and the gateway's
oidcmode store no reusable credential. - fullsend does not test it, so behaviour built around Anthropic models on Vertex may be wrong: cost reporting, model alias resolution, sub-agent model tiers and fallback chains.
Use the gateway route above. For Claude models, the default Vertex route is the other choice.
Environment the launch clears
The launch sources the sandbox .env and then clears variables before it starts Claude Code. .env carries the harness env.sandbox values, the host-file env files it sources, and anything the agent wrote there between iterations. The rest of the environment, including provider credentials, comes from the sandbox.
| Run | Cleared after .env | Sources it covers |
|---|---|---|
| Every run | LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE, NODE_OPTIONS, NODE_PATH, BUN_OPTIONS: code-loading variables, not routing ones | all |
| Gateway runs | ANTHROPIC_BASE_URL, ANTHROPIC_UNIX_SOCKET, ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_CUSTOM_HEADERS, ANTHROPIC_IDENTITY_TOKEN, ANTHROPIC_IDENTITY_TOKEN_FILE, CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR, CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR, CLAUDE_CODE_API_KEY_HELPER_TTL_MS, ANTHROPIC_PROFILE, ANTHROPIC_FEDERATION_RULE_ID, CLAUDE_CODE_GATEWAY_TOKEN_FILE_DESCRIPTOR, CLAUDE_CODE_SESSION_ACCESS_TOKEN, CLAUDE_CODE_HOST_AUTH_ENV_VAR, CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER, AGENT_PROXY_AUTH_TOKEN, and the provider selectors CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_FOUNDRY, CLAUDE_CODE_USE_MANTLE, CLAUDE_CODE_USE_ANTHROPIC_AWS, CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD, CLAUDE_CODE_USE_GATEWAY; then the runner exports its own values | all: env.sandbox, host-file env files, the agent's .env writes, and providers |
On non-gateway runs no ANTHROPIC_* variable is cleared. A value the harness sets is honoured. Model-choice variables (ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_*_MODEL) are never cleared or pinned. On gateway runs the cleared variables are also pinned in the inline --settings document, against a repository .claude/settings.json env block (see What the runner owns). On non-gateway runs that block is read by Claude Code itself, after launch. On every route, the egress profile and the endpoint-bound placeholder are what keep the credential on its host.
Behaviour differences worth knowing
These are the places Claude Code differs from pi — useful when comparing a run across runtimes.
- The agent definition replaces the system prompt.
--agentmakes the agent.mdbody the system prompt outright. pi appends it to its own default instead, so an agent that relies on Claude Code's exact framing can read differently there. - Native sub-agents via the
Agenttool. Their tokens and cost are included in themetrics.jsontotals and broken down per model inper_model_usage, read from the result'smodelUsage(see metrics.json fields). - A
CLAUDE.mdbridge is injected when the repo hasAGENTS.mdbut noCLAUDE.md, because Claude Code auto-loads only the former. pi readsAGENTS.mdnatively and needs no bridge. tools:is enforced unreliably (≥ 2.1.119); pi enforces its--toolsallowlist strictly. In both cases the sandbox, not the tool list, is the boundary (ADR 0027).- Failed tool calls cannot be rewritten. Claude Code fires
PostToolUseonly on success; a failed call goes toPostToolUseFailure, which accepts no output rewrite. Secrets or control characters in a failed command's output are detected and logged, and the agent is warned, but they reach the transcript unmasked. pi sanitizes those too. - The repo's own
.claude/settings.jsonstill auto-loads from the working directory. fullsend's hook wiring is passed explicitly with--settingsso it loads regardless, but repo-supplied hooks are a separate exposure to be aware of.
Troubleshooting
The model is not what you asked for. Check metrics.json: requested_model is what was handed to the runtime after overrides and override_source says where it came from — ending in remapped by <config path> models.aliases when a per-repo alias override applied — so a silent override is visible after the fact.
A tool call was blocked. The security hooks log to /sandbox/workspace/.security/findings.jsonl inside the sandbox. A blocked tool reports its reason in the transcript; an allowlist mismatch names the offending tool and the expected vocabulary.
The run refused to start with exit 97. Before every launch the runtime checks that /sandbox/claude-config/hooks.json and the hook scripts under /sandbox/claude-config/hooks/ are exactly what Bootstrap installed. A file that was changed, deleted or added, usually by an earlier iteration, fails the run instead of loading altered hooks. On the inference gateway route the settings document is passed inline, so only the hook scripts are checked. The launch also starts the claude binary it found before sourcing .env, so a claude function or a PATH entry from .env cannot stand in for it.
A harness plugin did not load. When Claude Code reports a plugin in the plugin_errors of its startup system/init event, the run output prints one Plugin <name> failed to load … warning per entry, with the error category and message. It also shows the plugin directory when Claude Code names one (2.1.283 and later, for --plugin-dir entries). The run continues: the warning is the signal to check the plugin's path and plugin.json.
Output looks truncated or condensed. The PostToolUse chain condenses verification-command output only on positive evidence of success, and attaches a note saying it did. Anything carrying a failure marker passes through untouched.
See also
- Agent runtimes — choosing and selecting a runtime
- Pi — the second runtime, for Grok and Gemini
- Running agents locally — local runs
