← Blog

Keeping secrets out of agent sandboxes

Engineering·The alineo team
A terminal inside an agent's sandbox — env shows no token, a curl to api.github.com returns HTTP 200, and after the credential is revoked the same request returns 401.

An agent that can run code and make network requests is, from a security point of view, a program that will do whatever its input tells it to. The model cannot reliably separate the task you gave it from instructions buried in a web page, a repo it cloned, or a tool result — prompt injection is not an edge case, it's the normal failure mode.

So the question for any credential you hand an agent is: what happens when the agent is told to exfiltrate it?

Environment variables are the worst answer

The usual way to give a process a secret is an environment variable. For an agent that's a bad fit on every axis:

  • It's readable by anything in the process — including code the model wrote itself, a dependency, or a shell command a tool ran.
  • env aggregates every service's key in one place, so one successful injection is a multi-system incident. GitGuardian's 2026 scan found over 24,000 secrets in MCP config files on public GitHub, 2,100+ of them still valid.
  • It has already happened in the wild — Devin was indirectly prompt-injected into reading its environment and POSTing the contents to an attacker's server.

"Instruct the agent not to leak the token" is not a control. If the token is in the sandbox, assume it can leave.

Inject it at the edge instead

alineo's answer is that the credential never enters the sandbox at all. sb.credentials.set() registers the value host-side, along with a rule for where it applies:

await sb.credentials.set("github", process.env.GH_TOKEN!, {
  host: "api.github.com",
  injection: { type: "header", name: "Authorization" },
});

From then on, any outbound request the sandbox makes to api.github.com gets that header added by the egress sidecar, on the wire. A plain curl https://api.github.com/user with no auth header of its own comes back authenticated. Requests to any other host are untouched. Run env inside the sandbox and the token isn't there — it isn't on the filesystem either, and the ledger records only that a credential named github was bound to a host, never the value.

What it looks like

A terminal inside an agent's sandbox: env | grep finds no token; curl to api.github.com/user returns HTTP 200 because the sidecar injected the header; after the credential is revoked the same request returns HTTP 401.

A prompt-injected agent in that sandbox has nothing to read, nothing to upload, and no Authorization header to copy out of a request it made — the value only exists on the wire, leaving for one host.

One layer of several

Credential injection is the layer that protects the secret's value. It sits inside a stack that also constrains what the agent can do with it:

  • Isolation — the code runs in an ephemeral container, not on your machine. Nothing it does outlives sb.close().
  • Network policy — an allow/deny list of hosts the sandbox may reach at all. This is what stops a bound credential from being redirected: an injected GitHub token can't be POSTed to evil.example.com if the sandbox can't open a connection there in the first place.
  • Permission gate — pause a tool call for a human before it runs, by mode ("ask", "readonly") or a per-tool policy.
  • Egress approval hold — mark a credential approval: "hold" and it isn't registered at all until a human approves the first request to that host. The secret does not exist in the sandbox until then.
  • Audit ledger — every credential set/remove and every gated request is written to the ledger (metadata only), so alineo logs <session> is a complete record.

What it doesn't do

  • It needs the egress sidecar — pass credentialProxy: true and a networkPolicy when creating the sandbox. A sandbox without them has ordinary unrestricted egress and sb.credentials.* throws.
  • It protects the value, not the capability. An agent with a GitHub token bound can still do anything that token permits against api.github.com. Scope your tokens, and reach for the permission gate or the egress hold when "can make authenticated calls to this host" is itself too much.
  • substitution injection (for APIs that want the key in the URL) only works if the outbound request already contains the placeholder verbatim — the sidecar substitutes, it doesn't append.

It's a complement to short-lived, narrowly-scoped tokens, not a replacement for them.

Try it