Skip to content

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.

bash
fullsend run triage --model opus --effort high

Choosing 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:

AliasResolves toBuilt-in fallback chain
opusclaude-opus-5-5claude-opus-4-6
sonnetclaude-sonnet-5-5claude-sonnet-4-6
haikuclaude-haiku-5-5claude-haiku-4-5
fablethe current model of that tier, as the pinned Claude Code version defines itnone

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:

SituationWhat happens
Alias on its default, project serves 5.5Runs on 5.5
Alias on its default, project does not serve 5.5Falls back along the built-in chain (table above)
FULLSEND_FALLBACK_MODELS=a,b setThat list replaces the built-in chain
Alias retargeted in models.aliasesNo fallback, unless FULLSEND_FALLBACK_MODELS is set
Pinned id (claude-opus-5-5) or provider/idNo fallback: the run fails at the first model call
Any other error (auth, quota, PERMISSION_DENIED)No fallback: the run fails
Inference gateway routeNo runner fallback

What you see:

text
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-6

metrics.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 ​

RolesAll, including review and retro — they need sub-agents
CredentialsVertex: 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
Artifactsoutput.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 oidc mode the block applies only on a run with a forge OIDC endpoint, so a local run with a gateway/ model is an error. Use auth: api-key for 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_files mount of ${GOOGLE_APPLICATION_CREDENTIALS} that is not optional is 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's url origin, with its path prefix if it has one;
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1, without which Claude Code also dials api.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):

ModeHow Claude Code gets itHeader 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=10000Authorization: Bearer, plus a copy in x-api-key (below)
api-keyANTHROPIC_AUTH_TOKEN set to the placeholder for FULLSEND_INFERENCE_GATEWAY_API_KEYAuthorization: Bearer
api-key with messages_auth_header: x-api-keyANTHROPIC_API_KEY set to the same placeholderx-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 the api-key mode'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 401 or 403, not a 5xx. agentgateway's jwtAuth and Praxis both answer 401, after a default 60 s leeway past exp.
  • The JWT's own exp does not change, and the gateway's exp check 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 after exp. Before, OpenShell stopped resolving it at exp. With the token's exp as the expiry, OpenShell would resolve the stale placeholder to nothing and answer 500 (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 exp would 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. Use inherit, 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.com and raw.githubusercontent.com from Claude Code, even with CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1. This is expected and harmless: the run does not need them.
  • fullsend inference gateway status reports 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 oidc mode 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.

RunCleared after .envSources it covers
Every runLD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE, NODE_OPTIONS, NODE_PATH, BUN_OPTIONS: code-loading variables, not routing onesall
Gateway runsANTHROPIC_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 valuesall: 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. --agent makes the agent .md body 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 Agent tool. Their tokens and cost are included in the metrics.json totals and broken down per model in per_model_usage, read from the result's modelUsage (see metrics.json fields).
  • A CLAUDE.md bridge is injected when the repo has AGENTS.md but no CLAUDE.md, because Claude Code auto-loads only the former. pi reads AGENTS.md natively and needs no bridge.
  • tools: is enforced unreliably (≥ 2.1.119); pi enforces its --tools allowlist strictly. In both cases the sandbox, not the tool list, is the boundary (ADR 0027).
  • Failed tool calls cannot be rewritten. Claude Code fires PostToolUse only on success; a failed call goes to PostToolUseFailure, 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.json still auto-loads from the working directory. fullsend's hook wiring is passed explicitly with --settings so 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 ​