Give your sandboxed agents API keys they can't read Superserve launched Secrets, a feature that lets developers attach API keys to sandboxed agents without exposing the credentials inside the sandbox. The real key is replaced with a stand-in token that gets swapped out only when requests leave the sandbox for allowed hosts, preventing prompt injection attacks from leaking credentials. ← Back to blog /blog/ Give agents API access without putting credentials in the sandbox 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: Harnesses like Claude Managed Agents avoid this by design: the agent loop runs outside the sandbox, so no credentials ever need to go in. But as more teams run agents directly inside the sandbox Claude Code, Codex, custom loops that need to hit APIs mid-task , that protection disappears. Secrets is built for exactly that case. The missing layer The requirements sound simple: no key in the sandbox, the agent can't exfiltrate it, and streaming input and output still work. Until now, meeting them meant building and running a credential-substituting layer yourself. So most teams avoided the work and set the env var hoping nothing read it. Some teams, like Listen Labs https://listenlabs.ai/blog/we-lied-to-our-claude-code-agent?ref=superserve , actually took the plunge and built it. Secrets is that layer, now available natively with Superserve sandboxes. How it works Store the credential once. The value is encrypted at rest and write-only. You can't read it back out: js 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: js 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 - without having to 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 mailto:engineering@superserve.ai?subject=Provider%20request 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 secrets to exactly the hosts they 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: js 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 https://docs.superserve.ai/secrets/overview to set up your first secret, or sign up at console.superserve.ai https://console.superserve.ai and try it now.