Based on the official Deploy Claude Desktop with an LLM gateway guide — shunt is the gateway you point it at. shunt implements the Anthropic Messages API (POST /v1/messages with streaming and tool use) and the optional GET /v1/models, which is exactly the gateway contract Claude Desktop’s third-party inference expects.
1. Point Claude Desktop at shunt
In Developer → Configure Third-Party Inference…, set Inference provider to Gateway and Gateway base URL to your running shunt (default bind 127.0.0.1:3001):
| Claude Desktop key | Value |
|---|---|
inferenceProvider |
gateway |
inferenceGatewayBaseUrl |
http://127.0.0.1:3001 (or your public shunt URL) |
shunt serves plain HTTP; for anything beyond a loopback deployment, terminate TLS in front or tunnel it, exactly as for sharing the gateway.
2. Choose an authentication approach
Claude Desktop offers several approaches. shunt maps cleanly onto the static key (and its credential-helper variant), and onto bootstrap sign-in, where shunt itself is the device-code authorization server. SSO with an external IdP’s JWT is a gateway-side capability shunt does not implement inbound.
| Claude Desktop approach | shunt side | Notes |
|---|---|---|
Static API key (inferenceGatewayApiKey) |
[server.auth] client token |
Recommended. |
Credential helper (inferenceCredentialHelper) |
an executable printing a [server.auth] client token |
For orgs that already mint gateway credentials. |
Bootstrap sign-in (bootstrapUrl + device code, inferenceCredentialKind: interactive) |
[server.gateway] login plus [server.gateway.desktop_bootstrap] |
One shunt sign-in per user, no CLI on the device — see Bootstrap sign-in. |
Interactive SSO (inferenceGatewayOidc + inferenceCredentialKind: interactive) |
not supported inbound | shunt does not validate external-IdP JWTs — see below. |
Static API key (recommended)
Enable [server.auth] on shunt and hand each user a client token:
[server.auth]
header = "x-shunt-token" # default
tokens_env = "SHUNT_CLIENT_TOKENS"Put that token in Claude Desktop’s inferenceGatewayApiKey. shunt accepts the client token in Authorization: Bearer or x-api-key, so either Gateway auth scheme works:
| Claude Desktop key | Value |
|---|---|
inferenceGatewayApiKey |
your shunt client token |
inferenceGatewayAuthScheme |
bearer (default) or x-api-key |
Without [server.auth], shunt requires no inbound credential (fine for a personal loopback gateway); Claude Desktop still wants the field populated, so enter any placeholder.
The token gates GET /v1/models and injected-credential (mapped/pooled) models; passthrough models stay open and carry the operator’s own provider credential.
Bootstrap sign-in (device code)
Claude Desktop can fetch its configuration from a bootstrap URL and sign the user in with an OAuth device-code grant against that same server (Anthropic’s bootstrap guide, mode “Bootstrap server as authorization server (device code)”). shunt serves this mode: each user signs in once on shunt’s /device page — with a password or your OIDC IdP — and Desktop reuses that bearer for inference. Nothing is installed on the device besides Claude Desktop, and there is no helper-script path to keep in sync across machines.
1. Enable gateway login. Configure [server.gateway] with a public_url, a signing secret, and static users or an OIDC IdP. This mode needs a real hostname with TLS in front of shunt: Anthropic’s guide describes the bootstrap endpoint as HTTPS, and the packaged Desktop app drops loopback URLs (127.0.0.1, localhost, [::1]) from response keys.
2. Point shunt at a template.
[server.gateway.desktop_bootstrap]
template = "/etc/shunt/claude-desktop-bootstrap.json" # file path onlyThe file must exist and be a JSON object, or config load (and shunt check) fails. shunt reads it on every request, so edits apply without a restart. Write it in the nested v2 format that Desktop’s own JSON export produces, or in the flat v1 format; shunt passes it through unchanged:
{
"$schemaVersion": 2,
"inference": {
"provider": "gateway",
"baseUrl": "https://shunt.example.corp",
"credential": { "kind": "interactive" }
},
"otlp": {
"resourceAttributes": { "user.email": "${user.email}" }
}
}The flat v1 equivalent is {"inferenceProvider":"gateway","inferenceGatewayBaseUrl":"https://shunt.example.corp","inferenceCredentialKind":"interactive","otlpResourceAttributes":{"user.email":"${user.email}"}}.
- The gateway base URL must be shunt’s
public_url. WithoutbootstrapOidc, Desktop drops a gateway base URL that does not share thebootstrapUrlorigin. Sharing the origin withinteractivecredentials is also what makes Desktop reuse the bootstrap bearer for inference (and, from 1.34493.0, renew it with a refresh grant), so users sign in once. - Placeholders. Inside string values,
${user.email},${user.sub}, and${user.name}become the signed-in user’s values. Unknown${...}tokens stay as-is; object keys are never substituted. - Return every key you want applied. Response keys are the effective configuration: a key the template omits is unset, not inherited from MDM.
3. Hand out the seed file. Each user imports it through Developer → Configure Third-Party Inference… → Import configuration:
{
"bootstrapUrl": "https://shunt.example.corp/user/bootstrap",
"trustBootstrapDelivery": true
}The bootstrapUrl origin must equal public_url’s origin. Desktop strips the trailing /user/bootstrap to find shunt’s OAuth discovery document, runs the device-code login, and then fetches GET /user/bootstrap with the issued bearer. trustBootstrapDelivery skips the “Apply settings from your organization?” dialog that Desktop otherwise shows when a bootstrap response delivers a base URL, helper, or marketplace value to a device configured from a local file. Desktop accepts that key only from MDM or the local file, never from the bootstrap response.
Who gets a configuration. Entitlement reuses [[server.gateway.policies]]: with no policies, every signed-in user is entitled; with policies, a user is entitled when a policy’s match.emails lists them or when a catch-all policy (no match) exists. Others receive 403. Without desktop_bootstrap the route answers 404. The response carries an ETag, so Desktop’s background re-check (every 10 minutes by default) usually costs a 304.
Desktop keeps the response in memory only. If shunt is unreachable at launch, Desktop starts in a degraded sign-in state — the same dependency inference already has. Signing out of Desktop is local-only: shunt does not advertise a revocation endpoint. See the endpoint reference for every status code.
3. Select models
shunt serves GET /v1/models, so Claude Desktop auto-discovers the picker at launch. Two things govern what appears.
Discovery filter. Claude Desktop’s auto-discovery shows only recognizably Claude ids — the tier-named ones (claude-sonnet-*, claude-opus-*, claude-haiku-*, claude-fable-*). shunt’s builtin catalog is the superset of what the Anthropic API and the reference Claude apps gateway serve — fourteen tier-named ids, so Claude Desktop shows every one. This is the fallback snapshot; when shunt can reach your Anthropic upstream it serves that caller’s own live list instead (see Model Discovery).
// GET /v1/models — builtin catalog (auto_include_builtin_models), all tier-named
// (each entry also carries "type": "model", "display_name", "created_at",
// "max_input_tokens", and "max_tokens")
{ "data": [
{ "id": "claude-sonnet-5-5" }, { "id": "claude-opus-5-5" },
{ "id": "claude-fable-5-1" }, { "id": "claude-opus-5" },
{ "id": "claude-sonnet-5" }, { "id": "claude-fable-5" },
{ "id": "claude-opus-4-8" }, { "id": "claude-opus-4-7" },
{ "id": "claude-sonnet-4-6" }, { "id": "claude-opus-4-6" },
{ "id": "claude-opus-4-5-20251101" }, { "id": "claude-haiku-4-5-20251001" },
{ "id": "claude-sonnet-4-5-20250929" }, { "id": "claude-opus-4-1-20250805" }
], "has_more": false,
"first_id": "claude-sonnet-5-5", "last_id": "claude-opus-4-1-20250805" }A curated claude-<slug>-via-<provider> alias (the pattern that works in Claude Code) is dropped by Claude Desktop — see Model Discovery → Claude Desktop recognizes only tier-named ids.
Exposing a non-Anthropic backend. Two options:
-
Map a tier-named id with a
[[routes]]upstream_model, so selecting it in Desktop resolves to your backend:[[routes]] model = "claude-sonnet-5" # a tier-named id Claude Desktop recognizes provider = "codex" upstream_model = "gpt-5.6-sol" # real backend slug -
Override discovery on the Desktop side with an explicit
inferenceModelslist of the exact ids shunt routes on. When every entry is a full id, Claude Desktop skips the/v1/modelscall.
4. Verify
Confirm shunt answers discovery and inference with the client token:
# Discovery — the token gates it when [server.auth] is set
curl -s "$SHUNT_URL/v1/models" -H "Authorization: Bearer $SHUNT_CLIENT_TOKEN" | jq '.data[].id'
# A tier-named id you mapped -> diverted to the backend
curl -s -X POST "$SHUNT_URL/v1/messages" \
-H "Authorization: Bearer $SHUNT_CLIENT_TOKEN" \
-H "anthropic-version: 2023-06-01" -H "content-type: application/json" \
-d '{"model":"claude-sonnet-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'Then open Claude Desktop; the model picker should list the tier-named entries. If it is empty, discovery was filtered out (non-tier ids) or /v1/models was unreachable — set inferenceModels explicitly as a fallback.