Engineering By Hsin, Head of Product

Running untrusted agent code: the hole isn't escape, it's egress

"We need to run untrusted code safely" is one of those sentences everyone nods at and nobody defines. Untrusted is doing an enormous amount of work in it — and which of the three things you actually mean decides whether a container is the right answer, the wrong answer, or beside the point.

Where this comes from: we build an agent sandbox, so read the recommendations with that in mind. We've tried to earn it by being specific about the case where our own product is the wrong tool — that section is near the bottom and it isn't hedged.

"Untrusted" is three different threats

They need different controls, and conflating them is why teams buy isolation they don't need while leaving open the thing that will actually bite them.

Three meanings of untrusted code and the control each one needs
What you mean Realistic worst case Control that actually helps
Unreviewed
code your agent wrote, from your prompt, on your repo
It deletes something, wedges a package manager, burns an afternoon A disposable container. Delete and recreate.
Unvetted
the dependency tree the agent just installed
A postinstall script reads your environment and posts it somewhere Egress control. The container doesn't help here.
Hostile
code from someone who wants in — e.g. you run strangers' code as a product
Kernel exploit, container escape, other tenants' data A machine or hypervisor boundary. Not a shared-kernel container.

Almost everyone asking this question about coding agents is in rows one and two. Almost everyone shops as though they were in row three — comparing isolation technologies, reading about microVMs — and then deploys with unrestricted outbound network access, which is the actual hole in rows one and two.

What a container boundary actually buys you

Being precise about this is the whole game. A container gets you:

  • Blast-radius containment. A destructive command destroys one environment. You delete it and make another. This is genuinely most of the value, and it is why "disposable" matters more than "hardened" for day-to-day agent work.
  • Credential separation. The agent isn't operating the machine that holds your SSH keys, cloud credentials, and every other repo you have checked out. This is the difference between "an agent broke something" and "an agent had my identity".
  • Reproducibility. A clean box tells you whether the failure was the code or the environment — which is most of what you argue about otherwise.

And it does not get you:

  • Protection from exfiltration. A container with a network connection can read the source tree it was given and send it anywhere. Isolation from the host says nothing about reaching the internet. This is the gap.
  • A boundary against a kernel exploit. Containers share the host kernel. Against genuinely hostile code, that is not a security boundary — a position we state on our own enterprise page too.

So control egress, not just escape

The realistic bad day isn't a kernel exploit. It's an agent installing a package whose postinstall script reads every environment variable in the process and POSTs them to a host you've never heard of — and the container doing exactly what containers do while it happens.

The control that helps is an outbound allowlist: the box reaches your package registry, your git remote, your model API, and nothing else. It is unglamorous and it is the one that would have caught most of the real incidents in this category.

How this works in Containarium — including the parts that will bite you

We enforce per-box egress allowlists with eBPF programs in the kernel, so the policy applies to the box's traffic regardless of what the code inside tries.

It is off by default, and observe-only until you arm it. Out of the box you get the attempts recorded, not blocked. Arming takes both a daemon-wide switch and a per-agent opt-in, on a Linux backend:

# daemon-wide enforcer
CONTAINARIUM_NETWORK_POLICY_BPF_OBJECT=/path/to/netpolicy.bpf.o
CONTAINARIUM_NETWORK_POLICY_ENFORCE=1

# per-agent enforcement + the egress it legitimately needs
CONTAINARIUM_AGENT_NETWORK_POLICY_ENFORCE=1
CONTAINARIUM_AGENT_EGRESS_CIDRS=<daemon-api>/32,<dns>/32

That last line is the trap. Arm enforcement without listing the platform egress the agent actually needs — the daemon API and DNS — and you'll strand it: a peer-only allowlist means it can't resolve a hostname or call home to the daemon, and you get a confusing hang rather than a clean denial. Budget a round of "observe the flows, then write the allowlist" before you switch it on.

Observe-first is deliberate — a default-deny that nobody validated against real traffic gets switched off in a hurry the first time it breaks a build, and then you have neither enforcement nor the habit of it.

The second question: whose machine is it running on?

There's a move that looks like a solution and is partly a trade. Hosted agent sandboxes contain untrusted code beautifully — on their infrastructure. Which means the code you were worried about has been copied out of your network, along with the repo it needs to be useful.

If your concern was "this code might leak our source", note what just happened: you addressed the risk of exfiltration by performing one. Deliberately, to a vendor with a contract — but the source tree left the perimeter either way, and for a regulated or data-residency-bound team that is the whole question, not a footnote.

This is the argument for self-hosting the sandbox: the isolation properties are the same, and the code stays on a machine you own. That's the case we make on the agent sandbox page, and it's the reason our core is Apache 2.0 and installs on one VM.

When a box is the wrong tool

If you are in row three — running code submitted by people you have no relationship with, as a product — then a shared-kernel container is not your boundary, and ours isn't either. One kernel CVE and the boundary is gone. No amount of seccomp tuning changes the shape of that.

What you want in that case is a boundary the kernel isn't part of:

  • A microVM (Firecracker and friends) — a real hypervisor boundary, at the cost of boot time and some device flexibility.
  • A userspace kernel like gVisor — a smaller host-kernel attack surface, at some syscall-compatibility and performance cost.
  • A machine per tenant. Blunt, boring, effective. This is what we point enterprise customers at: dedicated VMs in your own cloud account, where the machine is the boundary.

We'd rather say that plainly than sell you a container as a defence against an attacker who is specifically trying to break containers.

A short checklist

For the common case — a coding agent, your repo, dependencies you didn't audit:

  1. 01Give the agent its own environment, not your laptop. Blast radius and credential separation in one move.
  2. 02Make it disposable. If recreating is expensive you'll nurse a compromised environment instead of deleting it.
  3. 03Put nothing in it you wouldn't lose. Scope its credentials to itself — one SSH key, not a cluster token or your cloud profile.
  4. 04Observe its egress for a week, then allowlist it. You cannot write a correct policy from imagination, and a wrong one gets disabled.
  5. 05Decide consciously whether the code may leave your network — and if it may not, self-host rather than discovering the answer during procurement.
  6. 06If your threat model is genuinely hostile code, stop reading about containers and go get a machine boundary.

Give the agent a box, not your laptop.

Self-host the open source on your VM, or start free on the hosted cloud.