# Secrets in Cloudflare Sandbox

Cloudflare Sandbox is the one provider on this list where the
credential-never-enters-the-sandbox shape needs no proxy at all. **Outbound
handlers** run in the Workers runtime, outside the container, and can attach a
credential to a request on its way past — which is exactly what
[`seekrit-proxy`](/docs/guides/agent-proxy) does elsewhere, built into the
platform.

Cloudflare's own documentation recommends it over injection, in as many words:

> Use environment variables for **non-secret** configuration (paths, feature
> flags, `NODE_ENV`, and similar). Do not put live API keys or other long-lived
> credentials into the sandbox.

So this page leads with that shape, and covers injection second.

## Keep the credential in the Worker

Resolve in the Worker, hold the values there, and attach them in an outbound
handler. The container never sees a key:

```ts
import { getSandbox, Sandbox } from '@cloudflare/sandbox';
import { Seekrit } from '@seekrit/sdk';

export class AgentSandbox extends Sandbox {}

AgentSandbox.outboundByHost = {
  'api.openai.com': async (request: Request, env: Env) => {
    const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
    const authed = new Request(request);
    authed.headers.set('authorization', `Bearer ${secrets.OPENAI_API_KEY}`);
    return fetch(authed);
  },
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sandbox = getSandbox(env.AgentSandbox, 'user-123');

    // No credential passed in — the agent calls api.openai.com with no key and
    // the handler supplies one on the way out.
    const proc = await sandbox.exec('python agent.py');
    return Response.json({ output: await proc.output({ encoding: 'utf8' }) });
  },
} satisfies ExportedHandler<Env>;
```

What this buys, and it is the whole argument for the pattern:

- **Nothing to steal.** The container holds no key and no seekrit token, so
  prompt injection, a leaked log, or arbitrary code execution inside the sandbox
  yields nothing.
- **Rotation lands immediately.** The next request picks up whatever the Worker
  resolves; there is no sandbox to restart and no value to re-inject.
- **The host is the boundary.** A handler is registered per host, so a
  credential can only ever be attached to a request going to that host. An agent
  that POSTs your key's worth of traffic at `evil.example` gets no key.

> **Tip:** Cache the resolve rather than doing it per request. A Worker isolate can hold the resolved map in module scope, or you can keep it on the Durable Object — `/v1/resolve` is metered, and a busy agent makes a lot of outbound calls. Do **not** cache it into the container.

Per-instance credentials work the same way, using `ctx.containerId` to pick which
secret an instance gets — useful when each tenant's agent should carry its own
key:

```ts
AgentSandbox.outboundByHost = {
  'api.example.com': async (request, env, ctx) => {
    const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
    const authed = new Request(request);
    authed.headers.set('x-api-key', secrets[`TENANT_${ctx.containerId}_KEY`]);
    return fetch(authed);
  },
};
```

## Inject into the container

When the code inside is yours — a build step, a test run — injection is fine and
simpler. Resolve in the Worker and set the variables before anything runs:

```ts
const sandbox = getSandbox(env.Sandbox, 'build-42');
const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();

await sandbox.setEnvVars({
  DATABASE_URL: secrets.DATABASE_URL,
  NODE_ENV: 'production',
});

await sandbox.exec('npm run migrate');
```

Or per command, which keeps a value out of the steps that do not need it:

```ts
await sandbox.exec('node seed.js', {
  env: { DATABASE_URL: secrets.DATABASE_URL },
});

await sandbox.exec('node report.js'); // no credential
```

> **Note:** Call `setEnvVars()` **before** other sandbox operations — the SDK merges stored names in at each `exec()` launch, so a command that started earlier does not get them. The stored names live in the sandbox Durable Object's memory, not on the container filesystem, so after the DO is evicted you must set them again (or pass `env` per launch). Named keys, not the whole resolved map — see [Pick names, not the whole environment](/docs/guides/sandboxes#pick-names-not-the-whole-environment).

## Which one, concretely

| The sandbox runs | Use |
| --- | --- |
| Your build, migration, or test suite | `setEnvVars()` / `exec({ env })` |
| A coding agent, or model-generated code | An outbound handler |
| Untrusted user submissions | An outbound handler, and `unset` anything you injected earlier |

A useful tell: if you would be unhappy to find the value printed in the
sandbox's stdout, it belongs in the Worker.

## See also

- [Agent sandboxes](/docs/guides/sandboxes) — the two shapes and when each is right
- [Agent proxy](/docs/guides/agent-proxy) — the same pattern for every other runtime
- [Sync to Cloudflare](/docs/guides/third-party-sync/cloudflare) — for Workers, Pages, and Secrets Store, which are the other problem
