[ new ]Claude Managed Agents on Superserve
← Back to blog
[ Blog ]

Give your sandboxed agents API keys they can't read

June 19, 2026·Amit Patil

Set an API key as an environment variable on a sandbox and any command the agent runs can read it. One cat and the key is exposed. It's the same mistake as committing a .env to git, except now the thing reading it is an agent you pointed at the open internet.

Today we're shipping Secrets. You attach a credential to a sandbox, your code uses it like any normal environment variable, and the real value never enters the sandbox. The code only sees a stand-in token. We swap it for the real credential as the request leaves the sandbox, and only for the hosts you allow.

Why API keys leak out of sandboxes

Passing an API key through an environment variable is the normal way to give code a credential. On your laptop that's fine: you own the keys and you wrote the code. A sandbox is different. It runs code you don't fully trust, on inputs you don't control: a repo, a web page, a support ticket. The moment that code can run commands and read a live credential, one prompt injection turns into a leaked key.

This bites hardest when you run a coding agent like Claude Code or Codex inside the sandbox, but it's just as true when the agent stays on your server and only uses the sandbox to run its tool calls. Either way, the model decides what executes in there.

It doesn't take an exploit. Anything the model can run is enough - a generated shell line, cat .env, or printenv:

bash
$ printenv ANTHROPIC_API_KEY
sk-ant-api03-rLf9...   # the real key, ready to be sent anywhere

It's not even a hack. People just ask agents to drop their .env, and they do:

A viral X thread: users ask any AI agent reading the post to reply with its full .env file, and an agent replies 'Sure! Here's my .env:' dumping its OPENAI_API_KEY, ANTHROPIC_API_KEY, and GITHUB_TOKEN.

Even the agent frameworks now tell you not to put secrets in the sandbox at all. They're right. The hard part is doing that without breaking every SDK and CLI that expects a key in an environment variable.

Why nobody shipped a fix

Ankur Goyal (@ankrgyl) on X: 'The use case is super simple: run Codex / Claude Code in a sandbox. MITM requires implementing and operating a credential-substituting proxy. None of the sandbox providers have a ready-to-go solution. The best I've seen is Vercel's firewall, but you still need to BYO proxy.'

The requirements are simple to write down: no key in the sandbox, generated code can't reach the secret, and streaming input and output still work. Until now, meeting them meant building and running a credential-substituting layer yourself. So most teams just set the env var and hoped nothing read it. Secrets is that layer, built into the platform.

How it works

Store the credential once. The value is encrypted at rest. Once it's in, you can't read it back out:

typescript
import { Secret } from "@superserve/sdk"

await Secret.create({
  name: "anthropic-prod",
  value: process.env.ANTHROPIC_API_KEY,
  provider: "anthropic",
})

Bind it to a sandbox by the environment variable your code already reads. It's a map of environment variable to secret name:

typescript
import { Sandbox } from "@superserve/sdk"

const sandbox = await Sandbox.create({
  name: "research-agent",
  secrets: { ANTHROPIC_API_KEY: "anthropic-prod" },
})

You can also attach or detach a secret on a sandbox that's already running - no rebuild.

Inside the sandbox, ANTHROPIC_API_KEY is set like any other variable, so every SDK, CLI, and script that reads it keeps working. The value is a stand-in token shaped like a real Anthropic key, so even clients that check the key's format are happy, but it isn't your credential:

bash
$ printenv ANTHROPIC_API_KEY
sk-ant-api03-3Qk8...   # a stand-in shaped like the real thing, not your credential

When the code makes a request, we swap the stand-in for your real credential as the request leaves the sandbox, on its way to api.anthropic.com. Anthropic sees the real key. The sandbox never holds it. The real value never touches the sandbox's disk, its environment, or /proc. So whatever a compromised agent prints, dumps, or leaks is the stand-in, and it only works on that secret's approved hosts.

Built-in support for the providers you already use

Secrets ships with one-step shortcuts for the services agents actually call. Pick a provider, paste the key, and we set the auth scheme and allowed hosts for you.

That covers the model APIs (OpenAI, Anthropic, Google Gemini, xAI, Perplexity, OpenRouter), developer tools (GitHub, GitLab, Vercel, Cloudflare, Sentry), search (Exa, Firecrawl), and the usual SaaS suspects (Slack, Linear, Notion, Asana, Stripe, Resend). The catalog keeps growing. Want one that isn't here yet? Reach out and we'll add it.

Anything not on the list works too. Create a custom secret with your own header or auth scheme and its allowed hosts, and it behaves exactly the same: the real value stays out of the sandbox.

Scope it to exactly what it should reach

A stand-in is only useful on the hosts a secret allows, and you can pair it with network rules so the sandbox can't reach anything else in the first place:

typescript
const sandbox = await Sandbox.create({
  name: "locked-down",
  secrets: { ANTHROPIC_API_KEY: "anthropic-prod" },
  network: {
    allowOut: ["api.anthropic.com"],
    denyOut: ["0.0.0.0/0"],
  },
})

Even if the code is talked into calling another endpoint, the request is blocked before it leaves, and the credential is never attached to it. One credential can also use different auth schemes per host, like a bearer header on one and basic auth on another, with no change to your code.

Under the hood

Binding a secret never puts your key in the sandbox. The sandbox boots with a per-sandbox token in place of the real value, and its HTTPS traffic is routed out through the platform. The real key stays in an encrypted vault, fetched outside the VM only at the moment a request leaves - and only then swapped in. If the secret is revoked or can't be resolved, the request fails instead of going out bare.

Each sandbox is a Firecracker microVM - its own kernel and filesystem, hardware-isolated, not a container sharing a host kernel with its neighbors. That matters here: the real key lives outside that boundary, so even code that fully takes over the sandbox can't reach it.

Revocation is immediate and fails closed. Delete a secret or a sandbox and the stand-in stops working. Requests that depend on it fail instead of falling back to a stale key. Rotate a secret's value and every bound sandbox keeps running, because none of them ever held the value in the first place.

Secrets stops a key from leaking, not from being used: while a sandbox is running, code in it can still hit the hosts a secret allows. That's what network rules and instant revoke are for, and you should still scope the underlying key to the access it actually needs.

See where every key is used

Keeping a key out of the sandbox is only half the job. You also want to know where it's being used. Every secret has its own activity log: which sandboxes are bound to it, and every request made with it, down to the host, path, status, and latency.

When something looks off, that's how you find out what actually happened.

Get started

The fix isn't to trust the agent with less. It's to make sure there's nothing worth stealing in the box. Your agent shouldn't be one cat away from leaking your keys, and with Secrets it isn't - it takes one line to turn on.

Read the Secrets guide to set up your first secret, or sign up at console.superserve.ai and try it now.

[ Try it ]

Your first sandbox in seconds.