# Sync to Claude Managed Agents

A Claude Managed Agents session runs in a sandbox Anthropic hosts, so there is
no process of yours to inject into. What Anthropic offers instead is a **vault**:
you store a credential once, attach the vault to a session, and the agent's
sandbox sees only an **opaque placeholder** in the environment variable.
Anthropic swaps the real value in as a request leaves the sandbox — and only on
requests to the hosts you named. A binding owns the environment-variable
credentials seekrit writes into one vault.

| At a glance | |
| --- | --- |
| **What seekrit writes** | `environment_variable` credentials in one vault |
| **Addressed by** | The vault ID, `vlt_…` |
| **Connection carries** | Nothing — an API key plus the vault ID is the whole address |
| **Key permission** | An Anthropic API key from the **vault's own workspace** |
| **Takes effect** | New sessions that attach the vault; running sessions pick up a rotation without restarting |
| **Value visibility** | Write-only — and never visible inside the agent's sandbox, only at egress |

New to sync? Read [Third-party sync](/docs/guides/third-party-sync) first — the
decryption grant, name mapping, deletions, and failure handling are the same on
every destination.

> **Note:** **The workload never holds the value either.** Like every sync destination, seekrit's servers decrypt the environment to push it. Unlike most — [OpenAI Vault](/docs/guides/third-party-sync/openai-vault) is the other — the thing that *uses* the value never receives it: code the agent runs — including code a prompt injection writes — can read the variable and gets a placeholder. That is why a binding here must say **where each value may be used**. The host list is the boundary, and seekrit writes it on every push.

## 1. Create an API key

In the Claude Console, open **Settings → API keys** and create a key in the
**workspace that owns the vault**. Vaults are workspace-scoped: a key from
another workspace gets a `404` for a vault that exists.

```bash
printf '%s' "$ANTHROPIC_API_KEY" \
  | seekrit sync connect --name acme-claude --provider claude-managed-agents
```

That is the whole connection. There is nothing to scope: the key belongs to one
workspace, and a vault ID is globally unique.

> **Note:** **Claude Platform on AWS is not covered yet.** It runs Managed Agents too, but authenticates with AWS SigV4 rather than an API key, so it needs its own connection type. Bedrock, Vertex AI, and Foundry do not offer Managed Agents.

## 2. Bind an environment

Create the vault first if you do not have one — in the Console, or with
`ant beta:vaults create`. Then say which hosts each secret may be used on:

```bash
seekrit sync enable --connection acme-claude --provider claude-managed-agents \
  --vault vlt_01ABCdef \
  --allowed-hosts-for STRIPE_SECRET_KEY=api.stripe.com \
  --allowed-hosts-for OPENAI_API_KEY=api.openai.com \
  --allowed-hosts-for GITHUB_TOKEN=api.github.com,uploads.github.com \
  --app support-agent --env production --acknowledge-decryption
```

`--vault` is the `vlt_…` ID, not the vault's display name. `--app` is the seekrit
application the environment belongs to. `seekrit sync verify` takes the same
flags and checks that the key can read the vault and that it is not archived.

## 3. Attach the vault to sessions

A vault does nothing until a session names it. Pass `vault_ids` when you create
the session:

```bash
curl https://api.anthropic.com/v1/sessions \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: managed-agents-2026-04-01" \
  -H "content-type: application/json" \
  -d '{"agent": "agent_…", "environment_id": "env_…", "vault_ids": ["vlt_01ABCdef"]}'
```

Inside the session, the agent reads `$STRIPE_SECRET_KEY` like any environment
variable and sends it as usual. `vault_ids` can only be set when a session is
created, not added to a running one.

## Where each value may be used

Every name a binding pushes needs a host rule, and there are two places to put
one:

| Flag | Dashboard field | Applies to |
| --- | --- | --- |
| `--allowed-hosts-for NAME=hosts` (repeatable) | **Hosts per secret**, one `NAME = hosts` per line | That one name — by its name **in the vault**, after any prefix or rename |
| `--allowed-hosts hosts` | **Allowed hosts** | Every name without its own rule |

**A name covered by neither is not pushed.** It fails with a reason that names
it, rather than going out unrestricted — so a secret someone adds to the
environment next month cannot quietly widen where values flow. If seekrit had
already pushed that name under a rule that no longer covers it, the credential
it wrote is **removed** from the vault rather than left live under the old rule;
the agent loses that key until you add a rule.

Per-name rules are the better tool: a Stripe key belongs on `api.stripe.com` and
nowhere else, and a shared list lets every key in it be substituted on every
host in it.

A host is a bare hostname (`api.stripe.com`), a wildcard (`*.stripe.com`), or an
IPv4 address — up to 16 per rule. URLs, ports, paths, and IPv6 addresses are
refused. A wildcard matches every subdomain **but not the domain itself**, so a
service reached at its apex needs both `stripe.com` and `*.stripe.com`.

The literal `unrestricted` lets a value be substituted on any host the agent's
environment allows. It exists for tools that reach hosts you cannot list in
advance; it is never what an empty field means.

> **Warning:** **The environment must allow the host too.** A credential's host list decides which requests *get the value*, not which requests are *allowed out*. The Managed Agents environment the session runs in has its own network policy, and a host missing from it blocks the request whatever the credential says. seekrit cannot see that policy — check it when a substituted request fails.

### Headers, body, or both

`--injection-location` (the dashboard's **Substitute into**) picks which parts of
an outbound request the placeholder is swapped in:

| Value | Substituted in | Use for |
| --- | --- | --- |
| `header` *(default)* | Request headers | Almost every API key — `Authorization`, `x-api-key` |
| `body` | Request body | A client that sends the secret in a form or JSON body |
| `both` | Headers and body | Both of the above in one binding |

`header` is the default because request bodies are often built from content the
agent is working with, which makes the body the broader surface. A placeholder in
a location that is off is sent as-is, and the service rejects it. The setting
applies to the whole binding; to give one secret body injection, bind it
separately with `--include`.

## How a push behaves

- **seekrit lists the vault, then writes.** The vault API keeps creating and
  updating apart, so each run reads the vault's credential names once, updates
  the ones that exist, and creates the rest. Values are never read — the API
  does not return them.
- **Every run writes every name.** With no way to compare values, seekrit sends
  each one. That is cheap here: nothing restarts, and running sessions pick up
  the new value on their own.
- **Host rules are rewritten every push.** A host list or injection setting
  edited in the Console reverts to the binding's on the next run — the binding
  is the source of truth for where a value may go, as it is for the value.
  Editing a binding's rules keeps its history, so names it wrote are still
  removed later; moving it to another vault starts that history afresh.
- **The vault can be shared.** seekrit touches only environment-variable
  credentials whose names this binding pushes or pushed. MCP credentials beside
  them are never read or changed. An environment-variable credential that
  already has a name the binding pushes is taken over, as on every destination.
- **Removals run first**, so renaming a secret in a full vault frees the old slot
  before the new name needs it.
- **Credentials are marked** with `managed-by: seekrit` metadata, so the Console
  says who owns them; ones seekrit creates are also named `NAME · managed by
  seekrit`, while a credential it takes over keeps its display name. Nothing
  derived from a value is ever written there. The marker is also how seekrit
  tells its own credentials from yours: it only ever removes a name's
  credential for a lost host rule when the marker is on it.
- **A value Anthropic echoes back in an error is scrubbed** before the reason is
  stored on the run row.

> **Warning:** **A vault holds at most 20 credentials**, MCP credentials included. A create refused at the limit says so in the run ledger. Split a larger environment across two vaults — two bindings with `--include`/`--exclude` — and attach both to the session.

## Name and client rules

Substitution happens as a request leaves the sandbox, never inside it. That has
three consequences:

- **Variables the sandbox reads itself are refused.** `PATH`, `HOME`, `PWD`,
  `SHELL`, `USER`, `TMPDIR`, `LD_PRELOAD`, `LD_LIBRARY_PATH`, the proxy variables
  (`HTTPS_PROXY` and friends, either case), and the CA-bundle variables
  (`SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, …) are never sent to a host, so a
  placeholder there could only break the sandbox. seekrit reports the refusal
  against that name alone.
- **Clients that inspect the secret locally see the placeholder.** A CLI that
  checks a key's format at startup may reject it, and anything that *signs* with
  the secret — AWS SigV4 is the common case — produces a bad signature. Vault
  credentials work for clients that send the value verbatim.
- **An exchange hands back a real token.** If a client uses the stored secret to
  fetch a session token (an OAuth client-credentials grant, say), the returned
  token reaches the sandbox unredacted. Do the exchange elsewhere and sync the
  resulting token instead.

## Self-hosted sandboxes

Anthropic does not support environment-variable vault credentials in
**self-hosted** Managed Agents environments — substitution needs Anthropic's own
egress. On a self-hosted worker, keep decryption on your side instead: run the
agent under [`seekrit run`](/docs/guides/run), or put
[`seekrit-proxy`](/docs/guides/agent-proxy) in front of it so it holds
placeholders the proxy fills in. The
[Sprites guide](/docs/guides/sandboxes/sprites) walks through one such setup.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| "rejected the API key" | The key was revoked, or pasted wrong | Create a new one under Settings → API keys and re-create the connection |
| "no vault … visible to this API key" (`404`) | The display name in the ID slot, a deleted vault, or a key from another workspace | Use the `vlt_…` ID, and create the key in the vault's workspace |
| `403` on the vault | The organization does not have Managed Agents access | Check access in the Console |
| "was archived" | Archiving is permanent and purges the vault's credentials | Create a new vault and edit the binding's `--vault` |
| One name failed with "no allowed hosts" | The name has no per-name rule and there is no default. If seekrit had pushed it before, its credential was removed | Add `--allowed-hosts-for NAME=…`, set `--allowed-hosts`, or `--exclude` the name |
| One name failed, "read inside the sandbox" | The mapped name is one the sandbox uses itself | Rename it, or fix the prefix |
| A create failed with "holds 20 credentials" | The vault is full | Split the environment across two vaults |
| The agent's request is rejected with the placeholder visible | The host is not in the credential's rule, or the injection location is off for where the client sends it | Add the host, or switch to `body`/`both` |
| The agent's request never leaves the sandbox | The environment's network policy blocks the host | Allow it on the environment |

## See also

- [Third-party sync](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [Agent proxy](/docs/guides/agent-proxy) — the same placeholder model, on infrastructure you run
- [Claude Agent SDK](/docs/guides/frameworks/claude-agent-sdk) — agents you host yourself
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
