Based on the official Connect Claude Code to an LLM gateway guide — shunt is the gateway you connect to.
1. Point Claude Code at shunt
Set the base URL to your running gateway (default bind 127.0.0.1:3001), in your shell or persisted in a settings file env block:
export ANTHROPIC_BASE_URL=http://127.0.0.1:3001// ~/.claude/settings.json
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:3001"
}
}Keep your existing Anthropic credential — shunt forwards it unchanged to api.anthropic.com for every model you didn’t map, so unmapped models keep working exactly as before. Provider credentials for mapped models are injected by shunt itself; Claude Code never sends them.
2. Choose the Anthropic credential
The credential Claude Code sends to shunt plays two roles: it authenticates Claude passthrough models, and it gates model discovery — Claude Code only issues the GET /v1/models request when ANTHROPIC_AUTH_TOKEN, an API key, or an apiKeyHelper is set. Mapped models (gpt-* etc.) are unaffected either way.
| Credential | Token refresh | Discovery | Claude passthrough | Billing |
|---|---|---|---|---|
| claude.ai OAuth login only | automatic | ❌ never fires | ✅ | subscription |
ANTHROPIC_AUTH_TOKEN from claude setup-token — recommended |
none needed (one-year token) | ✅ | ✅ | subscription |
apiKeyHelper = shunt token |
the helper refreshes it | ✅ | ✅ | subscription |
ANTHROPIC_AUTH_TOKEN=<real API key> |
none needed | ✅ | ✅ | API (not subscription) |
A dummy value like sk-dummy satisfies the discovery gate but breaks passthrough — it is forwarded to Anthropic and returns 401.
Prefer claude setup-token. It mints a one-year OAuth token (authentication docs), so nothing needs refreshing, and one value covers both roles:
claude setup-token # browser sign-in → prints sk-ant-oat…
export ANTHROPIC_AUTH_TOKEN=sk-ant-oat… # or persist it in a settings `env` blockThe shunt token credential helper
shunt token prints a Claude subscription OAuth token to stdout, so it wires straight into Claude Code’s apiKeyHelper:
// ~/.claude/settings.json
{
"apiKeyHelper": "/path/to/shunt token"
}- Static mode — if
SHUNT_GATEWAY_TOKENorCLAUDE_CODE_OAUTH_TOKENis set, it echoes that value unchanged. Point it at aclaude setup-tokenvalue and nothing is ever refreshed. - Auto-refresh mode — otherwise it reads
~/.claude/.credentials.json(override withCLAUDE_CREDENTIALS), returns the access token, and refreshes it only within 5 minutes of expiry, writing back atomically at0600.
The static + setup-token route stays the simplest and safest default.
The shunt gateway token helper
If shunt runs [server.gateway], each user can carry their own gateway session instead of a shared token. Sign in once, then use the second helper:
shunt gateway login https://gateway.example.com// ~/.claude/settings.json
{
"apiKeyHelper": "/path/to/shunt gateway token"
}This is a different credential from shunt token above: that one prints an upstream Claude subscription token, this one prints a token the shunt deployment issued to you. The helper refreshes within 5 minutes of expiry, under a file lock so concurrent Claude Code sessions cannot replay a single-use refresh token. shunt gateway logout discards the session.
Skip the settings file: shunt gateway claude
To avoid editing ~/.claude/settings.json at all, let shunt do the wiring for one process:
shunt gateway claude
shunt gateway claude -p 'summarize this repo' --model opusIt launches Claude Code with an inline --settings document carrying both the gateway base URL and the apiKeyHelper, scoped to that invocation — your settings file is untouched, and the document wins over an ANTHROPIC_BASE_URL already exported in the shell (measured against Claude Code 2.1.234). Arguments after claude are forwarded verbatim; a leading --help or --config is taken by shunt instead (--config is rejected rather than silently swallowed), so pass claude’s own --help or --config after --. See shunt gateway claude.
It also strips 34 credential and provider-mode variables from the launched process, plus whatever variable CLAUDE_CODE_HOST_AUTH_ENV_VAR names. Without that, an exported ANTHROPIC_AUTH_TOKEN would beat the apiKeyHelper outright, and CLAUDE_CODE_USE_GATEWAY would flip the client into gateway provider mode, where the helper is never consulted at all — either one silently defeats the wiring. ANTHROPIC_BASE_URL is left in place, since the settings document injects it. This closes the ambient-environment channel and nothing more: a settings-file env block, an apiKeyHelper already in your own settings, an existing saved login, or the file-descriptor readers’ well-known-path fallback can all still supply a credential to the launched client.
3. Provide the mapped provider’s credential
These go to shunt’s environment, not Claude Code’s:
export OPENAI_API_KEY=sk-... # openai provider
codex login # codex/ChatGPT provider (auto-refreshed thereafter)4. Select a mapped model
Claude Code’s model discovery only honors ids beginning with claude/anthropic, so for OpenAI/Codex ids (gpt-*) use ANTHROPIC_CUSTOM_MODEL_OPTION — it adds a picker entry whose id skips validation:
export ANTHROPIC_CUSTOM_MODEL_OPTION="gpt-5.6-sol"Then pick it from /model in Claude Code. That id is what shunt routes on, so it must resolve through a matching [models.upstream_model] entry, [[routes]], or [[route_prefixes]] rule in your config.
The two picker-exposure methods split cleanly on the claude-/anthropic- prefix — they don’t overlap. Discovery honors only claude-/anthropic- ids; ANTHROPIC_CUSTOM_MODEL_OPTION and the CLAUDE_CODE_MAX_CONTEXT_TOKENS window override apply only to ids that do not start with that prefix:
| What | claude-/anthropic- id (discovery alias) |
non-claude- id (e.g. gpt-5.6-sol) |
|---|---|---|
/v1/models discovery → /model picker |
✅ auto-listed (“From gateway”), many models | ❌ dropped by Claude Code |
ANTHROPIC_CUSTOM_MODEL_OPTION |
❌ not honored | ✅ adds to picker (one id only) |
CLAUDE_CODE_MAX_CONTEXT_TOKENS window |
❌ ignored → 200k default | ✅ applies → set the real window |
So a claude-…-via-codex discovery alias is convenient (auto-listed, one-tap) but its context window is stuck at the 200k default — the override can’t reach a claude--prefixed id (Effort & Context). Pick the discovery alias for picker convenience across several models (accept the 200k denominator), or a non-claude- id via ANTHROPIC_CUSTOM_MODEL_OPTION for an accurate window, one model at a time.
Per-agent diversion
Per-context selection works via Claude Code’s own knobs — divert one agent to a mapped model while the main session stays on Claude:
# .claude/agents/researcher.md
---
name: researcher
model: gpt-5.6-sol # this agent's inference is diverted; the main session stays on Claude
---A named subagent’s model: frontmatter is the only way to put a subagent on a gpt-* id: that field takes any string, whereas the Agent/Task tool’s model parameter is restricted to the built-in aliases (opus/sonnet/haiku/fable) and can’t take a gateway id. Spawn the agent by its type without a model override — the tool parameter outranks frontmatter (CLAUDE_CODE_SUBAGENT_MODEL > tool model > frontmatter > inherit), so passing one would shadow the mapped model. CLAUDE_CODE_SUBAGENT_MODEL forces every subagent onto one model. The window follows the model id automatically, so one global CLAUDE_CODE_MAX_CONTEXT_TOKENS sizes the mapped subagent while the Claude main keeps its own.
5. Verify
# Unmapped model -> forwarded to Anthropic (uses your Anthropic credential)
curl -s -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-4-6","max_tokens":1,"messages":[{"role":"user","content":"."}]}'
# Mapped model -> diverted to the provider (uses shunt's provider credential)
curl -s -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"gpt-5.6-sol","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'Then start claude, run /status, and check the Anthropic base URL line shows your gateway. See also Effort & Context for reasoning-effort and context-window tuning, and Client Environment Variables for the environment variables that change behind a gateway.
Verify a named subagent end to end
The raw requests above verify shunt’s routes, but not Claude Code’s client-side subagent
selection. For cross-model review, verify the model Claude Code actually resolved instead of
trusting the agent name or asking the model to identify itself. The resolvedModel result field
requires Claude Code 2.1.174 or newer; no output from this filter on an older client is not evidence
that routing fell back:
claude --output-format stream-json --verbose -p \
"Use the researcher agent once without a model override, then stop." \
| jq -Rr '
fromjson?
| .tool_use_result?
| objects
| select(.resolvedModel?)
| [(.agentType // null), .resolvedModel]
| @tsv
'For the example agent above, the result should be researcher and gpt-5.6-sol. Then confirm
that shunt maps the resolved ID to the intended provider and upstream model when it uses an exact
[[routes]] entry:
curl -s "${ANTHROPIC_BASE_URL%/}/routes" \
| jq '.data[] | select(.model == "gpt-5.6-sol")'GET /routes exposes exact [[routes]] entries only; it does not expand [[route_prefixes]].
For a prefix-routed ID, inspect the active [[route_prefixes]] entry instead, then send a minimal
request through that ID to exercise the effective route.
Interpret the evidence in order:
| Observation | Diagnosis |
|---|---|
resolvedModel differs from the agent’s frontmatter |
Claude Code selected a different model ID before the request reached shunt. Check CLAUDE_CODE_SUBAGENT_MODEL, a tool-level model override, missing/stale agent frontmatter, and agent-definition precedence; then inspect the matching route because shunt may intentionally remap that ID. |
resolvedModel is correct, but the matching exact route or prefix selects the wrong provider or upstream model |
Fix the shunt [[routes]] or [[route_prefixes]] configuration. |
resolvedModel and the matching gateway configuration are correct, but inference fails |
Check provider authentication, entitlement, quota, and upstream compatibility. |
Model self-identification is not routing evidence: a prompt can produce an incorrect model name,
while resolvedModel records Claude Code’s selection and /routes records shunt’s mapping.