Skip to content

Connect Claude Code

Point Claude Code at shunt, choose the right Anthropic credential, and select mapped models.

Updated View as Markdown

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-tokenrecommended 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` block

The 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_TOKEN or CLAUDE_CODE_OAUTH_TOKEN is set, it echoes that value unchanged. Point it at a claude setup-token value and nothing is ever refreshed.
  • Auto-refresh mode — otherwise it reads ~/.claude/.credentials.json (override with CLAUDE_CREDENTIALS), returns the access token, and refreshes it only within 5 minutes of expiry, writing back atomically at 0600.

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 opus

It 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close