OpenSandbox Credential Vault: Your Agent Runs With a Fake API Key and the Requests Still Work
A sandbox runtime from Alibaba's OpenSandbox project sets ANTHROPIC_API_KEY to the literal string "fake-key-inside-sandbox," runs Claude Code against it, and the API call succeeds. Here's the mechanism, and the part it doesn't fix.
Buried in the OpenSandbox docs is a working example that reads like a mistake. It creates a sandbox, sets ANTHROPIC_API_KEY to "fake-key-inside-sandbox", installs Claude Code inside, runs claude -p '1+1', and prints the answer. The key is fake. The call to api.anthropic.com still authenticates.
That's the whole idea in one code block, and it inverts the question everyone building agent infrastructure has been arguing about. For two years the conversation has been about what an agent is permitted to do with a secret: which scopes, which approval gate, which audit trail. OpenSandbox's Credential Vault asks a different question. What if the agent process never has the secret at all?
Why "the agent shouldn't hold the key" is the right shape
Every credential control that lives inside the agent's blast radius is a control the agent can be talked out of. Environment variables get printed. Config files get read. Command lines land in shell history and in traces. Prompt injection doesn't need to defeat your permission system if it can convince the model to echo $ANTHROPIC_API_KEY into a file it's already allowed to write, or into a network call to a host you forgot to block.
The standard answers are all downstream of that problem rather than upstream of it. Short-lived tokens shrink the window. Narrow scopes shrink the damage. Approval gates add a human who, per Anthropic's own testing, approves 97% of what they see. All useful, none of them stopping the value from existing somewhere the agent can read it.
OpenSandbox puts the secret in a place the agent cannot reach and then adds it to the request after the agent is done with it. I think that's the correct trust boundary, and I'd been sketching a worse version of it with a local proxy for months before I read this doc.
The mechanism
Credential Vault is implemented by the egress sidecar, not by the SDK and not by the model. Six steps, straight from the guide.
The lifecycle server attaches an egress sidecar to the sandbox. The SDK, running on your host where the real secrets live, writes credentials and bindings to that sidecar's vault API. The sandbox process starts with fake or empty credential environment variables. When something inside the sandbox makes an HTTPS request, transparent interception in the sidecar inspects the request metadata. If exactly one binding matches the request's scheme, host, port, method, and path, the sidecar injects the configured auth header. Secret values get redacted from vault responses and from response headers.
The matching rule is the part worth reading twice. Exactly one binding. Overlapping bindings at the same precedence are rejected as ambiguous rather than resolved by some priority order you'd have to reason about later. A binding for the Anthropic API looks like this:
CredentialBinding(
name="anthropic-api",
match={
"schemes": ["https"],
"ports": [443],
"hosts": ["api.anthropic.com"],
"methods": ["GET", "POST"],
"paths": ["/v1/*"],
},
auth={"type": "apiKey", "name": "x-api-key", "credential": "anthropic-api-key"},
)
Four auth types cover most of what real tools need: bearer writes an Authorization: Bearer header, basic takes a pre-base64'd username:password, apiKey puts the value in a header name you choose, and customHeaders fans one binding out across several headers each backed by its own credential. That last one is what makes OAuth client-id-plus-secret pairs work without either half touching the sandbox.
Two design details tell you the authors thought about the adversary rather than just the happy path. The active vault used by the interception process is served over a Unix domain socket inside the sidecar, and the guide states plainly that the sandbox workload cannot fetch that state through the normal server proxy path. And plaintext credential values are write-only: get, list, and patch responses all return sanitized metadata. You can't read back what you put in, which means neither can anything running in the box.
The pattern generalizes past model APIs. The docs show git clone https://git.example.com/org/private-repo.git with no credentials in the URL, backed by a basic binding scoped to that repo's path. Same trick for curl against an internal API with a PRIVATE-TOKEN header. Your agent runs the ordinary command. The header appears on the wire.
The rest of the repo, briefly
Credential Vault is one feature of a larger thing. OpenSandbox is Apache 2.0, and the packaging leaves no doubt about the sponsor: the npm package is @alibaba-group/opensandbox, the Maven coordinates are com.alibaba.opensandbox. The project carries a CNCF listing and an OpenSSF Best Practices badge.
Underneath sit Docker and Kubernetes runtimes, a sandbox protocol with published OpenAPI specs so you can implement your own runtime, ingress routing with per-sandbox egress control, and support for gVisor, Kata Containers, and Firecracker microVMs if container isolation alone isn't enough for your threat model. There are SDKs in Python, Java/Kotlin, TypeScript, C#, and Go, an osb CLI, and an MCP server that exposes sandbox creation and command execution to Claude Code or Cursor directly. Worked examples cover Claude Code, Gemini CLI, Codex CLI, Qwen Code, Kimi CLI, LangGraph, Playwright, and full VNC desktops.
Put this into practice
You need Docker and Python 3.10+. The shortest honest path to seeing it work:
uvx opensandbox-server init-config ~/.sandbox.toml --example docker
uvx opensandbox-server
pip install opensandbox
Then create one sandbox with a default-deny network policy, one allow rule for the single host your tool needs, and one credential binding scoped as tightly as you can stand. The docs recommend defaultAction="deny" plus a narrow path match like /v1/*, and that recommendation is doing more work than it looks like: a binding scoped to a host but not a path will happily attach your key to any request to that host, including one an injected instruction wrote.
Keep the fake environment variable. Most CLIs refuse to start without something in the slot, and the guide is explicit that the fake value is fine because the injected header is what actually authenticates. That detail is what makes this deployable against tools you didn't write.
Start with a secret whose blast radius you can absorb. A read-only registry token beats your production database credential for a first run, because you're testing whether your binding match is correct, and the failure mode of a wrong match is a credential attached to a request you didn't intend.
Where it breaks
The Istio problem is the big one. Credential Vault depends on the egress sidecar's transparent redirect, and if your sandbox pods also get a service-mesh sidecar injected, both layers try to intercept outbound traffic in the same network namespace. OpenSandbox says it does not support that combination. Your options are to disable mesh injection for sandbox pods, keep the mesh and skip Credential Vault, or move credential handling outside the pod entirely. For a lot of production Kubernetes shops, that's a hard stop rather than a footnote.
OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_REQUIRE_TLS defaults to off. With it off, any authenticated request can write to the vault regardless of transport. The docs tell you to enable it when the sidecar is reachable from untrusted networks without a TLS-terminating proxy, which is correct advice and an odd default for a component whose entire job is holding secrets. Flip it.
The version floors are real and easy to trip over: opensandbox-server >= 0.2.0, egress >= 1.1.1, plus a different minimum for every SDK. This is not a config toggle on an existing install.
Then there's the honest architectural point. This is a man-in-the-middle on your agent's TLS, by design. You are choosing to trust a sidecar to read and rewrite traffic that was supposed to be end-to-end encrypted. That's a coherent trade in a sandbox you control, and it's still a thing you should say out loud before you deploy it.
The limitation that matters most: Credential Vault stops exfiltration of the value, not misuse of the capability. If your binding allows POST /v1/* to a host, a compromised agent can still make any POST to any path under /v1 and the sidecar will authenticate it faithfully. The secret doesn't leak. The action still happens. Narrow bindings are the only lever you have there, and narrowness is manual work nobody will do consistently.
Two data-quality notes. Star counts for this repo were inconsistent across three page loads in a single session, serving 11.7k, 12k, and 12.4k with forks moving between 968 and 1k, while third-party mirrors reported 12.5k and 13k. Treat the momentum as the signal and the number as noise. Separately, the Go SDK's import path is github.com/alibaba/OpenSandbox/sdks/sandbox/go while the repository itself lives at opensandbox-group/OpenSandbox. Worth knowing before you wire that path into a build and wonder which org you're actually pulling from.
What to take from it
You don't have to adopt OpenSandbox to use the idea. The design is copyable in an afternoon: a proxy your agent must route through, a credential store your agent can't read, and a match rule tight enough that the injected header only lands on requests you'd have signed yourself.
Go read the credential-vault guide and then go look at where your agent's API keys currently live. If the answer is an environment variable in the same process as the model, you already know what the exercise is.
Sources: OpenSandbox repository and README; Credential Vault guide; egress component; npm @alibaba-group/opensandbox.