# Agent sandboxes

A sandbox is a container or microVM you start from your own code to run
something you do not fully trust — model output, a coding agent, a user's
snippet. That makes it a different secrets problem from a deploy target, and a
better one: **your process is on the outside**, holding the token, deciding what
goes in.

Every provider here supports the same two shapes. They are not variants of each
other — they answer different questions.

| | Inject at boot | Keep the credential outside |
| --- | --- | --- |
| **What the sandbox gets** | The real values, as environment variables | Placeholders, and an endpoint |
| **Code you trust inside** | Your own, or an agent you supervise | Anything, including hostile output |
| **If the sandbox is compromised** | The key is gone — rotate it | Nothing to steal; the key was never there |
| **Effort** | Three lines | A proxy or an outbound handler |
| **Reach** | Everything the value is good for | Only the hosts and operations you allowed |

Reach for **inject at boot** when the sandbox exists for isolation from *your*
machine — a build, a test run, a notebook — and the code inside is code you
would have run anyway. Reach for **keep the credential outside** when the whole
point of the sandbox is that you do not trust what runs in it. An agent that can
read `os.environ` can exfiltrate a key it finds there, and "it only pipes it to
the model" stops being true the moment the model writes the code.

> **Note:** Cloudflare's own Sandbox documentation is blunt about this: *"Do not put live API keys or other long-lived credentials into the sandbox."* That is the second column, and it is the same pattern seekrit ships as [`seekrit-proxy`](/docs/guides/agent-proxy) — a credential the workload names but never holds.

## The shape that is the same everywhere

Whichever provider you use, injecting at boot is the same three steps, and
**none of them run inside the sandbox**:

```python
import seekrit, os

# 1. Resolve and decrypt on the host — your process, your token.
secrets = seekrit.Client(token=os.environ["SEEKRIT_TOKEN"]).resolve()

# 2. Hand the sandbox exactly what it needs, not the whole environment.
envs = {k: secrets[k] for k in ("OPENAI_API_KEY", "TAVILY_API_KEY")}

# 3. Start it.
sandbox = create_sandbox(envs=envs)   # provider-specific; see the pages below
```

That ordering is the point. The service token never enters the sandbox, so code
inside cannot re-resolve the environment, ask for a different one, or reach
seekrit at all — it gets the values you chose and nothing else. Decryption
happens in your process, which is where seekrit's
[zero-knowledge model](/docs/concepts/encryption) wants it.

> **Warning:** **Never pass `SEEKRIT_TOKEN` into a sandbox** running code you do not trust. A service token resolves a whole environment, so handing it over turns "the agent has two API keys" into "the agent has every secret in this environment, and can fetch them again after you rotate the two." If the sandbox genuinely needs to resolve for itself, give it its own token bound to its own environment — or better, a [temporary-access lease](/docs/concepts/temporary-access) that expires.

### Pick names, not the whole environment

`resolve()` returns everything the token can see. Injecting all of it is the
easy mistake: a sandbox that needed one model key ends up holding the database
password too, because both live in `production`.

Two ways to narrow it, and the second is better:

- **In your code**, as above — a dict comprehension over the names you meant.
- **In seekrit**, with an environment scoped to this job. A
  `sandbox` environment that [composes](/docs/guides/environments) only the
  group holding model keys can't leak a database password, because it never had
  one. The narrowing then survives someone editing the injection code.

## Per provider

| Provider | Inject at boot | Keep the credential outside |
| --- | --- | --- |
| [**E2B**](/docs/guides/sandboxes/e2b) | `Sandbox.create({ envs })`, or per-command | `seekrit-proxy` as a sidecar, or on the host |
| [**Modal**](/docs/guides/sandboxes/modal) | `Secret.from_dict()` at deploy or sandbox create | `seekrit-proxy` in the image |
| [**Daytona**](/docs/guides/sandboxes/daytona) | `envVars` at create, `updateEnv` after | `seekrit-proxy` on the host |
| [**Vercel Sandbox**](/docs/guides/sandboxes/vercel-sandbox) | `env` at create, or per `runCommand` | `networkPolicy: 'deny-all'` plus a proxy |
| [**Cloudflare Sandbox**](/docs/guides/sandboxes/cloudflare-sandbox) | `setEnvVars()` or `exec({ env })` | **Outbound handlers** — no proxy needed |

Cloudflare is the one that does the second column natively: an outbound handler
runs in the Worker, outside the sandbox, and attaches the credential on the way
past. Everywhere else the same job is `seekrit-proxy`.

## What about a hosted agent runtime?

If the platform runs the agent *for* you — you never start a process — there is
no host to resolve on and none of this applies. That is a
[third-party sync](/docs/guides/third-party-sync) problem instead:
seekrit decrypts on its own servers and pushes the values to the platform, which
is a real trade-off and the one case seekrit's zero-knowledge rule is explicitly
carved out for. [LangGraph
Platform](/docs/guides/third-party-sync/langgraph-platform) is the agent-hosting
destination.

The line is simply whether you control a process at startup. A sandbox you
create from your own code: you do — use this page. A managed Agent Server: you
don't — sync to it.

## See also

- [AI frameworks](/docs/guides/frameworks) — the same shapes in LangGraph's, Mastra's, or Pydantic AI's idiom, for the agent you are running *inside* the sandbox, starting from a three-command setup
- [`seekrit run`](/docs/guides/run) — the same injection for a process on your own machine
- [Agent proxy](/docs/guides/agent-proxy) — the credential the workload never holds
- [Agent access policy](/docs/guides/agent-proxy/policy) — bounding *which* hosts and operations an agent may reach
- [Temporary access](/docs/concepts/temporary-access) — a credential that expires on its own
- [SDKs](/docs/guides/sdks) — `resolve()` in Python, Go, JavaScript and Ruby
