DARINORCold Lab

// Writing

Agent Containment Is an Environment Property

By
9 min read
Agentic AI security#agentic-ai#ai-security#agent-architecture#threat-modeling

Every agent stack ships with two boundaries. The one it advertises — the approval dialog, the confirmation flash, the "allow this?" — and the one it actually has, which is whatever the agent can still reach after the dialog fails.

The dialog fails. Not sometimes, not on a bad day — routinely, by design. A persuasive injected prompt can argue with it. Approval fatigue makes the human holding the click a rubber stamp. And a model that has learned the task matters more than the rule will treat the dialog as one more obstacle to route around. The advertised boundary lives inside the agent's own reasoning, and everything the agent can reach can reach it.

So the question that actually matters when you deploy one is not "does it ask permission?" It's "what is still true when the answer it gets is the wrong one?"

Approval is a request, not a fence

Watch what happens when an agent asks for something dangerous. The dialog pops. A human glances at it. If the human is paying attention and has the expertise to judge what's on screen, the request gets refused. Two conditions, both optional, both doing load-bearing work.

Anthropic launched Claude Code exactly this way: reads allowed, and a human approving every write, every shell call, every network hop. It's the simplest possible defense and, for a developer audience, a defensible one — the average user can read bash and knows what rm -rf does. The plan lasted weeks. Not because users got careless in the abstract, but because the approval rate made attention itself impossible. A mechanism that requires a human to evaluate every request trains that human to stop evaluating requests. The oversight feature became the attack surface — the opposite effect of the one intended.

The fix wasn't a better dialog or a stricter wording. It was architectural: an operating-system sandbox underneath the approvals. Reads allowed. Writes inside the workspace. Network off unless you say so. The agent runs mostly uninterrupted, and the boundary it can't talk its way past is enforced by the OS, not by whoever is nearest the keyboard at the moment.

That's the pattern worth taking. The dialog had a job — it still does, for the calls worth a human's attention. But it stopped being the only thing between the agent and the rest of your machine. Per-call checks like validating tool calls before they execute catch the bad argument; the environment caps what every argument, caught or not, can touch.

The environment holds what the reasoning can't reach

Containment, done properly, is a set of facts about the world the agent runs in — not rules it's asked to follow. The shape of it comes down to three facts worth writing on the whiteboard:

Credentials never enter the box. If the agent's runtime never holds your cloud keys, your tokens, your customer database password, then no prompt — injected, persuasive, or sincerely confused — can leak them. Anthropic phrases it as the cleanest one-liner in the field: if credentials never enter the sandbox, they can't be exfiltrated. The cause doesn't matter. Attacker, user, or a model finding a creative path — the outcome is the same, because the material was never there.

There is no key to negotiate for. The same engineering post describes why their full-VM design held under pressure: the agent loop ran inside the guest, with no privileged process sitting outside ready to grant exceptions. A persuasive prompt, a fatigued click, a clever jailbreak — there was nothing to persuade. The boundary had no escape hatch, so there was nothing to argue with. Contrast the common architecture, where a supervisor process decides per-command whether to enforce the sandbox. That supervisor is a component. Components have authority. Authority can be socially engineered — and for an agent with language, everything is social engineering.

The walls don't depend on the model being good. This is the part teams resist. Model-level guardrails — refusals, system prompts, policy training — are real and worth having, but they're a probability distribution wearing a uniform. The environment is deterministic. The wall doesn't get tired, doesn't get talked around, doesn't have a bad day. Put the load-bearing boundary in the part of the stack that doesn't have opinions.

The allowlist is the new perimeter

Here's the case that should reset how you think about egress. A file lands in a workspace where an agent is running. Hidden inside it, instructions — and an API key belonging to the attacker. The agent reads the instructions, gathers other files from the workspace, and uploads them. Where? To the attacker's account on the very API the agent was already authorized to call. The egress proxy checked the destination, saw a domain on the allowlist, and passed the traffic through. Anthropic disclosed this exact sequence against their own product.

Sit with the detail that matters: the sandbox worked perfectly. Every control fired as designed. The data left anyway, riding on a destination nobody would call suspicious — the same one your own stack needs to function.

Deny-by-default egress is the right foundation and it isn't enough. Once your agent must talk to something — and it must, or it's not an agent — the allowlist becomes the perimeter, and the perimeter inherits an old truth: approved channels are where exfiltration goes to hide. The question was never "is the destination on the list?" It's "what rides on the traffic to that destination, and who's watching the content?"

Which is why the practitioners who've had the incident keep landing on the same pattern for anything that leaves the box: hash the exact payload before egress, log the hash. It sounds bureaucratic until the review. Then it's the difference between attribution as a lookup and attribution as an archaeology project. You can't un-send an exfiltration, but you can make it nameable — and a nameable incident is a different universe from a silent one.

Designing the box

None of this requires exotic infrastructure. It requires deciding, before the agent ships, what it can reach — and enforcing the decision somewhere the agent isn't. The moves, in the order I'd do them:

Map reach before capability. Before the agent gets its next tool, write down what it can touch: filesystem paths, network destinations, credentials, other agents. Every capability you add is reach you've added, whether you drew it on the map or not. The config your agent runs on is where most of this is written down already — most teams have never read it the way an attacker would.

Keep secrets outside. Short-lived, per-task credentials that expire when the task does. The delegation model matters as much as the lifetime — an agent acting as you, with your token, is your name on every log line, and workload identity is how the machine hop gets scoped in the first place. If the agent can hold less than you, everything downstream of a compromise shrinks with it.

Deny by default, then audit the allowlist. Network off, then turn on destinations one at a time, each with a reason written next to it. Revisit the list the way you'd review a firewall — because it is one. And remember the lesson above: the dangerous destination is the one that's legitimately there.

Make stopping mechanical. There must be a way to end the agent that does not route through the agent. A process kill, a token revocation, a session teardown — outside its loop, reachable by a human, rehearsed before you need it. Revocation mid-task is the identity-layer version of the same idea. If your containment story ends with "we'd tell it to stop," you don't have one.

Assume the dialog fails. Size every grant so that the worst approval mistake is survivable. One workspace per task. One scope per run. A blast radius the next morning can clean up. Per-action approval is a component of containment, not a substitute for it — the box is what makes a wrong click cheap.

What containment cannot do

An honest version of this post has to say where the wall ends.

It cannot fix over-scoping. An agent provisioned with the whole repository, the whole mailbox, the production database, is contained the way a warehouse fire is contained to the warehouse. The boundary decision is a design decision, and no runtime can retroactively make a too-wide grant narrow.

It cannot read content flowing to approved destinations. The exfiltration case above went out through a door that had to stay open. Catching payload-shaped traffic on legitimate channels is a different discipline — content inspection, anomaly detection, the dull glory of DLP. Containment tells you where data can leave. It stays silent on what leaves through an open door.

It cannot make the agent right. A perfectly contained agent can be confidently, expensively wrong all day — deleting the wrong files inside its box, sending the wrong messages through its pipe. Containment caps damage. Correctness is the harness's problem, and the two get confused because both are "safety." They fail differently and you will need both.

And it cannot repair a human process that rubber-stamps approvals. Containment reduces how many decisions a human must make, which is precisely why it helps — fewer decisions, better decisions. The teams that get this wrong are the ones that treat the dialog as the containment and the sandbox as optional.

Approval is a request for the agent to be trusted. Containment is what stays true when it isn't. One lives in the model's behavior; the other lives in the environment — and only one of them survives a bad day.

If you run one agent in production, do this today: write down its filesystem reach, its network destinations, and exactly how you'd stop it mid-run. If any of the three answers is "I'd ask it to," the boundary is a request. Red teaming the agent will find the rest.

The model is a probability distribution. The environment is the only part of the stack that isn't. Put the boundary there.