# Claude Code and the Agent SDK > Point Claude Code or the Agent SDK at Perdurance with two variables, what is forwarded, what an error looks like, and where subscriptions stand. Source: https://docs.perdurance.dev/claude-code/ --- **Claude Code runs through Perdurance on two environment variables and no code change.** It treats whatever `ANTHROPIC_BASE_URL` names as the Anthropic API, so a namespace URL and a Perdurance key are the whole of the configuration. Every turn is then a stored request: the body you sent, the answer, every stream chunk, and the token counts. The Agent SDK is the same configuration, because it passes its environment to the Claude Code process it spawns. ## Two variables ```bash export ANTHROPIC_BASE_URL='https://api.perdurance.dev/acme/prod' export ANTHROPIC_AUTH_TOKEN='sar_ab12cd34_…' ``` The base URL is your tenancy and your namespace, and it stops there. Claude Code appends `/v1/messages` itself, so a base URL that already ends in `/v1` asks for `/v1/v1/messages` and nothing answers there. This is the one place the address differs from the one an SDK is given in [Getting started](/getting-started). `ANTHROPIC_AUTH_TOKEN` is your **Perdurance** key, not your Anthropic key. Claude Code prefixes it with `Bearer ` and sends it in `Authorization`, which is where Perdurance reads a key from. The Anthropic key lives on the backend, encrypted, and Perdurance substitutes it on the way out. Two things have to exist in the namespace before this works: a backend of kind `anthropic` holding your Anthropic key, and a routing rule matching the model names Claude Code asks for. `/v1/messages` refuses a rule that resolves to an OpenAI-dialect backend with `422`, because nothing here translates between dialects. [Routing](/routing) has the patterns. ```bash export ANTHROPIC_BASE_URL='https://api.perdurance.dev/acme/prod' export ANTHROPIC_AUTH_TOKEN='sar_ab12cd34_…' claude ``` Lasts for one terminal session. ```json { "env": { "ANTHROPIC_BASE_URL": "https://api.perdurance.dev/acme/prod", "ANTHROPIC_AUTH_TOKEN": "sar_ab12cd34_…" } } ``` In `~/.claude/settings.json` this persists everywhere Claude Code runs. Keep it out of a project's `.claude/settings.json`, which is committed. ```typescript import { query } from '@anthropic-ai/claude-agent-sdk' const result = query({ prompt: 'Summarise last quarter’s incidents.', options: { env: { ...process.env, ANTHROPIC_BASE_URL: 'https://api.perdurance.dev/acme/prod', ANTHROPIC_AUTH_TOKEN: process.env.PERDURANCE_KEY, }, }, }) ``` The TypeScript SDK's `env` **replaces** the spawned process's environment rather than adding to it, so spread `process.env` into it or the child loses `PATH`. The Python SDK's `ClaudeAgentOptions(env=…)` merges instead, so variables already set in the parent carry through on their own. ## What reaches Anthropic **The body.** Every field, every content block, every tool definition, the `system` array in the order it arrived, and every `cache_control` marker. Only the model id changes, and only when a routing rule rewrites it. Object keys are re-serialised in a canonical order, which is what makes an identical retry hash to the same request. Array order is never touched, so the attribution block Claude Code writes as the first entry of `system` is still the first entry when the call goes out. **The headers.** Every request header whose name starts with `anthropic-` is forwarded, as an open list rather than a fixed one, so a capability Anthropic ships as a new header works on the release that introduces it. `openai-beta`, `openai-organization` and `openai-project` are forwarded too. These are not: | Not forwarded | Why | |---|---| | `authorization`, `x-api-key` | They carry the key you send Perdurance. The backend's own credential goes out instead | | `anthropic-version` | Perdurance sets it, at `2023-06-01`. A forwarded copy would be a second header | | `x-perdurance-key`, `last-event-id` | They address this service, and mean nothing upstream | | Everything else | Including `x-claude-code-session-id` and the agent id headers, which Anthropic's gateway protocol says a gateway may consume rather than forward | A forwarded value may be up to 4,096 bytes, and a value carrying a control character is dropped rather than sent, because a newline in a header would become a second header at the provider. **The stream.** Chunks are stored and forwarded as they arrive. Nothing waits for the whole answer, which matters because Claude Code reads the stream as it comes and stalls against a gateway that buffers. **Token counting falls back on its own.** There is no `/v1/messages/count_tokens` route here. Anthropic's gateway protocol makes that endpoint the only optional one, and Claude Code counts context through the messages endpoint instead when it is absent. **Model discovery is off by default.** There is no `/v1/models` route here either. Claude Code only asks for one when a developer sets `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`, and when the request fails the model picker falls back to its built-in list. Anthropic's protocol reference calls the startup probe at `HEAD /api/hello` best-effort traffic a gateway may reject without breaking anything, and Perdurance has no route for it. ## What an error looks like **Anthropic's refusal is Anthropic's.** The two dialect routes answer it with Anthropic's own status, Anthropic's body byte for byte as `application/json`, and `retry-after` when Anthropic sent one. Nothing is wrapped. This is deliberate: Claude Code matches the provider's error wording to retry a rejected capability and to turn it off for the rest of the conversation, and an envelope around the body breaks that recovery even when the status survives it. Perdurance's own envelope, `{"error": {"code", "message"}}`, is left for Perdurance's own outcomes. | Status | `code` | When | |---|---|---| | `502` | `upstream_failed` | The provider could not be reached, or refused in a way that was not an HTTP answer | | `504` | `held_too_long` | The connection was held past the synchronous hold. **The execution is still running** | | `4xx` | various | Every refusal before the call reaches the provider: no key, no grant, no matching rule, a dialect mismatch, no credit | [Errors and status codes](/errors) has the full table. **A stream decides on its first item.** A streaming call waits up to the deployment's synchronous hold, five minutes by default, for its first chunk. A provider that refuses before that first chunk is answered with the provider's status and body, not with an event stream that reports a failure inside a `200`. Once the first chunk has gone out the status line is spent, so a failure after it arrives as `event: failed` instead. ## Retries Claude Code and the Agent SDK re-send the identical body when a call fails. Inside the idempotency window, ten minutes from submission, those bytes hash to the record already there and the retry **attaches to the execution already running** rather than starting and paying for a second one. [Idempotency and resume](/idempotency) covers what counts as identical. That is also the way back from a `504 held_too_long`. The connection was let go, the execution was not cancelled, and the answer is stored when it finishes. Re-send the same body to attach to it, or read it by id. What attaches is a retry of one turn. The next turn of a conversation carries a longer transcript, so it is a different body and a new request, which is the point of the section below. ## Subscriptions **By default Perdurance carries no subscription credential, and asks for a Perdurance key instead.** A bearer token starting `sk-ant-`, which both an Anthropic API key and a claude.ai session token carry, is refused with `401` and code `provider_credential`. The message names `ANTHROPIC_AUTH_TOKEN` and `X-Perdurance-Key`, because a bare `unauthorised` would send you to check a key you typed correctly. The reason this comes up at all is what Claude Code does with a base URL and nothing else. Anthropic's gateway documentation, read on 2026-09-02: > Setting only that variable, without a gateway credential, doesn't replace the subscription. > Requests still route through the gateway, but a saved claude.ai login remains the active > credential, so its usage limits and billing apply. Gateways that pass this traffic on to > Anthropic must forward the OAuth capability in `anthropic-beta`. So a developer signed in with a claude.ai login who sets only `ANTHROPIC_BASE_URL` sends their subscription's own bearer token to the gateway. Anthropic's legal page on using the Developer Platform, read the same day, sets the limit on who may carry it: > Anthropic does not permit third-party developers to offer Claude.ai login into their own > applications, or to route requests through Free, Pro, or Max plan credentials on behalf of > their users. Moreover, developers may not collect, store, or intermediate Claude.ai > credentials or session tokens … Anthropic does not permit a third party to route requests through those credentials or to intermediate session tokens. Perdurance's shared service carries none. Setting `ANTHROPIC_AUTH_TOKEN` to a Perdurance key is what this page opened with, and it is the configuration this product is built for. ### A dedicated deployment can be asked to carry one **The switch is off by default, and Perdurance's shared service does not pass it.** The two quotations above are Anthropic's own words on who may carry a subscription credential, and they are what to read before turning it on anywhere. A deployment you run for your own developers is the case Anthropic's gateway documentation describes, and it is yours to decide. Perdurance can be started with `--credential-passthrough` on a dedicated deployment at the customer's request. The customer's own agreement with Anthropic is what settles whose terms that traffic runs under. Perdurance states Anthropic's position and does not interpret it. With the switch on, a backend can be created with `credential_source: "caller"`. Such a backend holds no key of its own and forwards the one each call brought. ```bash export ANTHROPIC_BASE_URL='https://gateway.example.com/acme/prod' export ANTHROPIC_CUSTOM_HEADERS='X-Perdurance-Key: sar_ab12cd34_…' # ANTHROPIC_AUTH_TOKEN is deliberately unset, so the claude.ai login stays active ``` `ANTHROPIC_CUSTOM_HEADERS` takes `Name: Value` pairs, newline-separated for more than one. The Perdurance key moves into `X-Perdurance-Key`, which wins over the bearer when both are present, and that leaves `Authorization` free to carry what Claude Code put there. The OAuth capability in `anthropic-beta` is forwarded by the open-list rule above, which is what Anthropic's gateway documentation asks of a gateway passing this traffic on. Six things bound it: - The credential is sealed with the deployment's cipher inside the submission transaction, kept for the life of the attempt, and destroyed by the completion that writes a terminal status. A deferred retry keeps it, because the retry has to make the same call. - No route returns it, no listing selects it, and no log line names it. - The idempotency claim is taken over the body **and** the credential. Two callers who send one body under two credentials run two executions, because a shared one would run a request on somebody else's subscription. The same credential re-sent inside the ten minutes attaches as usual. - A submission that brought no `Authorization` and no `x-api-key` of its own is refused with `401 caller_credential_missing`, and no request row is written. - `POST /requests`, the fire-and-forget route, refuses a caller backend with `422 passthrough_disabled`. Nobody is holding the connection for the answer, and the row would hold a token until a worker got to it. - Without the switch, creating a caller backend and submitting to one are both `422 passthrough_disabled`. ## Storage A Claude Code session stores more than it looks like. Every turn sends the whole conversation, so every turn is a new request row holding the whole conversation. Ten turns of a long session are ten copies of a transcript that got longer each time, not ten short messages. That is what makes a session the fastest way to spend a namespace's storage allowance. Check what one is holding with `GET /storage`, and shorten the retention horizon on a namespace you use for this if the answer is more than you want to keep. [Usage and storage](/usage) has both. - [Getting started](https://docs.perdurance.dev/getting-started/): The namespace, the backend, the routing rule and the key this page assumes. - [Agent SDK retries](https://docs.perdurance.dev/agent-sdk-retries/): What a retry re-runs in an agent loop, and why it costs more than a retried API call. - [Errors and status codes](https://docs.perdurance.dev/errors/): Every status this API returns, and the ones that are the provider's own. - [Idempotency and resume](https://docs.perdurance.dev/idempotency/): The ten-minute window, and what counts as identical.