One read of Claude Developer Platformapi-20261112T170714Z
12 pages moved out of 916 read.
What this read moved
1-12 of 12managed-agents/self-hosted New page · 90 lines, new page
## When to choose self-hosted Managed Agents ## What runs where ## Your security responsibilities ## The three credentials ## What leaves your network ## Where to start
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Self-hosted Claude Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/self-hosted
description: Host the Claude Managed Agents service and its sandboxes on your own infrastructure. Learn who hosts each part, when to choose this option, and where to start.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
Self-hosted Claude Managed Agents is Managed Agents on your own infrastructure, which can be a cloud account or an on-premise data center. You host the service and the sandboxes. The model runs at Anthropic or at a provider that you choose.
Managed Agents has two parts that you can host:
* **Sandboxes:** The containers, VMs, or other isolated workspaces where an agent’s tools run commands and work on files. See [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes).
* **The service:** The API, the agent loop, and the storage for agents, sessions, and session history.
Self-hosted sandboxes move the first part to you. Self-hosted Managed Agents extends that: you host the service too. Each option’s name says what you host:
| Option | Sandboxes | Service | Model |
| ------------------------------------------------------------------------------------------------- | --------- | --------- | ---------------------------------------- |
| Cloud sandboxes (default) | Anthropic | Anthropic | Anthropic |
| [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) | You | Anthropic | Anthropic |
| Self-hosted Managed Agents | You | You | Anthropic, or a provider that you choose |
On these pages, the hosted service is the service as Anthropic runs it, and your server is the service as you run it, with its own database. Both have the same API, so the SDK and the `ant` CLI work with your server, and the rest of the Managed Agents documentation applies. [Differences from the hosted service](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences) lists the exceptions.
<Note title="New to Claude Managed Agents?">
Claude Managed Agents runs Claude as an autonomous agent: you define an agent, start a session, and exchange events with the agent while it works. [Core concepts](https://platform.claude.com/docs/en/managed-agents/overview#core-concepts) explains agents, environments, sessions, and events. You don’t need to use the hosted service first.
</Note>
## When to choose self-hosted Managed Agents
Choose self-hosted Managed Agents when you need one of the following:
* **Storage on your infrastructure:** You must store agents, sessions, and session history yourself.
* **Your choice of inference provider:** Model calls must go to Amazon Bedrock, Claude Models on Google Cloud (Google managed), Microsoft Foundry, or your own model gateway. See [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider).
If you only need tools to run on your infrastructure, use self-hosted sandboxes, where Anthropic runs the service. See [Choosing an agent environment](https://platform.claude.com/docs/en/managed-agents/choosing-an-environment).
With self-hosted Managed Agents, you run and secure a server, its database, and your sandboxes, and you install each release yourself. Some features exist only on the hosted service, so check [Differences from the hosted service](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences) before you decide.
## What runs where
One or more servers, their database, and your sandboxes make one deployment.
```mermaid
flowchart LR
accTitle: Components of self-hosted Managed Agents
accDescr: Your applications, the server, its database, and your workers run inside your network. Applications call the server. Workers poll the server for work. The server stores its data in the database. The server sends each model call to the inference provider you configure, which is the Claude API by default.
subgraph network [Your network]
apps[Your applications] -->|Managed Agents API| server[Server]
workers[Your workers] -->|poll for work| server
server --> db[(Database)]
end
server -->|model calls| inference[Inference provider]
```
* **Server (`cma-server`):** Serves the API, runs the agent loop, and stores agents, sessions, and each session’s history. It calls your inference provider whenever the agent needs the model. The server never runs an agent’s shell or file tools.
* **Database:** PostgreSQL 16 or later. Without a database, the server keeps its data in memory, and a restart erases it.
* **Workers:** Programs that take work from the server and run the agent’s shell and file tools in your sandboxes. A worker is an `ant beta:worker` command or an SDK worker.
* **Your applications:** Your own code and scripts, which reach the server with the SDK or the `ant` CLI.
* **Console on your server:** A webpage at the server’s address, where people sign in through your identity provider to create agents and run sessions. See [Use the console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console).
* **Admin CLI (`cma-server-admin`):** Creates environments and their environment keys. It runs on Linux (x86-64 and Arm) and macOS on Apple silicon.
## Your security responsibilities
<Warning>
The agent’s `bash` tool is a real shell with the worker process’s permissions and network access. The worker doesn’t isolate that shell. Isolation comes from what the worker runs in. The Docker Compose stack and the `cma-sandboxes` Helm chart start each session’s worker in a container or pod of its own. If you start a worker yourself, run it in a disposable container or VM that holds nothing you need to protect.
</Warning>
The self-hosted sandboxes [Security model](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security) applies here, with two differences. What that page assigns to Anthropic is your responsibility, including securing the server, hosting memory stores, and detecting unusual usage. That page also suggests a separate workspace and environment for each trust boundary. Here, each trust level needs its own deployment, as [Multitenancy](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#multitenancy) explains.
## The three credentials
| Credential | Who holds it | What it authenticates |
| ------------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Server API key** | Your applications, the `ant` CLI, and the admin CLI | Requests to your server. Setup generates this key. It plays the role that a Claude API key plays with the hosted service. |
| **Inference credential** | The server | The server’s model calls to your inference provider. For example, it can be a Claude API key, your cloud provider’s credential, or the token that your model gateway accepts. Applications and workers don’t need it. |
| **Environment key** | Your workers that poll the server | One environment’s work queue and that environment’s sessions. |
## What leaves your network
For each model call, the server sends the conversation so far, including tool inputs and tool results, to your inference provider. By default, that is the Claude API. A call to the Claude API also carries the IDs of its session and of that session’s agent and environment. [Data and access in self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access) lists every connection that the server makes. It also says where the logs go and who can reach the server.
## Where to start
To request access to the download, contact your Anthropic account team. Then follow [Get started with self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart) to start a server and run a first session.
Each release’s archive includes an operator guide for that version, `operator-guide.md`. Have Claude read that file, so that Claude can help you configure and run your server and write code that connects to it.
managed-agents/self-hosted-data-and-access New page · 141 lines, new page
## What leaves your network ### Every connection that the server makes ### What workers can reach ### What the server sends to its inference provider ### Fields that name the server software ## Where the log, traces, and metrics go ## How long data is kept ## Who can reach the server ### Credentials that the server accepts ### Where the server listens ## Multitenancy ### Secret managers, single sign-on, and model gateways ### What roles don’t limit
A whole new page. There's nothing to diff it against, so here is what it says.
--- title: Data and access in self-hosted Claude Managed Agents url: https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access description: What self-hosted Claude Managed Agents sends outside your network, where its log, traces, and metrics go, how long data is kept, who can reach the server, and how to keep tenants apart. --- <Note> To request access to self-hosted Claude Managed Agents, contact your Anthropic account team. </Note> With [self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted), you run the server, its database, and the sandboxes where workers run the agent’s shell and file tools. This page answers the questions of a security review. The server’s inference provider is the service that answers its model calls. The server connects to Anthropic only when that provider is the Claude API. Anthropic has no remote access to the server or its log. ## What leaves your network ### Every connection that the server makes The server connects only to these destinations: * Your inference provider * The server’s database * The services that issue the server’s credential for model calls, if it gets that credential from a service * A cloud provider’s service for listing models, if your inference provider lists its models on a separate service * The MCP servers that you allow * The Git hosts that you allow, for the repositories that sessions use * An OpenTelemetry collector, if you configure one * The identity providers that you list for single sign-on, if you configure any * A secret manager or a key management service, if you configure one * The services that issue the server’s credential for that secret manager or key management service * Any other service that you configure the server to call The default inference provider is the Claude API. If you configure another inference provider, the model calls go there instead. The server connects to Anthropic only when the Claude API is its inference provider. Where the inference provider offers the web search and web fetch tools, it runs them. The server makes no connection of its own for them. The tools are off until you turn them on in the server’s settings. To check the server’s traffic, compare your egress proxy or firewall log with this list. The server’s requests to the Claude API carry a `User-Agent` header. See [Fields that name the server software](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#fields-that-name-the-server-software). ### What workers can reach The list of destinations covers only the server. The [egress gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials), if you run it, connects to the hosts that you list for it, and sends them the credential values that you store for them. Workers are separate. A worker has whatever network access you allow the container, VM, or other sandbox that it runs in. Every command that an agent runs through the worker has the same network access. ### What the server sends to its inference provider Each model call carries the conversation so far, including prompts, tool inputs, and tool results. When the Claude API is the inference provider, each model call also carries up to three headers. They hold IDs that your server generated: | Header | What it holds | | ----------------------------------------- | ------------------------------------------ | | `anthropic-managed-agents-session-id` | The ID of the session that the call serves | | `anthropic-managed-agents-agent-id` | The ID of that session’s agent | | `anthropic-managed-agents-environment-id` | The ID of that session’s environment | An ID holds no name and no content, but it does encode the time when your server created it. With these headers, Anthropic can attribute usage to a session, an agent, or an environment. No setting turns them off. If `ANTHROPIC_BASE_URL` names a proxy or a gateway, the headers go there. Under any other inference provider, the server never sends them. When the Claude API is the inference provider, the server also checks with the Claude API that its credential for model calls is valid. It checks at startup and then every 60 seconds, unless you change `health.check_interval`. These checks run no inference, and carry none of the three headers. ### Fields that name the server software Every request to the Claude API has a `User-Agent` header that names the server software and its version. The header itself doesn’t identify your deployment. As on every Claude API request, the API key identifies your organization. Anthropic can use that header to count usage of self-hosted Managed Agents. Model calls can also carry the label `cma-server`, in the request body as `metadata.user_id`. The label is the same on every server, so the label itself doesn’t identify your deployment, your users, or your sessions. Where a call reaches Anthropic, Anthropic can use the label to count usage of self-hosted Managed Agents, even if a proxy removed the `User-Agent` header. Whether a call carries the label depends on where it goes: * **The server sends the label on these calls:** calls to the Claude API, to Microsoft Foundry, and to a model gateway that accepts the same requests as the Claude API. * **The server never sends `metadata.user_id` on these calls:** calls to Amazon Bedrock, to Claude Models on Google Cloud (Google managed), and to a model gateway that takes another request format. If a model gateway or proxy passes a call on to another provider, `metadata.user_id` can go with it. On the calls that carry `metadata.user_id`, a proxy or gateway of your own must accept it in the request body. If it rejects the field, every model call fails. ## Where the log, traces, and metrics go The server writes its log to standard error. When you run the server under Docker, Kubernetes, or a service manager, that system collects the log from standard error. The server records no traces or metrics until you turn them on with the standard OpenTelemetry environment variables. It sends them only where those variables point. A Prometheus endpoint can also expose metrics. See [Where the server listens](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#where-the-server-listens). ## How long data is kept Your log pipeline, your collector, and your tracing backend determine how long the log, traces, and metrics are kept. The server keeps no log files and stores no traces or metrics. The server stores everything else, such as agents, sessions, and session history, in your database. If you set up no database, which is the default, the server holds that data in memory, and a restart erases it. If you configure a secret manager, the server keeps the secret of each new [vault credential](https://platform.claude.com/docs/en/managed-agents/vaults) there instead of in the database. Secrets that it stored earlier stay in the database. Sessions and their history stay in your database until you delete them. The server doesn’t delete a session because of its age, archived or not. Deleting a session through the API removes the session and its history from your database, though the server keeps the session’s ID. The server can refuse the delete while a session is running. The delete doesn’t reach copies elsewhere, such as database backups, exported logs and traces, or what the agent wrote to a [memory store](https://platform.claude.com/docs/en/managed-agents/memory). On Kubernetes, the delete also leaves the session’s `/workspace` volume in place, as [Free a session’s volume](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#free-a-sessions-volume) describes. What the server sends to the model, and what the model returns, fall under your organization’s agreement with the provider that serves the model. The agreement’s data retention terms apply. When the Claude API serves the model, the data retention agreement and settings that your organization has with Anthropic apply. If your organization has a [zero data retention (ZDR)](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#zero-data-retention-zdr-scope) arrangement, it covers these model calls in the same way as other Messages API calls. The API and data retention page lists Claude Managed Agents as [not covered by ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#what-zdr-does-not-cover). That is because when Anthropic runs the service, it stores session transcripts until you delete them. Who stores the transcripts depends on who runs the service: * **Anthropic runs it:** Anthropic stores them. With [self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes), you host only the sandboxes, and Anthropic runs the rest. * **You run it, with self-hosted Managed Agents:** You store them yourself. ## Who can reach the server Anthropic has no remote access to the server or its log. Your applications and workers connect to the server. The server never connects to a worker. ### Credentials that the server accepts Anthropic issues none of the credentials that the server accepts, and it holds none of them. Every credential that the server accepts is one of these: * One that you set or created * One that your identity provider issued * One that the server generated itself, such as the token it hands a worker with each piece of work ### Where the server listens The server accepts API requests on one address and port. The server opens two more listeners, each only if you turn it on. | Listener | On by default | Default address | What it asks for | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | API requests | Yes | `127.0.0.1:8400`, which only the same machine can reach. To change the address and port, set `server.addr`. | See [Credentials that the server accepts](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#credentials-that-the-server-accepts). | | The [console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console), a webpage | Yes. To stop serving it, see [Turn off the console](https://platform.claude.com/docs/en/managed-agents/self-hosted/console#turn-off-the-console). | The same address and port as API requests | Nothing to load the page, for anyone who can reach it. To do anything in the console, a person signs in through your identity provider. | | The Prometheus metrics endpoint | No | `localhost:9464`, which only the same machine can reach | No credential | | The server’s listener for the [egress gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials) | No | Port 8410 on every interface | The gateway’s key | More detail on the last two listeners: * **The Prometheus metrics endpoint:** It asks for no credential, so who can reach it depends on where it listens. If you set `OTEL_METRICS_EXPORTER` to `prometheus` and set no address, it listens on the default address. If you turn it on through the server Helm chart, it listens on port 9464 of every server pod. Anything with a route to a pod’s address can then reach it, unless a NetworkPolicy blocks that traffic. * **The listener for the egress gateway:** The egress gateway is an optional proxy between your sandboxes and the internet. The server listens for the gateway on a separate port. This listener speaks plain HTTP, and its answers carry stored credential values. The Compose stack and the server Helm chart let only the gateway reach it. If you run the server any other way, do the same. The Docker Compose stack publishes port 8400 on the Docker host’s loopback address, `127.0.0.1`. Run Docker Engine 28.3.3 or later. Under some earlier releases, machines on the host’s local network can reach a port that is published on loopback. To publish the port on another address, set `CMA_PUBLISH_ADDRESS` in `.env`. ## Multitenancy Each deployment serves one tenant at one trust level. Environments in one deployment don’t keep trust levels apart. The server API key reaches every environment. Every caller with a role can run sessions in every environment, with whatever network and files that environment’s workers can reach. | To keep apart | Do this | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Tenants | Give each tenant its own deployment, with its own database, server API key, and workers. | | Workloads at different trust levels | Give each trust level its own deployment, set up as for a tenant. Examples are trusted and untrusted code, or workers that reach systems that only some callers may use. | | Callers in a deployment | Turn on [single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#single-sign-on), map groups to the `developer` or `user` role, and keep the server API key for administrators. [What each role can do](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#what-each-role-can-do) shows what those callers see. | ### Secret managers, single sign-on, and model gateways If your deployments use one of these services, also keep them apart there: * **Secret manager:** Keep the deployments’ secrets apart, as in the [secret manager limits](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#limits). * **Single sign-on:** Give each deployment `audiences` entries that no other deployment lists, so that it refuses the other deployments’ tokens. In each trust level’s deployment, give a role only to the callers allowed at that level, and set no `default_role`. * **[Model gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#model-gateway):** To tell the deployments apart, give each one its own value for a header such as `x-tenant-id` under `inference.headers`. ### What roles don’t limit Roles limit only the requests that a caller makes with their own token. Code that an agent runs can call the server with any credential that its sandbox holds. An [environment key](https://platform.claude.com/docs/en/managed-agents/self-hosted#the-three-credentials) can read events and post tool results in every session in its environment. So run each session in a sandbox of its own, and keep the environment key out of that sandbox. The stacks in [Get started with self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart) and [Deploy on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes) do this.
managed-agents/self-hosted/agent-identity New page · 426 lines, new page
## Agent identity ### Set up the issuer ### Send the token to an MCP server ### What the receiver checks ### Test the MCP gateway ## Secret manager ### Grant access ### Check the setup ### Secret manager limits ## Environment variable credentials ### Turn on the gateway in the Compose stack ### Turn on the gateway on Kubernetes ### Add a credential ### Check the egress gateway ### Renew the certificate ### Egress gateway limits
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Agent identity and credentials
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity
description: Have your self-hosted Claude Managed Agents server sign a short-lived token that names each agent, keep vault credentials in your secret manager, and keep secrets out of sandboxes with the egress gateway.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
This page shows how to let the systems outside your server trust an agent, and where to keep the credentials that your agents use:
* [Agent identity](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#agent-identity): Let an MCP server or gateway give each agent its own access. The server signs a short-lived token that names the agent.
* [Secret manager](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#secret-manager): The server keeps vault credentials in AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault, or Azure Key Vault.
* [Environment variable credentials](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials): Let an agent’s commands call a service while the sandbox holds only a placeholder. The egress gateway puts the real value into the agent’s requests.
## Agent identity
Your server can sign a short-lived JSON Web Token (JWT) that names an agent, and send it with that agent’s MCP calls. The receiver, an MCP server or gateway, checks the token against public keys that you publish, and can then give each agent its own access.
### Set up the issuer
1. **Choose the issuer URL.** You publish your public keys at this URL in step 3, and receivers fetch them from it. A static web host works. Don’t use your server’s address, because the server doesn’t serve the keys at the paths that receivers fetch. Use an `https://` URL with no path or trailing slash, on a hostname that you can keep.
2. **Create the signing key.** Run these commands at the top of the extracted archive:
```bash
mkdir -m 700 agent-identity
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out agent-identity/signing-key.pem
```
3. **Publish the public keys.** This command writes them to `issuer-files/.well-known/`. Serve `issuer-files` at the root of the issuer’s hostname, with the content type `application/json`.
```bash
CMA_AGENT_IDENTITY__ISSUER=https://agents.example.com \
CMA_AGENT_IDENTITY__KEY_FILE=./agent-identity/signing-key.pem \
./cma-server export-issuer -out issuer-files
```
4. **Give the server the key.**
<Tabs>
<Tab title="Docker Compose">
1. If the archive has a `compose` directory, move the key’s directory into it: `mv agent-identity compose/agent-identity`.
2. In `docker-compose.yml`, delete the `volumes: *server_mounts` line of the `cma-server` service, and uncomment the `volumes:` block below it. If you changed the `x-server-mounts` list, make the same change in that block, because the `cma-server` service no longer reads the list.
3. In the directory that holds `agent-identity`, run `sudo chown -R root:root agent-identity`, unless you use Docker Desktop on macOS or rootless Docker.
</Tab>
<Tab title="Kubernetes">
Run `kubectl -n cma-server create secret generic cma-agent-identity --from-file=agent-identity/signing-key.pem`. Then set `server.agentIdentity.secretName` to `cma-agent-identity` in the server chart’s values file.
</Tab>
</Tabs>
5. **Change the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go).** List the receiver’s hostname in [`mcp.allowed_hostnames`](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#mcp-servers-need-an-allowlist-entry), and add this section:
```yaml
agent_identity:
issuer: https://agents.example.com
key_file: /run/cma-server/agent-identity/signing-key.pem
direct:
audiences:
- audience: https://mcp-gateway.example.com
urls:
- https://mcp-gateway.example.com
```
Each `audience` names one receiver, as an `https://` URL. Its `urls` are the URL prefixes of the MCP servers that can receive a token for it.
6. **Restart the server.** In the Compose stack, run `docker compose up -d` from the Compose file’s directory instead of a restart.
To keep the private key out of files, use `key_secret` (your [secret manager](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#secret-manager)) or `kms_key` (AWS KMS or Google Cloud KMS). [Configuration reference](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#agent-identity) describes both settings.
### Send the token to an MCP server
[MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) describes the fields of an agent’s MCP server entry. Add an `auth` field to that entry:
```yaml
mcp_servers:
- type: url
name: tickets
url: https://mcp-gateway.example.com/tickets
auth:
type: agent_token
audience: https://mcp-gateway.example.com
```
For each tool listing or tool call, your server signs a new token. It sends the token as `Authorization: Bearer <token>`, in place of a vault credential. Neither the model nor the worker receives it.
To send both the token and a [vault credential](https://platform.claude.com/docs/en/managed-agents/vaults), replace the `auth` field with an `agent_token_header` field:
```yaml
mcp_servers:
- type: url
name: tickets
url: https://mcp-gateway.example.com/tickets
agent_token_header:
audience: https://mcp-gateway.example.com
```
The vault credential goes in the `Authorization` header, as it does for an entry with neither field. The token goes in an `Agent-Identity-Token` header, with no `Bearer` prefix.
The SDK doesn’t have the `auth` and `agent_token_header` fields. In SDK code, add the field to the MCP server entry as an extra field, or send the request as plain JSON.
These pages show how to send extra fields:
[Python SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/python#undocumented-request-params) · [TypeScript SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript#undocumented-request-params) · [Go SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/go#request-fields) · [Java SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/java#undocumented-parameters) · [PHP SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/php#undocumented-request-parameters) · [Ruby SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/ruby#undocumented-request-params)
Some of those pages show extra fields only at the top level of a request, not inside a list entry. In that case, make `mcp_servers` itself the extra field. Send the whole list that way, with `auth` or `agent_token_header` in its entry.
To stop an agent’s tokens, remove the entry from the agent, or archive the agent. A token already sent stays valid until its `exp`, at most 5 minutes by default.
### What the receiver checks
Have the receiver accept a request only if all of these hold:
* The signature verifies under a key in your key set, `<issuer>/.well-known/jwks.json`. The right key is the one whose ID matches `kid` in the token’s header. For an RSA key, the algorithm is `RS256`.
* `iss` equals your issuer URL, and `aud` equals the receiver’s audience.
* `exp` is in the future, and `iat` is not in the future.
* `typ` in the token’s header is `agent+jwt`, and `token_use` is `agent`.
Then apply these rules:
* Base each agent’s access rules on the `agent_id` claim, the agent’s ID in the API.
* Accept a repeated `jti`, because one listing or call can take several requests with the same token.
* Have a gateway call each tool with its own credential, and never pass the token on.
With [single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#single-sign-on), the token of a tool call also names the caller that created the session, in the `actor_sub` and `actor_iss` claims. `actor_sub` doesn’t mean that the caller approved the call. The model chooses each call.
These tokens have no `actor_sub`:
* The token of a tool listing
* The token of a subagent’s call
* Every token of a session that the server API key created
If a rule depends on the caller, refuse a tool call that has no `actor_sub`.
### Test the MCP gateway
The `check-gateway` command sends an MCP gateway valid and invalid tokens. Give the command a settings file of its own, here `check-gateway.yaml`:
1. Copy into the file only the `mcp` and `agent_identity` sections of the server’s settings, and `limits.mcp_call_timeout` if the server sets it.
2. In the copy, change `key_file` to the key’s path on the machine where you run the command, such as `./agent-identity/signing-key.pem`. If you moved the directory into `compose`, that’s `./compose/agent-identity/signing-key.pem`.
For `-agent`, give the ID of an agent that the gateway allows. If root owns the key, use `sudo`.
```bash
./cma-server check-gateway -config check-gateway.yaml -agent agent_011CYm1BLqPXpQRk5khsSXrs \
-url https://mcp-gateway.example.com/tickets
```
The command prints one line for each request that it sends, with the gateway’s answer. A run that passes then prints a line that starts with `check-gateway: PASS`, and exits with code `0`. A run that fails says what went wrong, on a line that starts with `check-gateway:`, and exits with code `1`.
## Secret manager
By default, the server stores the secrets of [vault credentials](https://platform.claude.com/docs/en/managed-agents/vaults) in its database. With a `secret_store` section, the server writes each new vault credential’s secret to your secret manager, under a name prefix that you reserve for it. The database keeps only a reference.
Add the block for your secret manager to the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go):
<Tabs>
<Tab title="AWS Secrets Manager">
```yaml
secret_store:
type: aws_secrets_manager
region: us-east-1
prefix: cma-server/prod/
```
</Tab>
<Tab title="Google Cloud Secret Manager">
```yaml
secret_store:
type: gcp_secret_manager
project: my-project # the project ID, not its number
prefix: cma-server-prod--
```
</Tab>
<Tab title="Azure Key Vault">
```yaml
secret_store:
type: azure_key_vault
vault_url: https://my-vault.vault.azure.net
prefix: cma-server-prod--
```
</Tab>
<Tab title="HashiCorp Vault">
```yaml
secret_store:
type: hashicorp_vault
address: https://vault.internal:8200
prefix: cma-server/prod/
approle:
role_id: <role-id>
secret_id_file: /run/vault/secret-id
#kubernetes: { role: cma-server }
#token_file: /run/vault/token
```
Set exactly one of the three login methods:
* **`approle`:** Use it with Docker Compose, on a host, or with the server Helm chart. The server logs in with a role ID, and with a secret ID that it reads from a file.
* **`kubernetes`:** Use it only on a Kubernetes install of your own, whose server pod has a service account token. The server logs in with that token. The server Helm chart gives the server’s pods no such token, so this method doesn’t work with the chart.
* **`token_file`:** Use it for any other Vault login method. Run Vault Agent beside the server, and have it write its token to this file.
With the server Helm chart, give the server the secret ID in a Secret:
1. Put the secret ID in a Secret in the server’s namespace, and name that Secret in the chart’s `server.extensionFiles.secretName`.
2. Set `approle.secret_id_file` to the secret ID’s path. The chart mounts each key of that Secret as a file under `/run/cma-server/extension-files/`, so a key named `secret-id` is at `/run/cma-server/extension-files/secret-id`.
For the `kubernetes` method, bind the Vault Kubernetes role to the server’s namespace and service account, not to the sandboxes’ namespace, `cma`.
</Tab>
</Tabs>
### Grant access
Give the server one identity, and confine it to the prefix. The `iam/` directory of the archive has a file for each secret manager:
* **AWS Secrets Manager:** Replace the placeholders in `aws-secrets-manager-policy.json`, and attach the policy to the identity that the server runs as.
* **Google Cloud Secret Manager:** Run the commands in the comments of `gcp-secret-manager-role.yaml`.
* **Azure Key Vault:** An Azure role covers a whole key vault, not a prefix. So give the deployment a key vault of its own, one that uses Azure role-based access control. Replace `SUBSCRIPTION_ID` in `azure-key-vault-role.json`, and create a custom role from it. Assign the role on the key vault to the identity that the server runs as.
* **HashiCorp Vault:** Replace the placeholders in `hashicorp-vault-policy.hcl`, and attach the policy to the identity that the server runs as.
To AWS and Google Cloud, the server authenticates as it does for inference. To Azure Key Vault, it authenticates with Microsoft Entra ID, as it does for [Microsoft Foundry](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#microsoft-foundry).
### Check the setup
Run `validate-store` before the first start and after any policy change. It creates, reads, and deletes a test secret under the prefix, without starting the server. Run the command where the server runs, with the server’s environment:
* **Docker Compose:** Run it in the server’s container. On Arm, replace `linux_amd64` in this command with `linux_arm64`:
```bash
docker compose exec cma-server /opt/cma-server/bin/linux_amd64/cma-server validate-store -config /etc/cma-server/config.yaml
```
* **On a host:**
```bash
./cma-server validate-store -config /etc/cma-server/config.yaml
```
* **Kubernetes:** Run the same command as a Job under the server’s service account.
A run that passes ends with a line that says `pass`, and exits with code `0`.
With Azure Key Vault, run this check against your key vault before you rely on it.
### Secret manager limits
* **Choose the settings once.** After any of these changes, the server no longer finds the secrets that it wrote before the change:
* You remove the `secret_store` section.
* You change `type`, `prefix`, `region`, `project`, `vault_url`, `mount`, or `namespace`. The last two are optional HashiCorp Vault settings.
* You point `address` at another Vault.
The server finds those secrets again only when you restore the old values.
* **One identity reads the whole prefix.** If two groups must not be able to reach each other’s credentials, give each group its own deployment. Each deployment needs its own prefix or key vault and its own identity.
## Environment variable credentials
With an [environment variable credential](https://platform.claude.com/docs/en/managed-agents/vaults#add-a-credential), an agent’s commands can call a service while the sandbox holds only a placeholder for the secret. The egress gateway puts the real value into requests to the hosts that the credential lists.
The gateway, `cma-egress`, is a proxy in the release archive that stands between your sandboxes and the internet. It is off by default, and your server accepts these credentials only with the gateway on.
With the gateway on, two limits apply to your sandboxes:
* Sandboxes reach the internet only through the gateway.
* The gateway connects only to the hosts that you list in the server’s `egress.allowed_hostnames` setting.
List exact public hostnames, with no wildcards. Include every host that your agents’ commands download from.
If you built your own sandbox image, start the worker through `cma-with-egress`, as [Build your own sandbox image](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api#what-an-image-of-your-own-needs) shows.
### Turn on the gateway in the Compose stack
From the directory that holds `docker-compose.yml`, add two lines to `.env`. The first lists the hosts that the gateway connects to, with commas between them. The second loads the gateway’s Compose file. Then start the stack:
```bash
echo 'CMA_EGRESS__ALLOWED_HOSTNAMES=api.github.com' >> .env
echo 'COMPOSE_FILE=docker-compose.yml:docker-compose.egress.yml' >> .env
docker compose up --build -d
```
If you use another Compose file too, add it to the `COMPOSE_FILE` line. A `COMPOSE_FILE` variable in your shell overrides the one in `.env`, so unset the shell’s variable.
The gateway’s file sets the server’s other `egress` settings, so don’t add an `egress` section to `compose.config.yaml`.
### Turn on the gateway on Kubernetes
These steps start from the deployment in [Deploy on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes). They use that page’s `REGISTRY`, `TAG`, and `PLATFORM` variables and its two values files. Run the commands from the archive’s top directory, with OpenSSL 3.0 or later. The sandbox chart doesn’t offer the gateway on Azure Kubernetes Service (AKS).
The server chart’s upgrade in these steps also adds a network policy over the servers’ pods. Check two of the ports that it covers before you start:
* **Port 9464, the metrics port:** The policy closes it. If Prometheus scrapes that port, set `server.telemetry.prometheus.scraper.namespace` in `cma-server-values.yaml` to the namespace that Prometheus runs in.
* **Port 8400, the API’s port:** The policy admits every source. If a policy of your own limits who reaches port 8400, that limit stops working. Network policies add up, so a connection gets through if any one policy admits it.
<Steps>
<Step title="Build and push the gateway’s image">
```bash
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted/call-the-api New page · 751 lines, new page
## Prerequisites ## Point your client at your server ## Start a session ## Send a message and read the reply ## Move data in and out of a session ## Find more examples in the hosted guides ## Run your own workers ### Create an environment and its key ### Run a local experiment ### Run workers in production ### Build your own sandbox image ### Move workers from the hosted service ## Next steps
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Call the API on a self-hosted server
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api
description: Point the official SDK, cURL, or the ant CLI at your self-hosted Claude Managed Agents server, start a session, and read the agent's reply. Then connect workers of your own, if you need them.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
A self-hosted Claude Managed Agents server has the same API as Anthropic’s hosted [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) service. Your code calls it with the official SDK, cURL, or the `ant` CLI. Two things change: the base URL is your server’s address, and the API key is your [server API key](https://platform.claude.com/docs/en/managed-agents/self-hosted#the-three-credentials) instead of a Claude API key.
To create agents and run sessions in a browser, see [Use the console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console).
## Prerequisites
* **A running server:** Start one with [Get started with self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart) or [Deploy on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes).
* **Access to the server:** If someone else runs the server, ask them for its address and how to authenticate. If they use single sign-on, see [Let developers log in from a terminal](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal).
* **The `ant` CLI:** Install the latest version, as the [CLI quickstart](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation) shows.
## Point your client at your server
The SDK and the `ant` CLI read the server address from `ANTHROPIC_BASE_URL` and the API key from `ANTHROPIC_API_KEY`. Set both variables in a terminal that you keep for this server.
For the [quickstart stack](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart), run these commands from the directory that holds your `.env` file:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8400
export ANTHROPIC_API_KEY=$(grep '^CMA_SERVER__API_KEY=' .env | cut -d= -f2)
```
On Kubernetes, set the two variables as [Run a session](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#run-a-session) shows, and keep its port-forward running.
In that terminal, every `ant` command, such as `ant beta:agents list`, reaches your server. Don’t run Claude Code or `docker compose` in that terminal, because they read the same variables.
## Start a session
A session needs the ID of an agent and the ID of an environment:
* **Agent:** Create one as [Create an agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#create-an-agent) shows, with your client pointed at your server. If you send raw HTTP requests, replace `https://api.anthropic.com` in the URL with `$ANTHROPIC_BASE_URL`.
* **Environment:** The quickstart stack creates one named `compose-sandbox`, and the Helm charts create one named `default`. `ant beta:environments list` prints each environment’s name and ID. For your own workers, it’s the ID that the admin CLI printed in [Run your own workers](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api#point-a-worker-at-your-server).
Put your two IDs in your shell, in place of these example IDs:
```bash
export AGENT_ID=agent_011CYm1BLqPXpQRk5khsSXrs
export ENVIRONMENT_ID=env_01595EKxaaTTGwwY3kyXdtbs
```
The following example creates a session and sends its first message in the same request:
<CodeGroup>
```bash cURL
curl -sS --fail-with-body "$ANTHROPIC_BASE_URL/v1/sessions" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"agent": "'"$AGENT_ID"'",
"environment_id": "'"$ENVIRONMENT_ID"'",
"initial_events": [
{
"type": "user.message",
"content": [{"type": "text", "text": "List the files in the working directory."}]
}
]
}'
```
```bash CLI
ant beta:sessions create --agent "$AGENT_ID" --environment-id "$ENVIRONMENT_ID" \
--initial-event '{type: user.message, content: [{type: text, text: "List the files in the working directory."}]}'
```
```python Python
import os
from anthropic import Anthropic
client = Anthropic()
session = client.beta.sessions.create(
agent=os.environ["AGENT_ID"],
environment_id=os.environ["ENVIRONMENT_ID"],
initial_events=[
{
"type": "user.message",
"content": [
{"type": "text", "text": "List the files in the working directory."}
],
},
],
)
print(session.id)
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const session = await client.beta.sessions.create({
agent: process.env["AGENT_ID"]!,
environment_id: process.env["ENVIRONMENT_ID"]!,
initial_events: [
{
type: "user.message",
content: [{ type: "text", text: "List the files in the working directory." }]
}
]
});
console.log(session.id);
```
```csharp C#
using System;
using Anthropic;
using Anthropic.Models.Beta.Agents;
using Anthropic.Models.Beta.Sessions;
using Anthropic.Models.Beta.Sessions.Events;
AnthropicClient client = new();
var session = await client.Beta.Sessions.Create(new()
{
Agent = Environment.GetEnvironmentVariable("AGENT_ID")!,
EnvironmentID = Environment.GetEnvironmentVariable("ENVIRONMENT_ID")!,
InitialEvents =
[
new BetaManagedAgentsUserMessageEventParams
{
Content =
[
new BetaManagedAgentsTextBlock
{
Text = "List the files in the working directory.",
},
],
},
],
});
Console.WriteLine(session.ID);
```
```go Go
package main
import (
"context"
"fmt"
"os"
"github.com/anthropics/anthropic-sdk-go/v2"
)
func main() {
client := anthropic.NewClient()
session, err := client.Beta.Sessions.New(context.Background(), anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfString: anthropic.String(os.Getenv("AGENT_ID")),
},
EnvironmentID: os.Getenv("ENVIRONMENT_ID"),
InitialEvents: []anthropic.BetaSessionNewParamsInitialEventUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Text: "List the files in the working directory.",
},
}},
},
}},
})
if err != nil {
panic(err)
}
fmt.Println(session.ID)
}
```
```java Java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.beta.sessions.SessionCreateParams;
import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(System.getenv("AGENT_ID"))
.environmentId(System.getenv("ENVIRONMENT_ID"))
.addInitialEvent(BetaManagedAgentsUserMessageEventParams.builder()
.addTextContent("List the files in the working directory.")
.build())
.build());
IO.println(session.id());
}
```
```php PHP
<?php
use Anthropic\Client;
$client = new Client();
$session = $client->beta->sessions->create(
agent: (string) getenv('AGENT_ID'),
environmentID: (string) getenv('ENVIRONMENT_ID'),
initialEvents: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => 'List the files in the working directory.']],
],
],
);
echo $session->id, "\n";
```
```ruby Ruby
require "anthropic"
client = Anthropic::Client.new
session = client.beta.sessions.create(
agent: ENV.fetch("AGENT_ID"),
environment_id: ENV.fetch("ENVIRONMENT_ID"),
initial_events: [
{
type: :"user.message",
content: [{type: :text, text: "List the files in the working directory."}]
}
]
)
puts session.id
```
</CodeGroup>
The response holds the session’s ID, which starts with `sesn_`. The agent starts work at once. Its built-in tools, such as `bash`, run on a worker that serves the environment.
To watch the session, open the [console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console) in a browser, select **Sessions**, and then select the session. A session that you started with the server API key, as on this page, appears only to administrators.
To follow the session in your terminal instead, pass its ID to [`ant beta:sessions connect`](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/sessions-connect):
```bash
ant beta:sessions connect sesn_011CZkZAtmR3yMPDzynEDxu7
```
For what else a session can do, such as pinning an agent version, see [Start a session](https://platform.claude.com/docs/en/managed-agents/sessions).
## Send a message and read the reply
Put your session’s ID in your shell, in place of this example ID:
```bash
export SESSION_ID=sesn_011CZkZAtmR3yMPDzynEDxu7
```
Wait until the session is idle before you run the next example. The example stops reading when a turn ends. If an earlier turn is still running, its end can stop the example before the reply to your message arrives. The console and `ant beta:sessions connect` both show the session’s status.
The following example sends a message and prints the agent’s reply. It opens the session’s event stream first, because a stream delivers only the events that come after it opens:
<CodeGroup exclude="shell">
<CodeGroupItem language="cURL">
This workflow needs more logic than a single shell command. Use the [Anthropic SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) to run it.
</CodeGroupItem>
<CodeGroupItem language="CLI">
This workflow needs more logic than a single shell command. To send messages and read the replies in your terminal, use [`ant beta:sessions connect`](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/sessions-connect).
</CodeGroupItem>
```python Python
import os
from anthropic import Anthropic
client = Anthropic()
session_id = os.environ["SESSION_ID"]
with client.beta.sessions.events.stream(session_id) as stream:
client.beta.sessions.events.send(
session_id,
events=[
{
"type": "user.message",
"content": [
{"type": "text", "text": "Summarize what you did in one sentence."}
],
},
],
)
for event in stream:
match event.type:
case "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="")
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted/configuration-reference New page · 316 lines, new page
## Where settings go ### Pass a credential to the Compose stack ## How to read the tables ## Server and logging ## Database ## Inference provider ### Cloud providers ### Model gateway ### Short-lived gateway tokens ## Single sign-on ### Issuer entries ## Secret manager ### HashiCorp Vault ## Agent identity ## Egress gateway ## MCP servers ## Git repositories ## Web tools and skills ## Limits, timeouts, and scheduling ## Models and compaction ### Model entries ## Console on your server
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Configuration reference
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference
description: See where the settings of a self-hosted Claude Managed Agents server go, and look up each setting's key, environment variable, default, and purpose.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
This page shows where the settings of a self-hosted Claude Managed Agents server go, and lists them. To set up a feature, see [Single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure), [Agent identity and credentials](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity), and [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider).
## Where settings go
The server reads its settings from one YAML file, and its credentials from its environment.
* **Docker Compose:** The file is `compose.config.yaml`, beside `docker-compose.yml` in the extracted archive. Edit it, then run `docker compose restart cma-server` from that directory.
* **Kubernetes:** The file is the `server.config` value of the server Helm chart. Edit it in your values file, run `helm upgrade`, and then restart the servers with `kubectl -n cma-server rollout restart deployment -l anthropic.com/role=server`. See [Deploy on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes).
If the server rejects a setting that the self-hosted pages show, [upgrade the server](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade). `cma-server.example.yaml`, at the top of the archive, annotates the common keys.
Credentials never go in the file. The server reads them from environment variables, or from the identity that your cloud attaches to the host.
### Pass a credential to the Compose stack
In the Compose stack, the server container receives only the variables in the `environment:` list of the `cma-server` service in `docker-compose.yml`. To pass another one:
1. Add its name to that list, and set its value in `.env`.
2. If the credential is a file, put the file beside `docker-compose.yml`, and add a line such as `- ./gcp-key.json:/etc/cma-server/gcp-key.json:ro` to the `x-server-mounts` list in `docker-compose.yml`. The path after the first colon is the file’s path inside the container. Use that path as the variable’s value in `.env`, or as the value of the setting that names the file.
3. Run `docker compose up -d`. A restart doesn’t reread `.env`.
An upgrade replaces `docker-compose.yml`, so repeat these edits in the new release’s file before you start its stack.
## How to read the tables
Each setting has a key in the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go), and most settings also have an environment variable. A variable overrides the file, and the file overrides the default. The server doesn’t start if its settings file has an unknown key, and the error says where the key is.
* **Keys:** Each dot in a key is a level of nesting in the file. For example, `server.addr` is the `addr` key in the `server` section.
* **Variables:** A variable’s name is `CMA_` and then the key in capitals, with two underscores in place of each dot.
* **File only:** The setting has no variable.
* **Environment only:** The server doesn’t start if the setting is in the file.
* **`bedrock` only, and similar labels:** The setting works only with that provider, store, or secret manager. If you set it while you use a different one, the server doesn’t start.
* **Lists:** A list is a YAML list in the file, and a comma-separated string in a variable.
* **Durations:** A duration is a number with a unit, such as `30s`, `5m`, or `1h`.
* **Integers:** An integer is plain digits, with no sign, leading zero, or underscore.
At startup, the server logs the settings that you set and most defaults, each with where its value came from, such as `server.addr=0.0.0.0:8400 (file)`. It logs `***` in place of a secret, except a server API key that it generated itself.
## Server and logging
| Setting<br />Environment variable | Default | Description |
| --------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `server.addr`<br />`CMA_SERVER__ADDR` | `127.0.0.1:8400` | The address that the server listens on. |
| `server.api_key`<br />`CMA_SERVER__API_KEY` | None | The [server API key](https://platform.claude.com/docs/en/managed-agents/self-hosted#the-three-credentials). Required with `store.backend: postgres`, unless `auth.refuse_server_api_key` is `true`. With `memory`, the server generates a key if you set none, and logs it at startup. |
| `server.id_marker`<br />`CMA_SERVER__ID_MARKER` | `local` | What follows the prefix in each ID that the server creates. `local` gives `sesn_local_01…`, and `none` gives `sesn_01…`, as on the hosted service. |
| `server.shutdown_delay`<br />`CMA_SERVER__SHUTDOWN_DELAY` | `0s` | How long the server keeps serving after a stop signal, while its readiness probe answers `not_ready`. The maximum is `5m`. |
| `server.max_connections`<br />`CMA_SERVER__MAX_CONNECTIONS` | `0` | The most connections that the server holds open at once on `server.addr`. `0` means no limit. At the limit, a new connection waits until another closes or its client gives up. Event streams, worker polls, and health probes all count. On Kubernetes, the kubelet restarts a pod whose probes wait too long. |
| `logging.format`<br />`CMA_LOGGING__FORMAT` | `text` | The log format, `text` or `json`. |
| `logging.level`<br />`CMA_LOGGING__LEVEL` | `info` | The lowest level that the server logs. The values are `debug`, `info`, `warn`, and `error`. |
| `logging.errors`<br />`CMA_LOGGING__ERRORS` | `false` | Whether the log includes the error messages of `4xx` and `5xx` responses. A message can quote a value that the client sent. |
| `health.check_interval`<br />`CMA_HEALTH__CHECK_INTERVAL` | `60s` | How often the server checks its inference credential for the readiness probe. The minimum is `5s`. |
| `health.upstream_affects_readiness`<br />`CMA_HEALTH__UPSTREAM_AFFECTS_READINESS` | `true` | Whether the readiness probe answers `503` while the inference provider rejects the server’s credential. |
## Database
Every setting in this table except `store.backend` applies only with `store.backend: postgres`.
| Setting<br />Environment variable | Default | Description |
| ------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `store.backend`<br />`CMA_STORE__BACKEND` | `memory` | Where the server keeps its data. The values are `memory`, which a restart erases, and `postgres`. |
| `store.database_url`<br />`CMA_STORE__DATABASE_URL` | None | The PostgreSQL connection string. Required with `postgres`. |
| `store.control_database_url`<br />`CMA_STORE__CONTROL_DATABASE_URL` | The value of `store.database_url` | The connection string for the server’s own upkeep connections, such as its heartbeat. |
| `store.listen_database_url`<br />`CMA_STORE__LISTEN_DATABASE_URL` | The value of `store.database_url` | The connection string for the one connection on which the server listens for notifications. A connection pooler in transaction mode can’t carry that connection. |
| `store.migrate_database_url`<br />`CMA_STORE__MIGRATE_DATABASE_URL` | The value of `store.database_url` | The connection string that the server’s `migrate` command uses. A connection pooler in transaction mode can’t carry that connection. |
| `store.pool_max_conns`<br />`CMA_STORE__POOL_MAX_CONNS` | `24` | The most database connections that one server’s pool opens. The minimum is `4`. |
| `store.event_poll_interval`<br />`CMA_STORE__EVENT_POLL_INTERVAL` | `25ms` | How often each server asks the database for the latest events in the sessions that its clients are streaming. An event that another server recorded waits at most about this long to reach the stream. |
| `store.credentials_encryption_key`<br />`CMA_STORE__CREDENTIALS_ENCRYPTION_KEY`<br />Environment only | None | The key that encrypts stored credentials in the database, as 32 random bytes in base64. The Docker Compose and Kubernetes installs set it. The server doesn’t start without it, unless `store.allow_unencrypted_credentials` is `true`. |
| `store.allow_unencrypted_credentials`<br />`CMA_STORE__ALLOW_UNENCRYPTED_CREDENTIALS` | `false` | Whether the server can start without `store.credentials_encryption_key`. Without the key, it stores credentials in the database in plain text. `true` has no effect if `git.allowed_hostnames` lists a host, or if you install with Docker Compose or the server Helm chart. |
| `store.credentials_encryption_keys_retiring`<br />`CMA_STORE__CREDENTIALS_ENCRYPTION_KEYS_RETIRING`<br />Environment only | None | Earlier encryption keys that the server can still decrypt with during a key rotation. This setting needs `store.credentials_encryption_key`. |
## Inference provider
To set up a provider, see [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider).
| Setting<br />Environment variable | Default | Description |
| -------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inference.provider`<br />`CMA_INFERENCE__PROVIDER` | `anthropic` | Where the server sends model calls. The values are `anthropic`, `bedrock`, `vertex`, `foundry`, and `gateway`. |
| `inference.headers`<br />File only | None | Request headers that the server adds to every model call, as a map of header name to value. Quote every value. The server refuses to start on an unquoted value that YAML doesn’t read as text, such as `true` or `0042`. With `gateway`, a value can also be `{ from: request_id }` or `{ from: session_id }`. |
| `inference.model_fallback`<br />`CMA_INFERENCE__MODEL_FALLBACK` | `true` | `anthropic` only. Whether model calls ask for [server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback). Those calls carry a `fallbacks` field and the header `anthropic-beta: server-side-fallback-2026-07-01`. A proxy that you name in `ANTHROPIC_BASE_URL` might reject the field or the header. If it answers with a `400` or a `422`, the server sends the call again without them. |
| `inference.metadata.user_id`<br />`CMA_INFERENCE__METADATA__USER_ID` | None | `anthropic` and `foundry` only, or `gateway` with `format: anthropic-messages`. One identifier that the server sends as `metadata.user_id` with every model call. The startup log prints it. Without this setting, the server sends the label `cma-server`. You can’t turn `metadata.user_id` off. If the provider or a proxy rejects it, every model call fails. See [What leaves your network](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#what-leaves-your-network). |
### Cloud providers
| Setting<br />Environment variable | Default | Description |
| -------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inference.region`<br />`CMA_INFERENCE__REGION` | None | `bedrock` and `vertex` only. The AWS Region, or the location in Google Cloud’s Agent Platform. Required with `vertex`. Without it, `bedrock` uses the Region that the AWS SDK’s default chain finds. |
| `inference.project_id`<br />`CMA_INFERENCE__PROJECT_ID` | None | `vertex` only. The Google Cloud project that the calls are made in. Required unless the Google credential names a project. |
| `inference.api`<br />`CMA_INFERENCE__API` | `invoke_model` | `bedrock` only. The Amazon Bedrock API that carries the calls, `invoke_model` or `messages`. `messages` is the endpoint that [Claude in Amazon Bedrock (`bedrock-mantle`)](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) describes. With it, the server’s AWS role needs `bedrock-mantle:CreateInference` in place of `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`. |
| `inference.list_models`<br />`CMA_INFERENCE__LIST_MODELS` | `true` | `bedrock` only. Whether the server reads Amazon Bedrock’s lists of Claude models. With those lists, agents can use a model that came out after your server’s release. |
| `inference.model_id_prefix`<br />`CMA_INFERENCE__MODEL_ID_PREFIX` | `us.anthropic.` | `bedrock` only. The text that the server puts before each model ID. With `inference.api: messages`, the default is `anthropic.`. |
| `inference.model_id_overrides`<br />File only | None | `bedrock`, `vertex`, and `foundry` only. A map of model ID to the exact ID that the provider expects. With `foundry`, that ID is a deployment name. |
| `inference.foundry.resource`<br />`CMA_INFERENCE__FOUNDRY__RESOURCE` | None | `foundry` only. The name of the Foundry resource. Set this or `inference.foundry.base_url`, not both. |
| `inference.foundry.base_url`<br />`CMA_INFERENCE__FOUNDRY__BASE_URL` | None | `foundry` only. By default, calls go to `https://<resource>.services.ai.azure.com/anthropic`. To reach the resource at another address, such as a proxy, set that URL here instead. It ends in `/anthropic`. |
### Model gateway
These settings apply only with `inference.provider: gateway`. See [Model gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#model-gateway).
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `inference.gateway.url`<br />`CMA_INFERENCE__GATEWAY__URL` | None | Required. The URL that receives model calls. With `bedrock-invoke` and `vertex-raw-predict`, `{model}` marks where the model ID goes. With `vertex-raw-predict`, the URL must end in `/{model}:rawPredict`. |
| `inference.gateway.format`<br />`CMA_INFERENCE__GATEWAY__FORMAT` | None | Required. The request format that the gateway accepts: `bedrock-invoke`, `anthropic-messages`, or `vertex-raw-predict`. |
| `inference.gateway.model_map`<br />File only | None | Required. A map of each model ID that agents can name to the ID that the gateway expects. Quote every value, as for `inference.headers`. |
| `inference.gateway.streaming`<br />`CMA_INFERENCE__GATEWAY__STREAMING` | `false` | Whether the server sends the format’s streamed call, so that replies arrive as the model produces them. With `bedrock-invoke`, that call goes to the same URL with `-with-response-stream` appended. With `vertex-raw-predict`, that call goes to the same URL with `:streamRawPredict` in place of `:rawPredict`. Leave `:rawPredict` in `inference.gateway.url`. |
| `inference.credentials.supplier`<br />`CMA_INFERENCE__CREDENTIALS__SUPPLIER` | None | Required. Where the gateway’s credential comes from. `env` means one long-lived key, `oauth_exchange` means short-lived tokens, and `azure` means Microsoft Entra ID tokens issued to the server’s own Azure identity. |
| `inference.credentials.env.var`<br />`CMA_INFERENCE__CREDENTIALS__ENV__VAR` | None | Required with `env`. The name of the environment variable that holds the key. |
| `inference.credentials.azure.scope`<br />`CMA_INFERENCE__CREDENTIALS__AZURE__SCOPE` | None | Required with `azure`. The scope that the server requests tokens for: the application ID URI or the application ID of your gateway’s app registration, followed by `/.default`. |
| `inference.credentials.send_as.header`<br />`CMA_INFERENCE__CREDENTIALS__SEND_AS__HEADER` | `Authorization` | The request header that carries the credential. |
| `inference.credentials.send_as.scheme`<br />`CMA_INFERENCE__CREDENTIALS__SEND_AS__SCHEME` | `Bearer` | The word that precedes the credential in the header. Under any header other than `Authorization`, the default is no scheme. |
| `inference.credentials.renew_before`<br />`CMA_INFERENCE__CREDENTIALS__RENEW_BEFORE` | `90s` | With `oauth_exchange`, how early the server replaces a token. It stops using a cached token this long before the token expires, and gets another one for the next model call. Keep the value below your tokens’ lifetime. The server refuses any token that is valid for less than this value. |
### Short-lived gateway tokens
These settings apply only with `inference.credentials.supplier: oauth_exchange`. See [Short-lived tokens](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#short-lived-tokens). The `exchange` settings are optional as a group. Without them, the server sends the gateway what the callback returns.
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inference.credentials.assertion.url`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__URL` | None | Required. The URL of the HTTP callback that returns a signed assertion. |
| `inference.credentials.assertion.auth.type`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__AUTH__TYPE` | None | Required. How the server authenticates to the callback, `mtls` or `bearer_file`. |
| `inference.credentials.assertion.auth.cert_file`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__AUTH__CERT_FILE` | None | Required with `mtls`. The path of the server’s client certificate. |
| `inference.credentials.assertion.auth.key_file`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__AUTH__KEY_FILE` | None | Required with `mtls`. The path of the client certificate’s private key. |
| `inference.credentials.assertion.auth.token_file`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__AUTH__TOKEN_FILE` | None | Required with `bearer_file`. The path of a file that holds the bearer token. |
| `inference.credentials.assertion.auth.ca_file`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__AUTH__CA_FILE` | None | The path of a PEM file with the certificate authorities that the server trusts for the callback. |
| `inference.credentials.assertion.forward_metadata`<br />`CMA_INFERENCE__CREDENTIALS__ASSERTION__FORWARD_METADATA` | None | The session metadata keys whose values the server sends to the callback. They arrive as `session.metadata` in the request body. The values are whatever the API caller wrote, so check them against your own records. |
| `inference.credentials.exchange.token_url`<br />`CMA_INFERENCE__CREDENTIALS__EXCHANGE__TOKEN_URL` | None | Required for an exchange. The OAuth token endpoint where the server exchanges the assertion for a token. |
| `inference.credentials.exchange.grant_type`<br />`CMA_INFERENCE__CREDENTIALS__EXCHANGE__GRANT_TYPE` | None | Required for an exchange. The `grant_type` that the server sends, such as `urn:ietf:params:oauth:grant-type:jwt-bearer`. |
| `inference.credentials.exchange.encoding`<br />`CMA_INFERENCE__CREDENTIALS__EXCHANGE__ENCODING` | `form` | How the server encodes the exchange request, `form` or `multipart`. |
| `inference.credentials.exchange.expiry`<br />`CMA_INFERENCE__CREDENTIALS__EXCHANGE__EXPIRY` | `expires_in` | Where the server reads the token’s lifetime. `expires_in` means the earlier of the response’s `expires_in` and the token’s `exp` claim. `jwt_exp` means the `exp` claim alone. |
| `inference.credentials.exchange.headers`<br />File only | None | Extra request headers for the token endpoint, as a map of header name to value. Quote every value, as for `inference.headers`. |
| `inference.credentials.exchange.ca_file`<br />`CMA_INFERENCE__CREDENTIALS__EXCHANGE__CA_FILE` | None | The path of a PEM file with the certificate authorities that the server trusts for the token endpoint. |
## Single sign-on
To set up sign-in, see [Single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure).
| Setting<br />Environment variable | Default | Description |
| ------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.issuers`<br />File only | None | The OpenID Connect issuers whose tokens the server accepts. See [Issuer entries](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#issuer-entries). |
| `auth.refuse_server_api_key`<br />File only | `false` | Whether the server refuses the server API key on every route. `true` needs at least one issuer. Every admin CLI command that takes the key then fails. |
### Issuer entries
Each entry of `auth.issuers` takes these fields:
| Field | Default | Description |
| ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | None | Required. A label for the issuer in log lines. The server also records it as part of the creator of each resource. |
| `issuer_url` | None | Required. The issuer’s URL, exactly as in the `iss` claim of its tokens. The server fetches signing keys from this host and from the `jwks_uri` host that the issuer’s discovery document names. If your network limits outbound connections, admit both hosts. |
| `audiences` | None | Required. The values that a token’s `aud` claim can carry. If applications forward tokens that they already hold, list each application’s client ID. |
| `roles` | None | A list of `group` and `role` pairs. A group matches exactly, or as a pattern in which `*` stands for any run of characters and `?` for one character. A role is `admin`, `developer`, or `user`. The highest matching role wins. Set `roles`, `default_role`, or both. |
| `default_role` | None | The role of a caller whose groups match no `roles` entry. Set it only if everyone who can get a token for this audience should have access. |
| `subject_claim` | `sub` | The claim that identifies the caller. A name that starts with `/` is a JSON Pointer into nested claims. |
| `stable_subject` | `false` | Applies only if `subject_claim` names a claim other than `sub` or `oid`. Set `true` to confirm that your provider never changes a person’s value of that claim and never gives it to someone else. The server doesn’t check this. With `false`, [agent tokens](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#agent-identity) omit the `actor_sub` and `actor_iss` claims. |
| `groups_claim` | `groups` | The claim that lists the caller’s groups. A name that starts with `/` is a JSON Pointer into nested claims. |
| `jwks_file` | None | The path of a JSON Web Key Set file. The server reads signing keys from it and never contacts the issuer. |
| `login_client_id` | None | The client ID of the application that [terminal login](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal) uses. Only one issuer can set it. |
| `login_scopes` | `[openid]` | The scopes that the login requests. This field needs `login_client_id`. |
## Secret manager
To set up a store, see [Secret manager](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#secret-manager).
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `secret_store.type`<br />`CMA_SECRET_STORE__TYPE` | None | The secret manager that holds vault credentials. The values are `aws_secrets_manager`, `gcp_secret_manager`, `azure_key_vault`, and `hashicorp_vault`. Without it, vault credentials stay in the database. |
| `secret_store.prefix`<br />`CMA_SECRET_STORE__PREFIX` | None | Required with `secret_store.type`. The start of the name of every secret that the server creates. It must end with `/` for `aws_secrets_manager` and `hashicorp_vault`, with `-` or `_` for `gcp_secret_manager`, and with `-` for `azure_key_vault`. |
| `secret_store.region`<br />`CMA_SECRET_STORE__REGION` | None | `aws_secrets_manager` only, and required there. The AWS Region that holds the secrets. |
| `secret_store.project`<br />`CMA_SECRET_STORE__PROJECT` | None | `gcp_secret_manager` only, and required there. The ID of the Google Cloud project that holds the secrets. |
| `secret_store.vault_url`<br />`CMA_SECRET_STORE__VAULT_URL` | None | `azure_key_vault` only, and required there. The key vault’s URL, such as `https://my-vault.vault.azure.net`. |
### HashiCorp Vault
These settings apply only with `secret_store.type: hashicorp_vault`. Set exactly one login method: `token_file`, `kubernetes`, or `approle`.
| Setting<br />Environment variable | Default | Description |
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `secret_store.address`<br />`CMA_SECRET_STORE__ADDRESS` | None | Required. The Vault server’s `https://` URL. |
| `secret_store.namespace`<br />`CMA_SECRET_STORE__NAMESPACE` | None | The Vault Enterprise namespace that holds the mount and the login method. |
| `secret_store.mount`<br />`CMA_SECRET_STORE__MOUNT` | `secret` | The path of the KV version 2 secrets engine. |
| `secret_store.ca_cert_file`<br />`CMA_SECRET_STORE__CA_CERT_FILE` | None | The path of a PEM file with the certificate authority that signed Vault’s certificate. |
| `secret_store.token_file`<br />`CMA_SECRET_STORE__TOKEN_FILE` | None | The path of a file with a Vault token that another process, such as Vault Agent, keeps current. |
| `secret_store.kubernetes.role`<br />`CMA_SECRET_STORE__KUBERNETES__ROLE` | None | Required with `kubernetes`. The Vault role that the server logs in as. Bind it to the server’s service account. |
| `secret_store.kubernetes.mount_path`<br />`CMA_SECRET_STORE__KUBERNETES__MOUNT_PATH` | `kubernetes` | Where Vault mounts the Kubernetes login method. |
| `secret_store.kubernetes.service_account_token_file`<br />`CMA_SECRET_STORE__KUBERNETES__SERVICE_ACCOUNT_TOKEN_FILE` | `/var/run/secrets/kubernetes.io/serviceaccount/token` | The path of the service account token that the server presents at each login. |
| `secret_store.approle.role_id`<br />`CMA_SECRET_STORE__APPROLE__ROLE_ID` | None | Required with `approle`. The ID of the AppRole role that the server logs in as. |
| `secret_store.approle.secret_id_file`<br />`CMA_SECRET_STORE__APPROLE__SECRET_ID_FILE` | None | Required with `approle`. The path of a file with the role’s `secret_id`. |
| `secret_store.approle.mount_path`<br />`CMA_SECRET_STORE__APPROLE__MOUNT_PATH` | `approle` | Where Vault mounts the AppRole login method. |
## Agent identity
To set up agent tokens, see [Agent identity](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#agent-identity). Any setting in this table turns the feature on. The server then needs `agent_identity.issuer` and exactly one place for the signing key: `key_file`, `key_secret`, or `kms_key`.
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_identity.issuer`<br />`CMA_AGENT_IDENTITY__ISSUER` | None | The `https://` URL that agent tokens carry as their `iss` claim. |
| `agent_identity.key_file`<br />`CMA_AGENT_IDENTITY__KEY_FILE` | None | The path of the PEM private key that signs the tokens. |
| `agent_identity.key_secret`<br />`CMA_AGENT_IDENTITY__KEY_SECRET` | None | The name of the secret that holds the PEM private key in your secret manager, without `secret_store.prefix`. |
| `agent_identity.kms_key.provider`<br />`CMA_AGENT_IDENTITY__KMS_KEY__PROVIDER` | None | The key management service that holds the signing key, `aws_kms` or `gcp_kms`. |
| `agent_identity.kms_key.key`<br />`CMA_AGENT_IDENTITY__KMS_KEY__KEY` | None | Required with `kms_key.provider`. The key’s ARN for `aws_kms`, or the resource name of one key version for `gcp_kms`. |
| `agent_identity.published_key_files`<br />`CMA_AGENT_IDENTITY__PUBLISHED_KEY_FILES` | None | The paths of extra PEM public keys for the key set. The server publishes these keys but never signs with them. Use this setting when you rotate the signing key. |
| `agent_identity.token_lifetime`<br />`CMA_AGENT_IDENTITY__TOKEN_LIFETIME` | `5m` | The time from a token’s `iat` claim to its `exp` claim. A token already sent stays valid until its `exp`. The minimum is `limits.mcp_call_timeout` plus `1m`, and the maximum is `15m`. |
| `agent_identity.jwks_uri`<br />`CMA_AGENT_IDENTITY__JWKS_URI` | `<issuer>/.well-known/jwks.json` | The URL that the issuer’s discovery document gives for the key set. Set it only if you host the key set somewhere other than under the issuer URL. |
| `agent_identity.direct.audiences`<br />File only | None | The receivers that can get a token. Each entry has an `audience` URL and `urls`, the URL prefixes of that receiver’s MCP servers. |
| `agent_identity.actor.enabled`<br />`CMA_AGENT_IDENTITY__ACTOR__ENABLED` | `true` | Whether the server adds the `actor_sub` and `actor_iss` claims to the token of a tool call. `actor_sub` is the subject of the caller that created the session. `actor_iss` is the `issuer_url` of the `auth.issuers` entry that the caller signed in through. Neither claim means that the caller approved the call. |
## Egress gateway
To set up the gateway, see [Environment variable credentials](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials). Set the other settings in this table only with `egress.gateway_enabled: true`. Without it, the server doesn’t start if any of them is set.
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `egress.gateway_enabled`<br />`CMA_EGRESS__GATEWAY_ENABLED` | `false` | Whether the server works with the egress gateway and accepts `environment_variable` vault credentials. |
| `egress.allowed_hostnames`<br />`CMA_EGRESS__ALLOWED_HOSTNAMES` | None | The exact public hostnames that sandboxes can reach through the gateway. An entry with no port, such as `api.example.com`, allows port 443 for `https` URLs and port 80 for `http` URLs. An entry with a port, such as `api.example.com:8443`, allows that port alone. |
| `egress.swap_header_names`<br />`CMA_EGRESS__SWAP_HEADER_NAMES` | `Authorization` | The request headers in which the gateway can put a credential’s value. A list of your own replaces the default, so include `Authorization` in it. |
| `egress.gateway_listen`<br />`CMA_EGRESS__GATEWAY_LISTEN` | `:8410` | The address of the server’s second listener, which serves only the gateway. The default listens on every interface. |
| `egress.gateway_key_file`<br />`CMA_EGRESS__GATEWAY_KEY_FILE` | None | Required. The path of a file with the key that the gateway presents to the server. |
| `egress.ca_cert_file`<br />`CMA_EGRESS__CA_CERT_FILE` | None | Required. The path of the PEM certificate of the gateway’s certificate authority. |
| `egress.gateway_proxy_address`<br />`CMA_EGRESS__GATEWAY_PROXY_ADDRESS` | None | Required. The `host:port` at which sandboxes reach the gateway’s proxy port. |
## MCP servers
See [MCP servers need an allowlist entry](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#mcp-servers-need-an-allowlist-entry).
| Setting<br />Environment variable | Default | Description |
| --------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
| `mcp.allowed_hostnames`<br />`CMA_MCP__ALLOWED_HOSTNAMES` | None | The exact hostnames of the MCP servers that the server can connect to. Without it, the server connects to none. |
| `mcp.private_hostnames`<br />File only | None | The hostnames from `mcp.allowed_hostnames` that can resolve to a private address. |
| `mcp.max_result_bytes`<br />`CMA_MCP__MAX_RESULT_BYTES` | `102400` (100 KiB) | The most bytes of one MCP tool result that the model sees. The minimum is `1024`. |
## Git repositories
See [Git repositories need an allowlist entry](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#git-repositories-need-an-allowlist-entry).
| Setting<br />Environment variable | Default | Description |
| --------------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `git.allowed_hostnames`<br />`CMA_GIT__ALLOWED_HOSTNAMES` | None | The Git hosts that a session’s repositories can be on. Hostnames match exactly, with no wildcards, and an entry doesn’t cover its subdomains. An entry allows port 443, or the one port that it names, as in `git.example.com:8443`. Without this setting, the server accepts no repositories. |
| `git.private_hostnames`<br />File only | None | The hostnames from `git.allowed_hostnames` that resolve to a private address, written without a port. For any other hostname, the server connects only to its public addresses. |
| `git.allowed_push_refs`<br />`CMA_GIT__ALLOWED_PUSH_REFS` | None | The branches that agents can push to, each under `refs/heads/`. An entry that ends in `/` allows every branch under it. Any other entry allows that one branch. Without this setting, agents can’t push. |
| `git.commit_author_name`<br />`CMA_GIT__COMMIT_AUTHOR_NAME` | `Claude` | The author name on the agent’s commits. Set it together with `git.commit_author_email`. |
| `git.commit_author_email`<br />`CMA_GIT__COMMIT_AUTHOR_EMAIL` | `[email protected]` | The author email address on the agent’s commits. Set it together with `git.commit_author_name`. |
| `git.max_concurrent_transfers`<br />`CMA_GIT__MAX_CONCURRENT_TRANSFERS` | `32` | The most Git requests that one server carries at once. The minimum is `1`. |
| `git.max_session_transfer_bytes`<br />`CMA_GIT__MAX_SESSION_TRANSFER_BYTES` | `8589934592` (8 GiB) | The most Git traffic, in bytes, that one server carries for one session, uploads and downloads together. The count restarts at zero each time a sandbox picks up the session. At the limit, the server refuses the session’s next Git requests with a `403`. `0` means no limit. Otherwise, the minimum is `1048576`. |
| `git.max_push_bytes`<br />`CMA_GIT__MAX_PUSH_BYTES` | `2147483648` (2 GiB) | The most bytes that one push can send. The minimum is `1048576`. |
These limits apply to Git repositories:
* **Protocols:** The server doesn’t support SSH or Git LFS, and it refuses redirects, such as the one for a renamed repository.
* **Tokens:** The Git host must accept the token as the password of HTTP Basic authentication, under the username `x-access-token`.
* **URLs:** A repository URL has one owner and one repository name, so you can’t use a repository in a GitLab subgroup.
* **Hosts:** For a URL on a host that `git.allowed_hostnames` doesn’t list, the server returns a `400`. The error message says that the URL must be on `github.com`, even if you list other hosts.
* **Pushes:** The server always refuses a push that deletes a branch, or that changes anything other than a branch, such as a tag.
## Web tools and skills
See [Web tools need a server setting](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#web-tools-need-a-server-setting).
| Setting<br />Environment variable | Default | Description |
| ----------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `web_tools.enabled`<br />`CMA_WEB_TOOLS__ENABLED` | `false` | Whether agents can use the `web_search` and `web_fetch` tools. If you set another `web_tools` setting, the default is `true`. |
| `web_tools.allowed_domains`<br />`CMA_WEB_TOOLS__ALLOWED_DOMAINS` | None | The only domains that agents can read through the web tools. An entry covers its subdomains. |
| `web_tools.blocked_domains`<br />`CMA_WEB_TOOLS__BLOCKED_DOMAINS` | None | Domains that no agent can read through the web tools. Set this or `web_tools.allowed_domains`, not both. |
| `web_tools.max_content_tokens`<br />`CMA_WEB_TOOLS__MAX_CONTENT_TOKENS` | None | The most tokens of one fetched page that `web_fetch` returns to the model. |
| `skills.max_name_chars`<br />`CMA_SKILLS__MAX_NAME_CHARS` | `64` | The longest `name` that a `SKILL.md` file can have. The maximum is `255`. |
| `skills.max_description_chars`<br />`CMA_SKILLS__MAX_DESCRIPTION_CHARS` | `1024` | The longest `description` that a `SKILL.md` file can have. |
## Limits, timeouts, and scheduling
| Setting<br />Environment variable | Default | Description |
| ------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limits.max_active_sessions`<br />`CMA_LIMITS__MAX_ACTIVE_SESSIONS` | `64` | `store.backend: memory` only. The most sessions that run a turn at once. `0` means no limit. |
| `limits.max_live_subagent_threads`<br />`CMA_LIMITS__MAX_LIVE_SUBAGENT_THREADS` | `25` | The most subagent threads that one session can hold at once. A thread counts until it’s archived or terminated, so an idle thread counts. At the limit, the agent can’t start another subagent. The minimum is `1`, and the maximum is `4000`. |
| `scheduler.max_concurrent_turns`<br />`CMA_SCHEDULER__MAX_CONCURRENT_TURNS` | `64` | `store.backend: postgres` only. The most turns that one server runs at once. The minimum is `1`. The server keeps extra slots for subagent turns alone. Their number is a quarter of this value, and at least `4`. With the default, one server can run 64 turns plus 16 subagent turns. |
| `scheduler.claim_poll_interval`<br />`CMA_SCHEDULER__CLAIM_POLL_INTERVAL` | `1s` | `store.backend: postgres` only. How often a server with a free turn slot looks in the database for waiting work. The minimum is `50ms`. |
| `scheduler.sandbox_claim`<br />`CMA_SCHEDULER__SANDBOX_CLAIM` | `immediate` | When a worker can start a session’s sandbox. `immediate` means as soon as the session is created, so the first tool call waits least. `tool_call` means at the session’s first tool call that needs the sandbox, so a session that makes none uses no sandbox. |
| `limits.model_call_timeout`<br />`CMA_LIMITS__MODEL_CALL_TIMEOUT` | `20m` | How long one model call can run, retries included, before the server cancels it. |
| `limits.stream_idle_timeout`<br />`CMA_LIMITS__STREAM_IDLE_TIMEOUT` | `5m` | How long a model call’s response can deliver nothing before the server gives up that attempt. |
| `limits.mcp_call_timeout`<br />`CMA_LIMITS__MCP_CALL_TIMEOUT` | `4m` | How long one MCP tool call can run before the server gives it up. |
## Models and compaction
| Setting<br />Environment variable | Default | Description |
| ------------------------------------ | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `models.settings`<br />File only | None | Changes to the server’s built-in model catalog, keyed by model ID. See [Model entries](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#model-entries). |
| `compaction.triggers`<br />File only | About 90% of each model’s context window | The context size, in tokens, at which a turn on a model compacts its history, keyed by model ID. The minimum is `50000`. Each model must support compaction. |
### Model entries
Each entry of `models.settings` changes a model in the catalog or adds one. For a model in the catalog, a field that you leave out keeps its built-in value.
| Field | Description |
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted/configure New page · 133 lines, new page
## Add your identity provider ### If sign-in fails ### Microsoft Entra ID ## What each role can do ## Let developers log in from a terminal ### Other redirect URIs ## Let people sign in to the console on your server
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Single sign-on
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/configure
description: Let people and services call your self-hosted Claude Managed Agents server with tokens from your identity provider.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
By default, your server accepts one shared key, the [server API key](https://platform.claude.com/docs/en/managed-agents/self-hosted#the-three-credentials). With single sign-on, each person or service can also call the server with a token from your OpenID Connect (OIDC) identity provider. The server gives the caller a role from the groups in the token. The server API key keeps working as an administrator credential.
This page shows how to add your identity provider, what each role can do, and how to let developers log in from a terminal.
## Add your identity provider
<Steps>
<Step title="Register the server in your identity provider">
Register the server as an API with an identifier such as `api://cma-server`, and have clients request tokens for it.
</Step>
<Step title="List the identity provider in the settings file">
Add an entry under `auth.issuers` in the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go):
```yaml
auth:
issuers:
- name: okta
issuer_url: https://login.example.com/oauth2/default
audiences: [api://cma-server]
roles:
- group: cma-admins
role: admin
- group: "eng-*"
role: developer
```
* `name` labels the issuer in log lines, and the server records it as part of the creator of each resource. Don’t change it once people have created resources.
* `issuer_url` must equal the `iss` claim of your tokens.
* `audiences` lists the values that a token’s `aud` claim can carry, such as the identifier from the first step.
* `roles` maps a group to `admin`, `developer`, or `user`. The highest matching role wins.
For the other fields, see [Issuer entries](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#issuer-entries). If your provider is Microsoft Entra ID, also see [Microsoft Entra ID](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#microsoft-entra-id).
</Step>
<Step title="Restart the server and check sign-in">
Restart the server. A caller sends the token as `Authorization: Bearer <token>`, on any route that accepts the server API key.
For every request that a token authenticated, the server logs `auth=oidc`, the issuer’s `name`, and the caller’s subject (`sub=`).
</Step>
</Steps>
### If sign-in fails
| Response | Cause | The server’s log line contains |
| -------------------------------------- | ------------------------------------------------------ | ------------------------------ |
| `401`, the same as for a wrong API key | The token failed a check. The log line has the reason. | `oidc token` |
| `403` | The token’s groups match no `roles` entry | `token holds no role` |
### Microsoft Entra ID
Entra ID differs from other providers in two ways. Both settings go in the issuer entry:
* **The person:** Entra ID gives a person a different `sub` in each client application, so the default would record one person as several creators. Set `subject_claim: oid`.
* **The groups:** Entra ID leaves the `groups` claim out of a token if the person belongs to many groups. That person then matches no `roles` entry. To prevent this, have Entra ID emit only the groups assigned to the application. Or assign app roles to people in Entra ID, and set `groups_claim: roles` so that the server reads app roles in place of groups.
## What each role can do
| Role | Agents, skills, sessions, vaults, and memory stores | Environments |
| ------------------------------- | --------------------------------------------------- | ---------------------------------------------------------- |
| `admin`, and the server API key | Sees and changes everything | Creates, changes, and deletes them, and creates their keys |
| `developer` or `user` | Sees and changes only what that caller created | Lists them, reads them, and runs sessions in them |
`developer` and `user` have the same access. A caller with either role:
* **Can’t see a resource that someone else created.** The resource is absent from lists, and a request for its ID gets the same answer as a request for an unknown ID. That includes everything created with the server API key before you turned on single sign-on.
* **Can’t share a resource** with another caller or a group.
Map `admin` to exact group names. An administrator can also start a session that uses any user’s vault. So give the role only to groups that you trust with every user’s MCP tokens.
## Let developers log in from a terminal
Developers can log in through your identity provider and then use the `ant` CLI under their own identity, without the server API key. You do the first two steps, and each developer does the third.
<Steps>
<Step title="Register an application in your identity provider">
Register a native (“public”) application that has:
* No client secret
* The authorization code grant with PKCE
* The redirect URI `http://127.0.0.1/callback`
Some providers need one of the [other redirect URIs](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#other-redirect-uris).
</Step>
<Step title="Add the application to the issuer entry">
Add `login_client_id` and `login_scopes` to the issuer entry, and restart the server:
```yaml
login_client_id: 0oa1b2c3d4e5f6g7h8i9
login_scopes: [openid, cma-server.access]
```
`login_scopes` lists the scopes that your provider needs to issue a token whose `aud` claim is in the issuer entry’s `audiences` list.
</Step>
<Step title="Log in">
Each developer runs the login from the top of the extracted archive:
```bash
./cma-server-admin login https://cma.example.com
```
The server’s URL must be `https://`, or `http://` to `localhost`, `127.0.0.1`, or `::1`. With one of the [other redirect URIs](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#other-redirect-uris), add its flag to the command.
The command opens the provider’s sign-in page in a browser and then writes an `ant` CLI profile named `cma-server`. Pass `--profile cma-server` to any `ant` command, or run `ant profile activate cma-server` to make the profile the default.
</Step>
</Steps>
When the token expires, the developer runs `login` again. To have the `ant` CLI renew the token instead, add `offline_access` to `login_scopes`.
### Other redirect URIs
| Provider | Redirect URI to register | Flag that developers add to the `login` command |
| ---------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------- |
| Microsoft Entra ID | `http://localhost/callback` | `-listen localhost:0` |
| One that requires the port in the redirect URI | A URI with a fixed port, such as `http://127.0.0.1:8123/callback` | `-listen 127.0.0.1:8123` |
To list the other flags, add `-h` to the `login` command.
## Let people sign in to the console on your server
The console on your server signs people in through the application that you registered for the [terminal login](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal). To finish its setup, see [Set up sign-in](https://platform.claude.com/docs/en/managed-agents/self-hosted/console#set-up-sign-in).
managed-agents/self-hosted/console New page · 81 lines, new page
## Set up sign-in ## Open the console ## Run an agent from the console ## Who sees what ## Turn off the console ## Next steps
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Use the console on your server
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/console
description: Set up sign-in for the console on your self-hosted Claude Managed Agents server, then create agents and run sessions in a browser.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
The console on your server is a webpage that a self-hosted Claude Managed Agents server serves at the same address as its API. In it, you create agents, run sessions, and manage environments, credential vaults, memory stores, and skills, with no SDK or CLI.
It isn’t the Claude Console at platform.claude.com. It shows only what is on your server.
## Set up sign-in
An administrator sets up sign-in once. To try the console on one machine instead, follow [Try the console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart#try-the-console-on-your-server), which uses the release’s mock sign-in provider.
<Steps>
<Step title="Set up single sign-on and the terminal login">
Follow [Single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#single-sign-on), including [Let developers log in from a terminal](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#let-developers-log-in-from-a-terminal). The console signs people in through the application that you registered for the terminal login. It checks each token against the first entry of `audiences` in the issuer entry, so list that application’s audience first.
</Step>
<Step title="Serve the console over HTTPS">
Put a proxy that terminates TLS in front of port 8400. [Put TLS in front of the server](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#put-tls-in-front-of-the-server) lists what the proxy must do. Over plain HTTP, sign-in works only at `localhost` or `127.0.0.1`.
</Step>
<Step title="Add the console’s address to the application">
In your identity provider, add your server’s HTTPS address as a second redirect URI of that application, with a trailing slash: `https://cma.example.com/`. Also allow its origin, `https://cma.example.com`, for cross-origin (CORS) requests.
</Step>
</Steps>
To check the setup, [open the console](https://platform.claude.com/docs/en/managed-agents/self-hosted/console#open-the-console) and sign in.
## Open the console
If your applications call `https://cma.example.com`, open `https://cma.example.com/` in a browser, and click **Continue with SSO**.
You sign in through your identity provider. The console doesn’t accept the server API key. If the console says that logging in isn’t set up on this server, an administrator needs to [set up sign-in](https://platform.claude.com/docs/en/managed-agents/self-hosted/console#set-up-sign-in).
## Run an agent from the console
A blank agent’s built-in tools, such as `bash`, run on a worker, so select an environment that a worker is polling. [What runs where](https://platform.claude.com/docs/en/managed-agents/self-hosted#what-runs-where) says what a worker is. With no worker, the first tool call waits until one runs it. Administrators create environments, and they can see an environment’s workers on its **Workers** tab.
<Steps>
<Step title="Create an agent">
In the sidebar, select **Agents**, and click **Create agent**. Select a starting point, such as **Blank agent**, and enter a name.
Click **Create agent**. The agent’s page opens.
</Step>
<Step title="Start a session">
On the agent’s page, click **Start session**. Select an environment, and click **Create session**. The session’s page opens.
</Step>
<Step title="Send a message">
Enter a message in the box at the bottom of the page, and send it. The page updates as the agent works. If a tool call waits for you, its card shows **Approve** and **Deny**.
</Step>
</Steps>
While a tool call waits for you, the session’s badge says **Idle**.
To open any other session, such as one that an application started through the API, select **Sessions** in the sidebar, and then select the session.
## Who sees what
Your role determines what the console shows, as it does in the API:
* **Administrators** see and change everything on the server.
* **People with the `developer` or `user` role** see every environment. Of the agents, sessions, vaults, memory stores, and skills, they see only what they created under their own sign-in.
An agent or session that was created with the server API key, such as the quickstart’s, appears only to administrators. For the full table, see [What each role can do](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#what-each-role-can-do).
## Turn off the console
The server serves the console by default. To turn it off, set `console.serve: false` in the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go), and restart the server. The server then stops serving the console. Agents and sessions keep working through the API.
## Next steps
* [Call the API on a self-hosted server](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api): Do the same work from your code or the `ant` CLI.
* [Data and access in self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted-data-and-access#who-can-reach-the-server): See who can reach the server and its console.
managed-agents/self-hosted/differences New page · 152 lines, new page
## Features that only your server has ## Features that aren’t available ## Features that behave differently ## Features that need a server setting ### MCP servers need an allowlist entry ### Git repositories need an allowlist entry ### Web tools need a server setting ## Differences that depend on your inference provider ### Models that accept fast mode ## If you already run self-hosted sandboxes
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Differences from the hosted service
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/differences
description: See which features only self-hosted Claude Managed Agents has, which hosted features it doesn't have, which behave differently, and which need a server setting first.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
A self-hosted Claude Managed Agents server has the same API as Anthropic’s hosted [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) service, apart from the differences on this page. The tables say what to do about each. Check them before you move an application to your server. Nothing is copied from the hosted service, session history included. Create your agents, skills, vault credentials, and memory stores again on your server.
## Features that only your server has
| Feature | What it does | How to set it up |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| A choice of inference provider | The server can send model calls to a cloud provider or your own model gateway instead of the Claude API. | [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider) |
| Single sign-on | People call the server with a token from your OpenID Connect identity provider. The server gives each caller a role from the groups in the token. The server API key keeps working. | [Single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#single-sign-on) |
| Your own secret manager | The server can store the secret of each new vault credential in your secret manager instead of its own database. | [Secret manager](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#secret-manager) |
| Agent identity | The server can sign a short-lived token and send it with an agent’s calls to an MCP server. The MCP server or gateway checks the token against public keys that you publish. | [Agent identity](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#agent-identity) |
## Features that aren’t available
| Feature | What happens | What to do instead |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cloud environments](https://platform.claude.com/docs/en/managed-agents/environments) | Only `self_hosted` environments exist. A request for a cloud environment returns a `400` error. | [Deploy self-hosted workers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers) for every environment. |
| The rest of the Claude API, such as the [Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages) and the Models API | The server doesn’t serve it. The SDK’s calls to `/v1/messages` and `/v1/models` return a `404` error. | Send those requests to the Claude API or to your provider, from a client that doesn’t point at your server. |
| [File session resources](https://platform.claude.com/docs/en/managed-agents/files) and the Files API | The server rejects `file` resources, and `/v1/files` doesn’t exist. `memory_store` resources work. | Put files in the worker’s working directory. See [Move data in and out of a session](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api#move-data-in-and-out-of-a-session). To show the model an image or a PDF, send it as `base64` in a message. |
| [`mcp_oauth` vault credentials](https://platform.claude.com/docs/en/managed-agents/vaults) | The server rejects them when you create them, because it has no OAuth flow to refresh a token. | Store the token as a `static_bearer` credential. Update the credential with a new token before the old one expires. |
| [Prebuilt `docx`, `pdf`, `pptx`, and `xlsx` skills](https://platform.claude.com/docs/en/managed-agents/skills) | The server rejects an agent that declares them. Only skills you upload exist. | Download their sources from the [skills repository](https://github.com/anthropics/skills) and upload them as custom skills. |
| [Scheduled deployments](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments) and [dreams](https://platform.claude.com/docs/en/managed-agents/dreams) | The server has no routes for them. | Start sessions from your own scheduler, such as a cron job. Dreams have no replacement. |
| [Webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) | The server sends no webhooks, so a worker that starts on a webhook never starts. | Follow a session through its [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming), or list its events. Keep your workers running so that they can poll. |
## Features that behave differently
| Feature | What’s different | What to do |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Console | The Claude Console at platform.claude.com doesn’t show what is on your server. | Use the [console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console). |
| Tool calls | A session’s model turns run even when no worker is polling its environment. The first tool call then waits, with no error, until a worker runs it. | If a session stops at a tool call, check that a worker is polling its environment. |
| [Memory stores](https://platform.claude.com/docs/en/managed-agents/memory) | The API is complete. The worker, not the server, copies each attached store into the sandbox and writes changes back. See [Memory stores in self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory). | Run your workers on the [latest version of `ant`](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation), with a writable `/mnt/memory` directory. |
| [Custom skill](https://platform.claude.com/docs/en/managed-agents/skills#create-a-custom-skill) uploads | The server doesn’t unpack a zip archive. An upload of one returns a `400` error that says `SKILL.md not found in uploaded files`. | `ant apply` and the Python SDK’s `files_from_dir` helper work as they are. With anything else, upload the skill’s files as separate parts, and start each part’s file name with the skill’s folder, as in `my-skill/SKILL.md`. |
| [Multiagent orchestration](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) | An agent can delegate only to subagents that you list. Dynamic workflows, the advisor, and subagents that the agent defines itself aren’t available. The server accepts a `multiagent` (csharp, go: `Multiagent`) block only if its `type` is `coordinator`. The examples in Multiagent orchestration set `type` to `multiagent_20261001`, which returns a `400` error. A block with an `advisor` (csharp, go: `Advisor`) setting returns a `400` error too. | Set `multiagent` (csharp, go: `Multiagent`) to `{"type": "coordinator", "agents": [...]}`. List 1 to 20 agents in `agents` (csharp, go: `Agents`). Each entry takes one of the [forms of a predefined agent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#predefined-and-inline-agents). |
| [GitHub repository](https://platform.claude.com/docs/en/managed-agents/github) session resources | The server rejects `github_repository` resources until you list their Git hosts in its settings. The server doesn’t discover skills in a repository’s `.claude/skills` directory, so attach skills to the agent. | See [Git repositories need an allowlist entry](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#git-repositories-need-an-allowlist-entry). |
| [MCP servers](https://platform.claude.com/docs/en/managed-agents/mcp-connector) | The server connects only to the MCP hosts that you list in its settings. | See [MCP servers need an allowlist entry](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#mcp-servers-need-an-allowlist-entry). |
| [Web tools](https://platform.claude.com/docs/en/managed-agents/tools-web-restrictions) | The `web_search` and `web_fetch` tools are off until you turn them on in the server’s settings. | See [Web tools need a server setting](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#web-tools-need-a-server-setting). |
| [`environment_variable` vault credentials](https://platform.claude.com/docs/en/managed-agents/vaults) | The server rejects them until you turn on the egress gateway, a proxy between your sandboxes and the internet. | See [Environment variable credentials](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials). Without the gateway, give your sandbox the CLI and API credentials it needs through your own tooling. |
| Custom tool results | The server sends the model the whole text of a custom tool result. If a result is too large for the model’s context window, the server still answers with `200`, and the session then ends with a `session.error`. | Shorten long results in your client before you send them. |
| MCP tool results | The server cuts a result that is larger than its `mcp.max_result_bytes` setting, which is 100 KiB by default. It tells the model that the result was cut, and the rest is lost. The hosted service cuts long results too, but can [save the full result to a file](https://platform.claude.com/docs/en/managed-agents/mcp-connector#mcp-tool-output-handling) for a session in a cloud sandbox. | To change the limit, set `mcp.max_result_bytes` in the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go). |
| Images and documents sent by URL | The server accepts an `image` or `document` block with a `url` source, but the model never sees it, and there is no error. The same happens to an `image` block with a `file` source. | Send the data as `base64`, or as `text` for a document. |
| The `rescheduling` session status | The server retries a model call that fails for a temporary reason and reports each attempt on the event stream, as the hosted service does. The session’s status stays `running` between attempts, so a `statuses[]=rescheduling` filter matches no sessions. | Read retries from the event stream. |
| Session metadata, with a model gateway | If the server’s `inference.credentials.assertion.forward_metadata` setting lists session metadata keys, an update that adds, removes, or changes one of them returns a `400` error. | Set those keys when you create the session. For the setting, see [Short-lived gateway tokens](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#short-lived-gateway-tokens). |
## Features that need a server setting
Each of these features is off, or refuses every host, until you change the server’s [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go).
### MCP servers need an allowlist entry
The server connects only to MCP servers whose hosts you list under `mcp.allowed_hostnames` in its [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go):
```yaml
mcp:
allowed_hostnames:
- mcp.example.com
```
Hosts match exactly, with no wildcards and no subdomains, and the list is empty by default. Until an MCP server’s host is on the list, your server refuses to save an agent that names it.
An MCP server inside your private network needs no [MCP tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview). Put its URL in the agent definition, as [MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) describes, and add its host to the allowlist. Two rules apply to the URL:
* It must use `https`, unless the MCP server is on the loopback address.
* If its host is a name that resolves only to private or reserved addresses, your server refuses to connect by default. Pick the fix that fits your case:
| Your case | Fix |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| The address is in `10.0.0.0/8`, `172.16.0.0/12`, or `192.168.0.0/16`, and you control the name’s DNS records | Keep the name, and also list it under `mcp.private_hostnames` in the settings file. |
| Any other case | Use the IP address in place of the name, in the URL and on the allowlist. The MCP server’s certificate must name that address. |
### Git repositories need an allowlist entry
On the hosted service, sessions that use [self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#session-resources) can’t have `github_repository` resources. On your server, sessions can. The server rejects these resources until you list their Git hosts in its [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go). Agents can push only to the branches that you list:
```yaml
git:
allowed_hostnames:
- github.com
allowed_push_refs:
- refs/heads/agent/
```
Note these points:
* **URL:** The host doesn’t have to be `github.com`, but the URL must use HTTPS.
* **Token:** The token that your client sends never reaches the sandbox, because Git traffic goes through your server, which adds the token.
* **Branches:** Agents aren’t told the list of branches, so name a branch in your prompt.
* **Force pushes:** The server doesn’t refuse force pushes, so an agent can overwrite any branch that you list. To refuse force pushes, protect the branch on your Git host.
[Accessing GitHub](https://platform.claude.com/docs/en/managed-agents/github) shows the request and how to rotate a token. If a clone fails, the session gets a `session.error` event that names the cause. To check a push, look for the branch on your Git host. The server’s log says `ok` even for a push that your host refused.
### Web tools need a server setting
The `web_search` and `web_fetch` tools are off by default. To turn them on, add this to the server’s [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go):
```yaml
web_tools:
enabled: true
```
With this setting alone, each agent’s own domain lists and content limit apply, as on the hosted service.
To limit the tools for every agent, set `web_tools.allowed_domains`, `web_tools.blocked_domains`, or `web_tools.max_content_tokens`. Each of these settings also turns the tools on. The server refuses to save an agent that asks for more than these settings allow. For example, it refuses an agent that allows a domain outside `web_tools.allowed_domains`, or that sets a larger content limit. If such an agent already exists, it can’t start a session until you update it.
While the tools are off, what happens to an agent depends on the `web_search` and `web_fetch` entries in its toolset:
| The toolset has | What happens |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| No entry for either tool | The agent runs without the tools, and no error says so. |
| An entry that only turns its tool off, such as `{"name": "web_fetch", "enabled": false}` | The same. |
| An entry that leaves its tool on, or that sets its domain lists, content limit, or location | The server refuses to save the agent, or to start a session for it. |
Which web tools exist also [depends on your inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#differences-that-depend-on-your-inference-provider).
## Differences that depend on your inference provider
Some features work only under some values of the server’s `inference.provider` setting. The table shows what happens under the other values. Some of these cases return no error.
The `web_search` and `web_fetch` rows apply after you [turn the web tools on](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#web-tools-need-a-server-setting). Until then, neither tool works under any provider.
| Feature | Works when `inference.provider` is | Under the other values | What to do |
| ----------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `web_search` tool | `anthropic`, `vertex`, `foundry` | The server never offers the tool to the model. Agents still save and run, and still list the tool, but the model can’t call it. | Give the agent a custom tool or an MCP server that searches. |
| `web_fetch` tool | `anthropic`, `foundry` | The same as for `web_search`. | Give the agent a custom tool or an MCP server that fetches pages. |
| `inference_geo` (java, php: `inferenceGeo`; csharp, go: `InferenceGeo`) | `anthropic` | The server refuses to save an agent that sets it. | Remove `inference_geo` (java, php: `inferenceGeo`; csharp, go: `InferenceGeo`) from the agent. |
| Fast mode (`speed: "fast"`) | Depends on the provider and on the agent’s model | An agent that sets it has to pass two checks.<br />**When you save the agent:** The server’s model catalog must list the agent’s model as one that accepts fast mode. Otherwise, the server refuses to save the agent.<br />**On each turn:** If the provider doesn’t serve fast mode for that model or deployment, the turn ends with an error. | Remove `speed` (csharp, go: `Speed`) from the agent. See [Models that accept fast mode](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#models-that-accept-fast-mode). |
Under `anthropic`, the server asks for the Claude API’s [server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback) by default. When a safety classifier declines a request, another Claude model, possibly a less capable one, can then answer it. Anthropic picks the model, and no event reports the change, but a read of the session shows that model’s ID. To turn fallback off, set `inference.model_fallback: false`. Under the other providers, such a request ends the turn with a refusal.
Your provider’s documentation says which models and regions support each feature. Custom tools, MCP tools, and the tools that your worker runs are the same under every provider.
### Models that accept fast mode
By default, the server’s model catalog lists four models as ones that accept fast mode: Claude Opus 5.5, Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.6. On Claude Opus 4.6, the Claude API accepts the request but [runs it at standard speed](https://platform.claude.com/docs/en/build-with-claude/fast-mode#checking-which-speed-was-used). These values of `inference.provider` are exceptions:
* **`foundry`:** The catalog lists Claude Opus 5.5 alone.
* **`gateway` with `format: vertex-raw-predict`:** The catalog lists no model.
* **`vertex`:** The catalog lists no model, and no setting changes that.
To change what the catalog lists, see [Model entries](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#model-entries).
## If you already run self-hosted sandboxes
The worker software is the same, so your workers can connect to your server. [Run your own workers](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api#point-a-worker-at-your-server) lists the three values for the process that polls, and what to change first. A worker that starts on a webhook doesn’t work here.
managed-agents/self-hosted/inference-provider New page · 257 lines, new page
## Give the server a cloud credential ## Amazon Bedrock ## Claude Models on Google Cloud (Google managed) ## Microsoft Foundry ## Map a model ID that differs ## Model gateway ### Short-lived tokens ### Microsoft Entra ID tokens ## Check the provider
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Inference provider
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider
description: Send model calls from your self-hosted Claude Managed Agents server to Amazon Bedrock, Claude Models on Google Cloud (Google managed), Microsoft Foundry, or a model gateway that you run, instead of the Claude API.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
By default, your server sends model calls to the Claude API. This page shows how to send them to a cloud provider, or to a model gateway that you run. To change the provider:
1. Put the sample from your provider’s section of this page in the server’s [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go).
2. Give the server the provider’s credential, as the same section shows.
3. Restart the server, as [Where settings go](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go) shows. On Docker Compose, if step 2 changed `.env` or `docker-compose.yml`, run `docker compose up -d` instead, because a restart doesn’t reread them.
4. [Check the provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#check-the-provider).
The `inference.provider` setting takes these values:
| `inference.provider` | Model calls go to | The server authenticates with |
| --------------------- | ---------------------------------------------- | ---------------------------------------- |
| `anthropic` (default) | The Claude API | `ANTHROPIC_API_KEY` |
| `bedrock` | Amazon Bedrock | Your AWS credential |
| `vertex` | Claude Models on Google Cloud (Google managed) | Google Application Default Credentials |
| `foundry` | Microsoft Foundry | A Foundry API key, or Microsoft Entra ID |
| `gateway` | A model gateway that you run | A key or token that your gateway accepts |
Under every value except `anthropic`, the server needs no Claude API key. It ignores `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_BASE_URL`. The one exception is a [model gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#model-gateway) whose key you tell the server to read from one of those variables.
Agents name models by their Claude API IDs, such as `claude-opus-5-5`. The server translates the ID, so an agent definition works unchanged under any provider that serves that model.
Some tools and agent settings depend on the provider. See [Differences that depend on your inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/differences#differences-that-depend-on-your-inference-provider).
## Give the server a cloud credential
How a cloud provider’s credential reaches the server depends on your install:
* **Docker Compose:** [Pass each variable and file to the server](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#pass-a-credential-to-the-compose-stack).
* **Kubernetes:** The server chart passes no AWS, Google Cloud, or Azure variables, so use your cloud’s workload identity. Create a service account with your cloud’s workload identity annotation, and set `server.serviceAccountName` to its name. The chart’s `values.yaml` names the annotation. On Azure Kubernetes Service, also set `server.azureWorkloadIdentity: true`.
## Amazon Bedrock
```yaml
inference:
provider: bedrock
region: us-east-1
#model_id_prefix: global.anthropic.
#model_id_overrides:
# claude-opus-5-5: us.anthropic.claude-opus-5-5
```
The server signs its calls with whatever the AWS SDK’s default credential chain finds on the host. That can be a task role or a pod identity. On a workstation, it can be `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`.
Enable the models that you use in the account. The AWS identity that the server signs with needs these permissions:
* `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on the models and inference profiles that you use.
* `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles` on resource `*`. The server reads Amazon Bedrock’s model lists, so that agents can use a model that’s newer than your server’s release. To skip this, set `inference.list_models: false`.
By default, the server puts `us.anthropic.` before each model ID, which names a cross-region inference profile. To use another family of profiles, uncomment `model_id_prefix`.
## Claude Models on Google Cloud (Google managed)
```yaml
inference:
provider: vertex
region: us-east5
project_id: my-project
#model_id_overrides:
# claude-haiku-4-5: claude-haiku-4-5@20251001
```
`region` is required. It is a location of Google Cloud’s Agent Platform that offers the models you use, or `global`. `project_id` is the project that the calls are made in and billed to.
The server uses Google Application Default Credentials: the file that `GOOGLE_APPLICATION_CREDENTIALS` names, or the service account that Compute Engine, Cloud Run, or GKE attaches. That identity needs the `roles/aiplatform.user` role on the project. The project needs the `aiplatform.googleapis.com` API and the Claude models you use enabled.
## Microsoft Foundry
```yaml
inference:
provider: foundry
foundry:
resource: my-resource
#model_id_overrides:
# claude-opus-5-5: team-a-opus
```
`resource` is the name of your Foundry resource, the first part of its hostname: `<resource>.services.ai.azure.com`.
The credential comes from the server’s environment:
* **A Foundry API key:** Set `ANTHROPIC_FOUNDRY_API_KEY`. In the Compose stack, [pass the variable to the server](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#pass-a-credential-to-the-compose-stack). On Kubernetes, put it in the `cma-server-upstream` Secret. With `earlierNames: true` in your server values file, the Secret has another name. Find it in [Kubernetes: the names of your objects](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#object-names).
* **Microsoft Entra ID:** Leave `ANTHROPIC_FOUNDRY_API_KEY` unset. The server then gets a token for a service principal, a workload identity on Azure Kubernetes Service, or a managed identity. The identity needs an Azure role on the Foundry resource that allows calls to its model deployments. For a service principal, set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET`.
A request to Foundry names a deployment, and the server sends the model ID as the deployment name. If a deployment has another name, map it with [`model_id_overrides`](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#map-a-model-id-that-differs).
## Map a model ID that differs
Your cloud account can have a model under a different ID from the one that the server sends. In that case, uncomment the `model_id_overrides` lines in that provider’s sample, and map the Claude API ID to your account’s ID. Both IDs must name the same model, because the server builds each request for the model that the agent names.
## Model gateway
First check which requests your gateway accepts:
* **The same requests as the Claude API, with one long-lived credential:** Keep the default provider. Set `ANTHROPIC_BASE_URL` to the gateway’s address, and set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` to the credential that the gateway expects.
* **The same requests as the Claude API, but with short-lived tokens, a credential in another header, or its own model IDs:** Set `inference.provider: gateway` with `format: anthropic-messages`.
* **Amazon Bedrock’s InvokeModel format:** Set `inference.provider: gateway` with `format: bedrock-invoke`.
* **The `rawPredict` format of Claude Models on Google Cloud (Google managed):** Set `inference.provider: gateway` with `format: vertex-raw-predict`.
This sample sets `inference.provider: gateway` with `format: bedrock-invoke`, for a gateway that takes one long-lived key. Put the key in an environment variable and name the variable, never the value:
```yaml
inference:
provider: gateway
gateway:
url: "https://llm-gateway.example.com/v1/model/{model}/invoke"
format: bedrock-invoke
# Quote every model_map value.
model_map:
claude-opus-5-5: "us.anthropic.claude-opus-5-5"
claude-sonnet-5: "us.anthropic.claude-sonnet-5"
credentials:
supplier: env
env: { var: LLM_GATEWAY_KEY }
send_as: { header: x-api-key }
```
* `url` is the gateway’s model route. `{model}` marks where the model ID goes. The URL must be `https://` and end in `/invoke`.
* `model_map` maps each model ID that an agent can name to the ID that the gateway expects. The server refuses a model that has no entry.
* `send_as` names the header that carries the key. Without it, the server sends `Authorization: Bearer <key>`.
* In the Compose stack, [pass the variable](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#pass-a-credential-to-the-compose-stack) to the server.
* On Kubernetes, the server chart can’t pass a variable of your own. It does pass `ANTHROPIC_AUTH_TOKEN`, which it reads from the `cma-server-upstream` Secret, so use that name:
1. Set `env: { var: ANTHROPIC_AUTH_TOKEN }`.
2. Put the gateway’s key in the `cma-server-upstream` Secret, under the name `ANTHROPIC_AUTH_TOKEN`. With `earlierNames: true` in your server values file, the Secret has another name. Find it in [Kubernetes: the names of your objects](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#object-names).
For `format: anthropic-messages`, replace the `gateway` block with this one:
```yaml
inference:
gateway:
url: "https://llm-gateway.example.com/v1/messages"
format: anthropic-messages
# Quote every model_map value.
model_map:
claude-opus-5-5: "corp-claude-opus"
claude-sonnet-5: "corp-claude-sonnet"
```
Here, `url` is the whole URL of the gateway’s Messages route, with no `{model}`.
For `format: vertex-raw-predict`, replace the `gateway` block with this one:
```yaml
inference:
gateway:
url: "https://llm-gateway.example.com/v1/projects/my-project/locations/global/publishers/anthropic/models/{model}:rawPredict"
format: vertex-raw-predict
# Quote every model_map value.
model_map:
claude-opus-5-5: "claude-opus-5-5"
claude-sonnet-5: "claude-sonnet-5"
```
Here, `url` is the gateway’s `rawPredict` route, and it must end in `/{model}:rawPredict`. A `model_map` value can’t contain a colon. The `credentials` block stays as it is. The server sends your gateway’s own credential and gets no Google credential.
[Configuration reference](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#model-gateway) lists the gateway’s other settings, such as `streaming`, and the `inference.headers` setting.
### Short-lived tokens
If your gateway takes short-lived tokens, replace the `credentials` block with this one:
```yaml
inference:
credentials:
supplier: oauth_exchange
assertion:
url: "https://app.example.com/assertion"
auth: { type: mtls, cert_file: /etc/callback/client.pem, key_file: /etc/callback/client.key }
exchange:
token_url: "https://login.example.com/oauth2/token"
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer"
```
The server then gets each token in two steps. First, it calls an HTTP callback that you build. The callback returns an assertion, a signed statement that’s usually a JSON Web Token (JWT). Then the server exchanges the assertion at your OAuth token endpoint for the access token that it sends to the gateway.
* `auth` is how the server authenticates to the callback. The sample uses a client certificate. To use a bearer token that the server reads from a file, set `auth: { type: bearer_file, token_file: /etc/callback/token }`.
* `exchange` names your OAuth token endpoint. The server sends that endpoint only `grant_type` and the assertion, with no client ID or secret.
* If your token endpoint needs more than that, leave out `exchange`. Have your callback get the access token itself and return it in the `assertion` field, which the server then sends to the gateway.
* On Kubernetes, put the certificate files in a Secret, name it in the chart’s `server.extensionFiles.secretName`, and point `cert_file` and `key_file` at `/run/cma-server/extension-files/`.
The server sends the callback a `POST` request with this body:
```json
{
"version": 1,
"purpose": "inference",
"session": {
"id": "sesn_011CZkZAtmR3yMPDzynEDxu7",
"agent_id": "agent_011CYm1BLqPXpQRk5khsSXrs",
"environment_id": "env_01595EKxaaTTGwwY3kyXdtbs"
},
"principal": {
"issuer": "https://login.example.com/oauth2/default",
"subject": "00u1a2b3c4d5e6f7g8h9"
},
"request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}
```
`principal` is the caller that created the session. For a [single sign-on](https://platform.claude.com/docs/en/managed-agents/self-hosted/configure#single-sign-on) caller, `issuer` is that caller’s `issuer_url`, and `subject` is the caller’s subject. For the server API key, `principal` is `{"issuer": "cma", "subject": "server"}`. Have the callback ignore fields that it doesn’t know.
The callback answers with status `200` and this body:
```json
{ "assertion": "<signed JWT>", "reuse": "session" }
```
`reuse` says which sessions can use the token. With `session`, the default, only the session in the request can, which suits a token that names one user. With `deployment`, sessions share the token, so return `deployment` only for a token that names your organization and no person.
If the callback or the token endpoint fails, the turn ends with a `session.error` event of type `model_request_failed_error`, and the session stays usable. The reason is in the server log, on a line that contains `credential supplier failed`.
If the token endpoint’s reply has no `expires_in` and the token has no `exp` claim, the server sends the token but doesn’t store it. It then asks for another token on every model call.
For the other settings, such as `renew_before` and `ca_file`, see the Configuration reference’s tables of [model gateway settings](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#model-gateway) and [short-lived token settings](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#short-lived-gateway-tokens).
### Microsoft Entra ID tokens
If your gateway checks Microsoft Entra ID tokens against its own app registration, replace the `credentials` block with this one:
```yaml
inference:
credentials:
supplier: azure
azure: { scope: "api://11111111-2222-3333-4444-555555555555/.default" }
```
`scope` is the application ID URI or the application ID of the gateway’s app registration, followed by `/.default`. Name only the gateway’s own registration. A token for another resource would let whoever runs the gateway act as the server there.
The token is issued to the server’s own Azure identity, set up as for Microsoft Entra ID under [Microsoft Foundry](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider#microsoft-foundry). If the registration requires an app role, assign that role to the identity before you start the server. A later assignment takes effect only when the server gets another token.
The token names that identity and no person, so a gateway that must know which person is calling needs `oauth_exchange`. Run a turn through your gateway, under the identity that your servers will use, before you rely on this supplier.
## Check the provider
After you change the settings and restart the server, look for this line in the server log. It says that the server found a credential for the provider:
```text wrap
level=INFO msg="upstream credential resolved" provider=bedrock
```
The line doesn’t prove that your account has a model enabled. A missing model shows up as a `session.error` on the first turn that uses it.
A gateway that you name in `ANTHROPIC_BASE_URL` might reject the [server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback) field or header. If every turn fails for that reason, set `inference.model_fallback: false` in the [settings file](https://platform.claude.com/docs/en/managed-agents/self-hosted/configuration-reference#where-settings-go).
managed-agents/self-hosted/kubernetes New page · 380 lines, new page
## Prerequisites ## Install the stack ## Run a session ## Put TLS in front of the server ## What sandboxes can reach ## Other clusters ## Free a session’s volume ## Renew keys and certificates ### Renew the environment key that the charts created ### Renew the dispatcher’s certificate ### Replace an environment key for your own workers ## Uninstall the stack ## Next steps
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Deploy on Kubernetes
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes
description: Install the self-hosted Claude Managed Agents server and its sandboxes on a Kubernetes cluster with the two Helm charts in the release archive, then run a first session in a sandbox pod.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
This page takes an empty Google Kubernetes Engine (GKE) cluster to a session that runs a tool in a sandbox pod. It uses the two Helm charts in the release archive, which run the whole stack on the cluster:
* **`cma-server`:** Applies the database schema and runs two servers behind a Service, in the namespace `cma-server`.
* **`cma-sandboxes`:** Runs a dispatcher and the sandbox pods in the namespace `cma`. The dispatcher polls the server for work and gives each session its own sandbox pod, where a worker runs the agent’s shell and file tools.
Your PostgreSQL database stays outside both. On another kind of cluster, the steps are the same, but the cluster prerequisites and a few chart values differ. See [Other clusters](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#other-clusters).
## Prerequisites
* **A cluster:** Kubernetes 1.30 or later, with:
* The Agent Sandbox controller (on GKE, the [Agent Sandbox add-on](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/how-install-agent-sandbox))
* Nodes that run gVisor (on GKE, a [GKE Sandbox node pool](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/sandbox-pods))
* A network plugin that enforces NetworkPolicy (on GKE, Dataplane V2 or Calico)
* **A database:** PostgreSQL 16 or later that the cluster can reach.
* **A registry:** One that the cluster can pull from. You build the images yourself.
* **A Claude API key:** The server uses it for model calls. To use another provider, see [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider).
* **Permissions:** Cluster-admin rights or the equivalent. The install creates namespaces, Roles, and a ClusterRoleBinding.
* **Tools on your machine:** `helm`, `kubectl`, `docker buildx`, `openssl`, and the [`ant` CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation).
* **The release archive:** To request access to the download, contact your Anthropic account team. Extract the archive. Run every command on this page from its top directory.
## Install the stack
<Steps>
<Step title="Build and push the images">
Set these variables to your own values. `PLATFORM` is your nodes’ architecture. `TAG` is the release’s version, which is in the archive’s name after the `v`. Both charts expect images that have this version as their tag.
```bash
REGISTRY=us-docker.pkg.dev/my-project/cma
TAG=1.2.3
PLATFORM=linux/amd64
```
Then build and push the three images:
```bash
docker buildx build --platform "$PLATFORM" -f kubernetes/server/Dockerfile -t "$REGISTRY/cma-server-image:$TAG" --push .
docker buildx build --platform "$PLATFORM" -f kubernetes/dispatcher/Dockerfile -t "$REGISTRY/cma-dispatcher:$TAG" --push .
docker buildx build --platform "$PLATFORM" -t "$REGISTRY/cma-sandbox:$TAG" --push kubernetes/sandbox
```
</Step>
<Step title="Create a secrets file and the database">
Create a file with a generated database password and a placeholder for your Claude API key:
```bash
cat > secrets.env <<EOF
PG_PASSWORD=$(openssl rand -hex 32)
UPSTREAM_KEY=sk-ant-...
EOF
chmod 600 secrets.env
```
Open `secrets.env` in an editor and replace `sk-ant-...` with your Claude API key. Then connect to your database server as an administrator, for example, with `psql`. Run this SQL, with the value of `PG_PASSWORD` in place of `...`:
```sql
CREATE ROLE cma_server LOGIN PASSWORD '...';
GRANT cma_server TO CURRENT_USER;
CREATE DATABASE cma_server OWNER cma_server;
```
</Step>
<Step title="Create the server’s namespace and Secrets">
Create the server’s namespace and its Secrets:
```bash
source secrets.env
ENCRYPTION_KEY=$(openssl rand -base64 32)
kubectl create namespace cma-server
kubectl label namespace cma-server pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest
kubectl -n cma-server create secret generic cma-server-db \
--from-literal=PGPASSWORD="$PG_PASSWORD"
kubectl -n cma-server create secret generic cma-server-keys \
--from-literal=CMA_SERVER__API_KEY="$(openssl rand -hex 32)" \
--from-literal=CMA_STORE__CREDENTIALS_ENCRYPTION_KEY="$ENCRYPTION_KEY"
kubectl -n cma-server create secret generic cma-server-upstream \
--from-literal=ANTHROPIC_API_KEY="$UPSTREAM_KEY"
```
Print the encryption key with `echo "$ENCRYPTION_KEY"`, and keep a copy outside the cluster. The server uses this key to encrypt the credentials that it stores, and a restored database needs it. Then delete `secrets.env`. The `cma-server-db` Secret keeps the database password.
</Step>
<Step title="Create the sandboxes’ namespace and certificate">
Create the sandboxes’ namespace, and a certificate that the dispatcher serves and the sandboxes trust:
```bash
kubectl create namespace cma
kubectl label namespace cma pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 825 \
-subj "/CN=cma-dispatcher.cma.svc" \
-addext "subjectAltName=DNS:cma-dispatcher.cma.svc,DNS:cma-dispatcher.cma.svc.cluster.local" \
-keyout tls.key -out tls.crt
kubectl -n cma create secret tls cma-dispatcher-tls --cert=tls.crt --key=tls.key --dry-run=client -o yaml | kubectl apply -f -
kubectl -n cma create configmap cma-dispatcher-ca --from-file=ca.crt=tls.crt --dry-run=client -o yaml | kubectl apply -f -
```
</Step>
<Step title="Install the server">
Save this as `cma-server-values.yaml`, with your registry and your database’s address:
```yaml
earlierNames: false # a new install
image:
registry: us-docker.pkg.dev/my-project/cma # REGISTRY in the build commands
database:
url: host=10.20.0.5 port=5432 user=cma_server dbname=cma_server sslmode=require # your database's address
environments:
- name: default
namespace: cma
```
Then install the chart:
```bash
helm install cma-server kubernetes/server/chart --namespace cma-server -f cma-server-values.yaml --wait
kubectl -n cma-server logs job/cma-server-environment-default
```
The install creates the environment `default` and stores its key in the Secret `cma-environment` in the namespace `cma`. The second command prints one line that starts `filled Secret cma/cma-environment`. If a first `helm install` fails, run `helm uninstall` for that release before you install again.
</Step>
<Step title="Find the cluster addresses to block">
This install lets sandboxes reach the public internet. A cluster’s API endpoint usually has a public address, so sandboxes could reach it too. To block it, you list its address under `egress.clusterCIDRs` in the next step.
To print the address, run `gcloud container clusters describe my-cluster --format 'value(endpoint)'` with your cluster’s name.
The chart itself blocks the private ranges `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `100.64.0.0/10`. If your cluster has a pod, service, or node range outside those four, list that range as well.
</Step>
<Step title="Install the sandboxes">
Save this as `cma-values.yaml`, with your registry and the addresses from the previous step:
```yaml
platform: gke
image:
registry: us-docker.pkg.dev/my-project/cma # REGISTRY in the build commands
egress:
mode: publicInternet
clusterCIDRs:
- 203.0.113.10/32 # the cluster's API endpoint
cmaServer:
pods:
namespace: cma-server
matchLabels:
anthropic.com/role: server
port: 8400
```
`egress.cmaServer` opens the path to your servers. To compare `publicInternet` with the two other modes, see [What sandboxes can reach](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#what-sandboxes-can-reach).
If your cluster uses one of these DNS setups, add a line to the file:
* **Cloud DNS for GKE, which Autopilot clusters use:** `gke: {dns: cloudDns}`
* **NodeLocal DNSCache:** `gke: {dns: nodeLocalCache, kubeDnsUpstreamIP: <address>}`. To print the address, run `kubectl -n kube-system get svc kube-dns-upstream -o jsonpath='{.spec.clusterIP}'`.
Then install the chart:
```bash
helm install cma kubernetes/chart --namespace cma -f cma-values.yaml --wait
```
</Step>
</Steps>
## Run a session
These steps use a new install’s names. If your server values file has `earlierNames: true`, find the names to use instead in [Kubernetes: the names of your objects](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#object-names).
<Steps>
<Step title="Forward the server’s port">
In one terminal, forward the server’s port to your machine:
```bash
kubectl -n cma-server port-forward svc/cma-server 8400:8400
```
</Step>
<Step title="Point the CLI at your server">
In a second terminal, point the `ant` CLI at the forwarded port and read the server API key from the `cma-server-keys` Secret:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8400
export ANTHROPIC_API_KEY=$(kubectl -n cma-server get secret cma-server-keys -o jsonpath='{.data.CMA_SERVER__API_KEY}' | base64 -d)
```
</Step>
<Step title="Create an agent and ask it to run a command">
Follow three steps of the quickstart’s [Run your first session](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart#run-your-first-session): Create an agent, Start a session, and Ask the agent to run a command. In Start a session, replace `compose-sandbox` with `default`, the name of the environment that the charts create.
</Step>
<Step title="Find the sandbox pod">
The agent replies with the hostname of the pod that ran the command. In a third terminal, `kubectl -n cma get pods` lists a pod of that name beside the dispatcher’s. By default, the pod goes away about 5 minutes after the agent’s turn ends. If it isn’t in the list, ask the agent to run `hostname` again.
To leave the chat, press Ctrl+C.
</Step>
</Steps>
## Put TLS in front of the server
The server speaks plain HTTP on port 8400, and requests from your applications carry the server API key. Before clients outside the cluster connect, put a load balancer or ingress that terminates TLS in front of the `cma-server` Service. It must:
* Pass request paths through unchanged.
* Accept request bodies up to 32 MiB.
* Pass responses through unbuffered.
* Set a default read timeout of 2 minutes. For Git requests, set a read timeout of 10 minutes for paths matching `^/cma-[a-z]+/git/`.
* Send traffic only to servers that are ready. The `cma-server` Service already does this. A load balancer that reaches the pods directly must probe `/cma-server/readyz` on each pod and skip one that returns `503`.
* Never resend a `POST` that a server already accepted.
* Never cache a response.
To check it from outside the cluster, run `curl -s https://<host>/cma-server/healthz`. The response is `{"status":"ok"}`.
## What sandboxes can reach
In `cma-values.yaml`, `egress.mode: publicInternet` lets sandboxes reach the public internet.
This table compares it with the two other modes:
| `egress.mode` | What sandboxes can reach |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `closed`, the default | Your servers and the dispatcher, and no DNS server |
| `publicInternet` | Your servers, the dispatcher, a DNS server, and the public internet |
| `gateway` | Your servers, the dispatcher, and, through the egress gateway, the hosts that you list. See [Turn on the gateway on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#turn-on-the-gateway-on-kubernetes). |
## Other clusters
Set `platform` in `cma-values.yaml` to `eks` for Amazon EKS, to `aks` for Azure Kubernetes Service (AKS), or to `generic` for any other cluster. All three need the upstream Agent Sandbox controller with its extensions, and a network plugin that enforces NetworkPolicy. They also need:
* **Amazon EKS:** A node group with gVisor’s `runsc` registered as a containerd handler named `runsc`. Label the nodes `cma.anthropic.com/sandbox-node=true`, and taint them `cma.anthropic.com/sandbox-node=true:NoSchedule`. The chart registers the `gvisor` RuntimeClass against that handler.
* **AKS:** A node pool with Pod Sandboxing, with the same label and taint as on Amazon EKS. Put the name of the cluster’s network policy engine (`cilium`, `calico`, or `azure`) in `aks.networkPolicy`. Keep `egress.mode: publicInternet`, the only mode that the chart accepts on AKS. Test the chart on your cluster before agents do real work on it.
* **Any other cluster:** A RuntimeClass for gVisor or another sandboxed runtime. Put its name in `runtimeClass.name`.
The folder `kubernetes/chart/examples/` in the archive has a complete values file for each kind of cluster.
## Free a session’s volume
By the chart’s defaults, each session has a 10 GiB volume for `/workspace`. The volume stays after the session’s pod goes away, so the session’s next message finds its files.
| To free | Do this |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| One session’s volume | Archive the session after its turn ends. The dispatcher usually deletes the volume within about 5 minutes. |
| The volumes of sessions that nobody archives | Set `dispatcher.retentionDays` in `cma-values.yaml`. The dispatcher then deletes a volume once its session has been idle for that many days, and the session’s next message starts with an empty `/workspace`. The chart’s default, `0`, keeps every volume. |
The commands in this section are for GKE. On another cluster, use `sandboxclaims` in place of `sandboxes`.
Deleting a session doesn’t free its volume. Archive the session first. Delete it only after `kubectl -n cma get sandboxes -l anthropic.com/session-id=<session-id>` lists nothing.
If you deleted a session without archiving it, free its volume yourself:
1. Find the session’s ID in the dispatcher’s log. This command prints a line for each session that appears to be deleted and still has a volume, with the ID after `session_id`:
```bash
kubectl -n cma logs deployment/cma-dispatcher --since=6m \
| grep "could not check the claim's session" | grep "403 Forbidden"
```
The dispatcher writes these lines every 5 minutes. If the session isn’t listed, wait 5 minutes and run the command again.
2. Confirm that the session is deleted. With the port forwarded and `ANTHROPIC_API_KEY` set as in [Run a session](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#run-a-session), run `ant --base-url http://127.0.0.1:8400 beta:sessions retrieve --session-id <session-id>`. Go on only if the answer is `404 Not Found` with `Session not found`. If the command prints a session, stop: that session isn’t deleted.
3. Delete the Kubernetes object that owns the volume. The session’s files go with it:
```bash
kubectl -n cma delete sandboxes -l anthropic.com/session-id=<session-id>
```
## Renew keys and certificates
An environment key is valid for 365 days, and the dispatcher’s certificate is valid for 825 days. Renew each before it expires.
In an environment key’s last 30 days, the server logs a warning on each request that uses the key. The warning starts `environment key is within 30 days of expiry`.
### Renew the environment key that the charts created
To see when the key expires, run `kubectl -n cma describe secret cma-environment`.
Run these two commands at least once in the key’s last 60 days, from the top directory of the installed release’s archive:
```bash
helm upgrade cma-server kubernetes/server/chart --namespace cma-server -f cma-server-values.yaml --wait
kubectl -n cma rollout restart deployment/cma-dispatcher
```
Each time the server chart’s `helm upgrade` command runs, it stores a new key in the Secret if the key expires within 60 days. Nothing else renews the key. The second command restarts the dispatcher so that it reads the new key.
### Renew the dispatcher’s certificate
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted/quickstart New page · 251 lines, new page
## Prerequisites ## Start the stack ## Run your first session ## Try the console on your server ## Stop the stack ## Troubleshooting ## Next steps
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Get started with self-hosted Claude Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart
description: Start self-hosted Claude Managed Agents on one machine with Docker Compose and run a first session in a sandbox container.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
In this quickstart, you start self-hosted Claude Managed Agents on one machine with Docker Compose. The Compose stack is the server, its PostgreSQL database, and a set of workers that run each session in a sandbox container of its own. You then chat with an agent from your terminal and confirm that its tool calls ran in a sandbox container. You can then open the session in the [console on your server](https://platform.claude.com/docs/en/managed-agents/self-hosted/console).
<Warning>
The Compose stack is for development, not for production. It serves plain HTTP, and by default publishes port 8400 only on the Docker host’s loopback address, `127.0.0.1`. Under some Docker Engine releases before 28.3.3, machines on the host’s local network can reach that port too. Agent tools run in containers on that host, so use a machine that holds nothing you need to protect.
</Warning>
For production, Anthropic recommends the release’s two Helm charts. See [Deploy on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes).
Let Claude Code run this quickstart for you
```text
Set up self-hosted Claude Managed Agents on this machine by following https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart.md. I've already downloaded the release archive.
```
If you’re an agent that runs these steps for a person:
* **Release archive:** Ask the person where the archive is. Only they can download it.
* **Claude API key:** Have the person edit `.env` themselves. Don’t ask for the key, and don’t print `.env`.
* **Variables:** If your shell doesn’t keep variables between commands, set `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY` again in each `ant` command. Without them, `ant` calls the hosted service.
* **Compose:** Put `env -u ANTHROPIC_BASE_URL -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN` in front of each `docker compose` command, so that your shell’s values don’t reach the server.
* **Chat:** `ant beta:sessions connect` needs an interactive terminal. Without one, send the message with `ant beta:sessions:events send`, and read the reply with `ant beta:sessions:events list`. [Send events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#send-events) and [List past events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#list-past-events) have examples.
* **Console:** Do the section “Try the console on your server” only if the person asks for it. If your shell doesn’t keep variables between commands, also put `COMPOSE_FILE=docker-compose.yml:docker-compose.oidc.yml` in front of each `docker compose` command from that section on.
## Prerequisites
* **The release archive:** To request access to the download, contact your Anthropic account team.
* **A Claude API key:** The server uses it for model calls. Create one in the [Claude Console](https://platform.claude.com/settings/keys).
* **Docker:** The [latest version of Docker Engine](https://docs.docker.com/engine/install/) with the Compose plugin (`docker compose`, not the older `docker-compose`).
* **A host with at least two CPUs:** x86-64 or Arm. On a smaller host, set `CMA_SANDBOX_CPUS` to its CPU count when you create `.env`.
* **Network access on first start:** The first build pulls from Docker Hub, the Alpine package repositories, and GitHub releases.
* **Tools:** On the machine where you run Docker, you need `openssl` and the [`ant` CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation).
## Start the stack
<Steps>
<Step title="Extract the archive">
Extract the archive, and work from its `compose` directory. In the command, replace `<archive>` with your archive’s name, without `.tar.gz`.
```bash
tar -xzf <archive>.tar.gz && cd <archive>/compose
```
If your umask is stricter than `022`, the binaries lose their execute permission for other users, and the server container can’t start them. In that case, set it to `022` and extract the archive again.
</Step>
<Step title="Create the .env file">
Compose reads the stack’s secrets from a `.env` file beside `docker-compose.yml`. Create the file:
```bash
cat > .env <<EOF
CMA_PG_PASSWORD=$(openssl rand -hex 32)
ANTHROPIC_API_KEY=sk-ant-...
CMA_SERVER__API_KEY=$(openssl rand -hex 32)
CMA_STORE__CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -base64 32)
EOF
chmod 600 .env
```
Open `.env` in an editor and replace `sk-ant-...` with your Claude API key. The block generates the rest: the database password, the server API key that your applications send, and the key that encrypts the server’s stored secrets.
</Step>
<Step title="Start the containers">
Build and start the stack, and then follow the server’s log:
```bash
docker compose up --build -d
docker compose logs -f cma-server
```
When the log shows a line with `msg="cma-server listening"`, press Ctrl+C to stop following it.
</Step>
<Step title="Check that the server is ready">
```bash
curl -s http://127.0.0.1:8400/cma-server/readyz
```
A ready server answers with `"status":"ready"`. Right after start, it can answer `"status":"degraded"` for a few seconds while its first checks run. If it stays `degraded` or answers `"status":"not_ready"`, see [Troubleshooting](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart#troubleshooting).
</Step>
</Steps>
## Run your first session
Create an agent and chat with it from your terminal with the `ant` CLI.
<Steps>
<Step title="Point the CLI at your server">
Open a second terminal in the same directory, and keep it for `ant`. The following commands set `ANTHROPIC_API_KEY` to your server API key, not your Claude API key, as [Call the API on a self-hosted server](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api) explains:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8400
export ANTHROPIC_API_KEY=$(grep '^CMA_SERVER__API_KEY=' .env | cut -d= -f2)
```
Keep `docker compose` commands in the first terminal. If you run `docker compose up` in this terminal, Compose passes these variables to the server, and the server calls itself instead of the Claude API.
</Step>
<Step title="Create an agent">
Save the file that follows the command as `quickstart-agent.md`, and then run the command. The file gives the agent the built-in tools and lets it run them without asking you first.
<CodeGroup>
<CodeGroupItem>
```bash CLI
ant apply --yes quickstart-agent.md
```
<File filename="quickstart-agent.md">
```markdown
---
name: Quickstart agent
model: claude-opus-5-5
tools:
- type: agent_toolset_20260401
---
You are a helpful assistant.
```
</File>
</CodeGroupItem>
</CodeGroup>
The command first prints a warning that it `could not determine the organization and workspace`, with a `404` error, and then creates the agent. That’s expected on your own server.
</Step>
<Step title="Start a session">
These commands find the agent and the stack’s environment, start a session, and keep the session’s ID in `SESSION_ID`. The stack names its environment `compose-sandbox`.
```bash
AGENT_ID=$(ant beta:agents list --format raw \
--transform 'data.#(name=="Quickstart agent").id' --raw-output)
ENVIRONMENT_ID=$(ant beta:environments list --format raw \
--transform 'data.#(name=="compose-sandbox").id' --raw-output)
SESSION_ID=$(ant beta:sessions create --agent "$AGENT_ID" \
--environment-id "$ENVIRONMENT_ID" --transform id --raw-output)
```
</Step>
<Step title="Ask the agent to run a command">
Connect your terminal to the session:
```bash
ant beta:sessions connect "$SESSION_ID"
```
The command opens a [chat view](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/sessions-connect) of the session. Send this message:
```text wrap
Run `hostname` with the bash tool and tell me the output.
```
The agent replies with a hostname that starts with `cma-sandbox-`. That’s the name of the sandbox container that ran the command, which confirms that the tool ran in a sandbox and not on your machine.
</Step>
<Step title="Find the sandbox container">
Leave the chat open. In the first terminal, list the sandbox containers:
```bash
docker ps --filter label=cma-server.sandbox
```
The list shows the container that the agent reported. The container exits about a minute after a turn ends, and the session’s next message starts a new container. If the list is empty, ask the agent to run `hostname` again, and list the containers again while it replies.
To leave the chat, press Ctrl+C in its terminal. The session keeps running.
</Step>
</Steps>
## Try the console on your server
This section is optional. The console needs single sign-on. To try it, restart the stack with the release’s mock sign-in provider. Port 8080 on your machine must be free.
<Warning>
Use the mock provider with throwaway data only. It gives an administrator’s token to anyone who reaches it. On Docker Desktop, that includes the sandbox containers. Under some Docker Engine releases before 28.3.3, it includes machines on your network.
</Warning>
<Steps>
<Step title="Restart the stack with the mock sign-in provider">
Run these commands in the first terminal:
```bash
export COMPOSE_FILE=docker-compose.yml:docker-compose.oidc.yml
docker compose up --build -d
```
The `export` line holds for the first terminal only, so run it again in any new terminal that you open for `docker compose`. To change the server’s settings in this stack, edit `compose.config.oidc.yaml`, not `compose.config.yaml`, and then run `docker compose restart cma-server`.
</Step>
<Step title="Open the session">
In a browser on the same machine, open `http://127.0.0.1:8400/`, click **Continue with SSO**, and select **alice**, the demo administrator. In the sidebar, select **Sessions**, and then select the session.
The page updates as the agent works, and you can send the agent a message from it.
</Step>
</Steps>
To create agents and start sessions in the console too, see [Run an agent from the console](https://platform.claude.com/docs/en/managed-agents/self-hosted/console#run-an-agent-from-the-console).
## Stop the stack
In the first terminal, run this command:
```bash
docker compose down --remove-orphans
```
The command deletes the containers and keeps the database. To also delete the database, with every agent, session, and session history in it, run `docker compose down --remove-orphans -v`.
## Troubleshooting
* **Your archive has no `compose` directory:** The archive holds an earlier release, and this page’s commands don’t match it. Ask your Anthropic account team for the current release. To move a running deployment to the current release, see [Upgrade self-hosted Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade).
* **The server never comes up:** Run `docker compose logs migrate`. The one-time `migrate` container creates the database tables. It must exit with status `0` before the server starts.
* **The readiness check stays `degraded`, or every turn fails:** If you ran `docker compose up` in a terminal where `ANTHROPIC_BASE_URL` is `http://127.0.0.1:8400`, the server calls itself instead of the Claude API. Run `docker compose up -d` again in the first terminal, where the variable isn’t set. Otherwise, the server log shows what failed. To read it, run `docker compose logs -f cma-server` again.
* **The readiness check answers `not_ready`:** In the answer, find the check with the state `failed`:
* **`upstream`:** The Claude API rejected your Claude API key. Correct `ANTHROPIC_API_KEY` in `.env`, then run `docker compose up -d` again in the first terminal. If your shell also sets `ANTHROPIC_API_KEY`, Compose uses the shell’s value, so run `unset ANTHROPIC_API_KEY` first.
* **`database`:** Either the database rejected the server’s login, for example because of a wrong password, or an open connection to the database broke. To see which, run `docker compose logs -f cma-server` again.
* **The agent replies but its tool call never finishes:** No worker is polling the environment. Check the workers’ log with `docker compose logs sandbox`. A worker that can’t reach the Docker daemon’s socket exits with a message that names `CMA_DOCKER_SOCKET`. Set that variable in `.env` to your daemon’s socket path, then run `docker compose up -d` again in the first terminal.
* **The workers are running, but a tool call still never finishes:** The stack starts 10 workers, and each one runs one session’s container at a time. A session takes a free worker when you create it, and a session that never gets a message keeps its worker for up to an hour. With all 10 taken, any other session’s tool calls wait for a worker. If the `docker ps` command in the “Find the sandbox container” step lists 10 containers, archive the sessions that you don’t use:
1. In the first terminal, run `docker ps --filter label=cma-server.sandbox --format '{{.Label "cma-server.session"}}'`. It prints the ID of the session behind each container. The session that waits has no container yet, so it isn’t in the list.
2. In the second terminal, press Ctrl+C to leave the chat, and run `ant beta:sessions archive --session-id <session-id>` for each listed session that you don’t use.
3. Run `ant beta:sessions connect "$SESSION_ID"` again. The tool call finishes when a worker takes its session.
## Next steps
<CardGroup cols={2}>
<Card title="Call the API" icon="code-brackets" href="https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api">
Point the SDK and the `ant` CLI at your server.
</Card>
<Card title="Differences from the hosted service" icon="arrows-left-right" href="https://platform.claude.com/docs/en/managed-agents/self-hosted/differences">
Check which features behave differently or aren’t available.
</Card>
<Card title="Console on your server" icon="browser" href="https://platform.claude.com/docs/en/managed-agents/self-hosted/console">
Set up sign-in, and create agents and run sessions in a browser.
</Card>
</CardGroup>
managed-agents/self-hosted/upgrade New page · 392 lines, new page
## Before you upgrade ## Upgrade on Kubernetes ## Upgrade on Docker Compose ## If the upgrade fails ## Roll back ### Deploy the old release again ### Restore the backup on Kubernetes #### If the key check fails ### Restore the backup on Docker Compose ### After a restore ### Test a backup ## Settings and paths to check ### Kubernetes: the names of your objects ### Docker Compose: the archive’s layout and paths
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Upgrade self-hosted Claude Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade
description: Upgrade a self-hosted Claude Managed Agents deployment on Kubernetes or Docker Compose, and roll back if the new release doesn't work.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
This page shows how to upgrade a deployment on Kubernetes or Docker Compose, and how to roll back. On Kubernetes, the downtime depends on how far the upgrade goes:
| Upgrade | Back up the database first | Downtime on Kubernetes |
| ------------------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------------------------- |
| Patch: only the third number changes | No. The database schema stays the same. | None |
| Minor: the second number goes up by one, such as `1.2.0` to `1.3.0` | Yes. The schema changes. | None, unless a `CHANGELOG.md` section says that the servers must stop |
| Skips a minor release, such as `1.1.0` to `1.3.0`, or changes the first number | Yes. The schema changes. | From when you stop the old servers until the new ones are ready |
A Docker Compose stack is down while it restarts, for every kind of upgrade.
## Before you upgrade
Read the new release’s section of `CHANGELOG.md`, at the top of the extracted archive. It starts with the upgrade steps that are specific to that release. If you skip releases, follow every section newer than your version, oldest first.
Then go through [Settings and paths to check](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#settings-and-paths-to-check).
A minor or major upgrade includes a database backup. The dump holds every conversation in plain text, so the commands on this page encrypt it with the [age encryption tool](https://age-encryption.org). Create a key pair once, and keep the private key, `cma-backup.agekey`, away from the backups:
```bash
age-keygen -o cma-backup.agekey
age-keygen -y cma-backup.agekey > cma-backup.agepub
```
## Upgrade on Kubernetes
The `kubectl` commands on this page read the name of the server’s Deployment from a shell variable. Run these two lines in each terminal where you run the commands. The first line looks up the name, and the second prints it:
```bash
server=$(kubectl -n cma-server get deployment -l anthropic.com/role=server -o jsonpath='{.items[*].metadata.name}')
echo "$server"
```
If the second line prints `cma-server`, go on. If it prints another name, read [the names of your objects](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#object-names) before you upgrade. If it prints nothing, check that `kubectl` points at the right cluster, and run the two lines again.
<Steps>
<Step title="Build and push the new images">
Extract the new archive. Build and push its three images under the new version, as in [Build and push the images](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#build-and-push-the-images). If you run the [egress gateway](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#environment-variable-credentials), build and push its image too.
If you build a sandbox image of your own, rebuild and push it too, under the new version. Compare its Dockerfile with `kubernetes/sandbox/Dockerfile` in the new archive, and see [what an image of your own needs](https://platform.claude.com/docs/en/managed-agents/self-hosted/call-the-api#what-an-image-of-your-own-needs).
</Step>
<Step title="Set the new version in your values files">
If a values file sets `image.tag`, change it to the new version.
</Step>
<Step title="Stop every server, if the upgrade needs it">
Do this step only in these cases:
* A server that you run is more than one minor release behind the new release.
* The first number, the major version, goes up.
* A section of `CHANGELOG.md` newer than the release that you run says that the servers must stop.
From this step until the new servers are ready, clients can’t reach the deployment, so plan a maintenance window.
```bash
kubectl -n cma-server scale deployment/$server --replicas=0
kubectl -n cma-server wait --for=delete pod -l anthropic.com/role=server --timeout=90s
```
</Step>
<Step title="Back up the database">
Do this before a minor or major upgrade, or if you stopped the servers. If the old release can’t run on the changed schema, this backup is the only way back.
Use `pg_dump` from the same major version of PostgreSQL as your database server. For `<role>` and `<database>`, use the `user` and `dbname` values of `database.url` in `cma-server-values.yaml`. `pg_dump` prompts for the role’s password.
```bash
set -o pipefail
PGSSLMODE=require pg_dump -h <database-host> -U <role> -d <database> -Fc \
| age -r "$(cat cma-backup.agepub)" -o cma_server-<old-version>.dump.age
```
If either command prints an error, the file it leaves behind is not a backup. Fix the cause and run the command again before you go on.
If the servers are running, the backup holds only what clients wrote before the dump began.
</Step>
<Step title="Upgrade the server, then the sandboxes">
Run these commands from the new archive’s top directory, with the values files from your install:
```bash
helm upgrade cma-server kubernetes/server/chart --namespace cma-server -f cma-server-values.yaml --wait
helm upgrade cma kubernetes/chart --namespace cma -f cma-values.yaml --wait
```
If you run more than one environment, run the second command for each sandbox release, with that release’s name, namespace, and values file.
If you stopped the servers, the first command migrates the schema, restores the server count, and returns once the new servers are ready.
</Step>
</Steps>
With the servers running, Helm starts each new server before it stops an old one, so the number of ready servers never drops. In a minor upgrade, the first command changes the schema while the old servers keep serving, and then it replaces them. An open event stream on a server that is being replaced ends cleanly, so a client that follows a stream has to reconnect.
If a command fails, see [If the upgrade fails](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#if-the-upgrade-fails).
## Upgrade on Docker Compose
The steps are the same for every kind of upgrade. Run each `docker compose` command from a release’s Compose directory, which is the directory that holds `docker-compose.yml`. In the extracted archive, that directory is `compose`. Use a terminal where `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, and `ANTHROPIC_AUTH_TOKEN` aren’t set, because Compose passes a shell’s values of those variables to the server.
<Steps>
<Step title="Back up the database">
Do this before a minor or major upgrade, from the old release’s Compose directory:
```bash
set -o pipefail
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
| age -r "$(cat cma-backup.agepub)" -o cma_server-<old-version>.dump.age
```
In the command, leave `$POSTGRES_USER` and `$POSTGRES_DB` as they are. The text in single quotes runs inside the `postgres` container, which holds the names of its role and its database in those variables.
The stack keeps serving during the backup, so the dump lacks whatever clients write after the dump begins. If either command prints an error, the file it leaves behind is not a backup. Fix the cause and run the command again before you go on.
</Step>
<Step title="Extract the new archive">
Put the new archive in a directory where the new release can stay, such as the old release’s parent directory. The commands extract the archive there, and the new stack runs from the extracted files.
If your umask is stricter than `022`, run `umask 022` first. Then run this from the old release’s Compose directory. In place of `<directory>`, write the full path of the directory that holds the new archive. In place of `<archive>`, write the archive’s name without `.tar.gz`.
```bash
releases="<directory>"
tar -xzf "$releases/<archive>.tar.gz" -C "$releases" && cp .env compose.config.yaml "$releases/<archive>/compose/"
```
The second line also copies your `.env` and `compose.config.yaml` into the extracted `compose` directory. The old stack keeps running through both lines. If a line prints an error, fix the cause and run both lines again.
</Step>
<Step title="Repeat your other changes">
Only `.env` and `compose.config.yaml` carry over. Make your other changes again in the new release’s Compose directory:
* **A file that you added:** Copy it.
* **A shipped file that you edited:** Make the same edit in the new release’s copy. Don’t copy the old file over it, because that undoes the release’s changes to the file.
* **Agent identity:** Copy the `agent-identity` directory with `cp -a`, using `sudo` if root owns it, and edit the `volumes:` block of `docker-compose.yml` again.
* **A Compose file of your own:** Copy it, and check its relative paths.
Then make the Docker Compose changes in [Settings and paths to check](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#settings-and-paths-to-check).
</Step>
<Step title="Stop the old stack and start the new one">
Run these lines in the terminal where you extracted the archive, because they use its `$releases` variable. Stay in the old release’s Compose directory, and write the same name in place of `<archive>`. If you exported `COMPOSE_FILE` for the old stack, export the same value first.
```bash
ls "$releases/<archive>/compose/.env" && docker compose down
cd "$releases/<archive>/compose" && docker compose up --build -d
```
The first line stops the old stack only if the new release’s `.env` is in place. `docker compose down` keeps the database. Don’t add `-v`, which deletes it. The new stack picks up the database, and its `migrate` container updates the schema before the server starts.
</Step>
</Steps>
If a command fails, see [If the upgrade fails](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#if-the-upgrade-fails).
## If the upgrade fails
Before you roll back, find the cause in the log:
* **Kubernetes:** If the migration Job fails, `helm upgrade` waits a few minutes while the Job retries, and then it fails. During that wait, run `kubectl -n cma-server logs job/$server-migrate` in a second terminal, with `$server` set as in [Upgrade on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#upgrade-on-kubernetes). Kubernetes deletes the log when the Job stops retrying. If you missed it, run `helm upgrade` again, which starts the Job again.
* **Docker Compose:** The output of `docker compose up --build -d` might not show the reason. Run `docker compose logs` to find it.
| What the log shows | What it means | What to do |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A line that contains `refusing to migrate: servers of another release are still running against this database` | Some upgrades need every server stopped first, and a server of the old release is still running. The migration changed nothing. | Stop every server, as in [Upgrade on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#upgrade-on-kubernetes), and run `helm upgrade` again. If a server didn’t shut down cleanly, wait 20 seconds first: until then, the migration still counts that server as running. |
| A line that contains `cannot downgrade`, when you deploy the old release again | The old release can’t run on the changed schema. | Restore the backup, as in [Roll back](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#roll-back). |
| Another error from the migration | You don’t need to roll back. On Kubernetes, the servers stay as they were until the migration succeeds. | Fix the cause, and run `helm upgrade` or `docker compose up --build -d` again. |
The migration can succeed while the new servers don’t become ready. To find the cause, read the servers’ log:
* **Kubernetes:** `kubectl -n cma-server logs -l anthropic.com/role=server --prefix --tail=20`
* **Docker Compose:** `docker compose logs cma-server`
For example, on Kubernetes, a line that contains `has invalid keys` names a setting under `server.config` that the new release doesn’t accept. Fix the cause and run the upgrade command again before you consider a restore, because a restore loses data. On Kubernetes, once the schema has changed, a running server of the old release keeps serving, but it might not start again if it stops.
## Roll back
The `kubectl` commands in this section use the `$server` variable from [Upgrade on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#upgrade-on-kubernetes). Before you roll back, check [If the upgrade fails](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#if-the-upgrade-fails). Then roll back by the kind of upgrade:
* **After a patch upgrade:** [Deploy the old release again](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#deploy-the-old-release-again). Both releases use the same schema.
* **After a minor or major upgrade:** Open `CHANGELOG.md` in the new archive. Read the section of every release that came after the one that you’re going back to.
* If a section says that the old release can’t start on the migrated database, restore the backup.
* Otherwise, deploy the old release again. If that fails with `cannot downgrade`, restore the backup.
<Warning>
A restore loses everything that clients wrote after the backup began.
</Warning>
### Deploy the old release again
* **Kubernetes:** Go to the old archive’s top directory. Run the two `helm upgrade` commands from [Upgrade on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#upgrade-on-kubernetes), with the values files as they were for that release. For example, if you added `earlierNames` for the upgrade, take it out. Run the second command only when the first succeeds.
* **Docker Compose:** Run `docker compose down` in the new release’s Compose directory, and then `docker compose up --build -d` in the old one.
If the old release can’t run on the changed schema, the log shows `cannot downgrade`:
* **Kubernetes:** The migration Job fails, and the servers stay as they are. [If the upgrade fails](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#if-the-upgrade-fails) says how to read the Job’s log.
* **Docker Compose:** The server doesn’t start. Read the log with `docker compose logs migrate`.
On Docker Compose, the output of `docker compose logs` might instead show `stored value is encrypted but no encryption key is configured`. In that case, the old release’s `.env` has no `CMA_STORE__CREDENTIALS_ENCRYPTION_KEY` line. Copy that line from the new release’s `.env`, and run `docker compose up --build -d` again.
### Restore the backup on Kubernetes
Your database server needs room for a second copy of the database. For `<role>` and `<database>`, use the `user` and `dbname` values of `database.url` in `cma-server-values.yaml`.
<Steps>
<Step title="Restore the backup into a new database">
Connect to the `postgres` database on your database server as an administrator, and run:
```sql
CREATE DATABASE <database>_restored OWNER <role>;
```
Then, on the machine that holds the backup, run this with the paths of your key and backup file. Use a `pg_restore` of the same major version as your database server. It prompts for the role’s password, and it prints nothing if it works.
```bash
age -d -i cma-backup.agekey cma_server-<old-version>.dump.age \
| PGSSLMODE=require pg_restore -h <database-host> -U <role> -d <database>_restored --no-owner --exit-on-error
```
If this prints an error, stop. Your database hasn’t changed. Run `DROP DATABASE <database>_restored;` as an administrator, and start this step again.
</Step>
<Step title="Stop every server">
```bash
kubectl -n cma-server scale deployment/$server --replicas=0
kubectl -n cma-server wait --for=delete pod -l anthropic.com/role=server --timeout=90s
```
</Step>
<Step title="Switch to the restored database">
Connect to the `postgres` database as an administrator, and run:
```sql
ALTER DATABASE <database> RENAME TO <database>_upgraded;
ALTER DATABASE <database>_restored RENAME TO <database>;
```
If the first statement says that other sessions are using the database, close those sessions and run both statements again. A connection pooler can hold such sessions.
</Step>
<Step title="Deploy the old release">
Follow the Kubernetes item of [Deploy the old release again](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#deploy-the-old-release-again). The first command also restores the server count. If it fails but `kubectl -n cma-server get pods` shows that the old servers are ready, see [If the key check fails](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#if-the-key-check-fails).
</Step>
<Step title="Delete the database that you set aside">
That database still holds what clients wrote after the backup. When the old release works, delete it with `DROP DATABASE <database>_upgraded;`.
</Step>
</Steps>
#### If the key check fails
In the “Deploy the old release” step, the first `helm upgrade` command waits until the old servers are ready. Then it checks the key that the sandboxes connect with. If the command fails but `kubectl -n cma-server get pods` shows that the old servers are ready, that check failed.
Your data is restored and the servers work, but the sandboxes can’t connect to them yet. The sandboxes connect with a key from the `cma-environment` Secret in their own namespace, `cma`. That key is newer than your backup, so the restored database doesn’t recognize it.
To confirm the cause, run the first command again. While it waits, open a second terminal and set `$server` there too. Then run `kubectl -n cma-server logs job/$server-environment-default`, with your environment’s name in place of `default`. Look for a line that contains `Secret cma/cma-environment records key`. If the log shows another error, fix what it names.
If the log shows that line, delete the Secret with `kubectl -n cma delete secret cma-environment`, and run the first command again. It creates the Secret again, with a new key. Then run the second command.
### Restore the backup on Docker Compose
<Steps>
<Step title="Start only the database">
Run `docker compose down` in the new release’s Compose directory. Then run `docker compose up -d --wait postgres` in the old release’s Compose directory. Stay in that directory for the next steps.
</Step>
<Step title="Restore the backup into a new database">
From the old release’s Compose directory, run this with the paths of your key and backup file:
```bash
docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d postgres \
-c "DROP DATABASE IF EXISTS ${POSTGRES_DB}_restored" -c "CREATE DATABASE ${POSTGRES_DB}_restored"' \
&& age -d -i cma-backup.agekey cma_server-<old-version>.dump.age \
| docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "${POSTGRES_DB}_restored" --no-owner --exit-on-error'
```
It worked if it prints `DROP DATABASE` and `CREATE DATABASE`, and no error.
Leave `$POSTGRES_USER` and `$POSTGRES_DB` as they are, here and in the next steps. The text in single quotes runs inside the `postgres` container, which holds the names of its role and its database in those variables.
If it prints an error, your database hasn’t changed, so you can run the whole command again. If you can’t restore the backup, run `docker compose up --build -d` in the new release’s Compose directory to start that release again.
</Step>
<Step title="Switch to the restored database">
```bash
docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d postgres \
-c "ALTER DATABASE $POSTGRES_DB RENAME TO ${POSTGRES_DB}_upgraded" \
-c "ALTER DATABASE ${POSTGRES_DB}_restored RENAME TO $POSTGRES_DB"'
```
</Step>
<Step title="Start the old release">
Cut at 300 lines. The page has the rest.