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.
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 — 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:
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 wants it.
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 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
sandboxenvironment that composes 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 | Sandbox.create({ envs }), or per-command | seekrit-proxy as a sidecar, or on the host |
| Modal | Secret.from_dict() at deploy or sandbox create | seekrit-proxy in the image |
| Daytona | envVars at create, updateEnv after | seekrit-proxy on the host |
| Vercel Sandbox | env at create, or per runCommand | networkPolicy: 'deny-all' plus a proxy |
| 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 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 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 — 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— the same injection for a process on your own machine- Agent proxy — the credential the workload never holds
- Agent access policy — bounding which hosts and operations an agent may reach
- Temporary access — a credential that expires on its own
- SDKs —
resolve()in Python, Go, JavaScript and Ruby