Running AI Agent Generated Code Safely: What Actually Contains It

If an agent can run shell commands, you have an arbitrary code execution problem. Here is what each isolation option genuinely protects against.

Share
Running AI Agent Generated Code Safely: What Actually Contains It. Abstract ai tooling illustration in orange and dark grey on debugly.dev

Disclosure: I run Krova Cloud, which builds isolated microVMs used for exactly this workload. Running the platform is where most of the detail below comes from.

The short answer

The moment an agent can execute shell commands, install packages, or write files, you have an arbitrary code execution surface. Not a hypothetical one.

Ranked by what they actually contain:

Approach Contains a mistake Contains an attacker Cost
Nothing, agent runs on your laptop no no free
Allowlisted commands, no shell partly no free
Docker container mostly no free
gVisor or Kata mostly mostly moderate
microVM per session yes yes per minute
Separate physical machine yes yes high

The important column is the middle one. A plain container is decent at stopping an agent from accidentally deleting your home directory, and it is not a security boundary against code that is actively trying to get out.

The two different threats

People conflate these and they need different answers.

The agent makes a mistake. It runs rm -rf with a variable that expanded to empty. It pip installs something that overwrites a system package. It writes a 40GB file. It commits secrets. This is the common case, it happens weekly, and it is not adversarial.

Something in the chain is hostile. The agent fetched a web page containing a prompt injection that told it to exfiltrate your environment variables. It installed a package that was typosquatted. A tool returned crafted output. The agent is now doing what an attacker wants, using your credentials.

The second threat is not theoretical. Agents that browse, that read issue trackers, or that install dependencies are all consuming untrusted input and acting on it with your permissions. Prompt injection has no reliable fix at the model layer, which means the containment has to be architectural.

What each option actually gives you

Running on your machine with no isolation

Your agent has your SSH keys, your cloud credentials, your .env files, your git history, and your browser cookies if the profile is readable.

This is where most people start, and it is fine for a scratch project. It stops being fine the moment the agent has network access and reads anything it did not author.

If you do this, at least scope the credentials. A separate OS user for agent work costs an afternoon and removes an enormous amount of blast radius.

Command allowlists

Restricting the agent to a fixed set of commands with no shell interpolation is genuinely effective against mistakes and it is what most well designed agent tools do by default.

The failure mode is that the allowlist has to be short to be safe. Allow git, and git has hooks. Allow npm, and package install scripts run arbitrary code. Allow make, and the Makefile is a shell script. Allow python, and you have allowed everything.

Useful as a first layer. Not a boundary.

Docker

The standard answer and a reasonable one for the mistake threat.

docker run --rm -it \
  --network none \
  --read-only \
  --tmpfs /tmp:size=512m \
  --memory 2g --cpus 2 --pids-limit 256 \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  -v "$PWD:/work" -w /work \
  python:3.13-slim

Every flag there is doing something worth understanding. --network none is the highest value one by far, because most of what goes wrong with a compromised agent requires egress. --read-only with a tmpfs stops persistence. --pids-limit stops fork bombs. --cap-drop ALL removes the forty odd root capabilities.

What this does not do is stop a kernel exploit, because the container shares your kernel. It also does nothing if you mount the Docker socket in, which people do so the agent can build images, and which hands over the host entirely.

For most solo developers running an agent on their own code, a well flagged container is a sensible place to land.

Sandboxed runtimes

gVisor intercepts syscalls in user space and implements most of them itself, so the host kernel sees a much smaller surface. Kata runs each workload in a lightweight VM behind a normal container interface.

Both are real improvements and both cost you something: gVisor breaks on unusual syscalls and slows syscall heavy work, Kata needs nested virtualisation support and more memory.

If you are already running Kubernetes and need better isolation for a subset of workloads, a separate RuntimeClass pointing at one of these is much less disruptive than changing platforms.

One microVM per session

The agent gets its own kernel. A kernel bug in the guest does not reach the host, and the interface to the outside is a handful of emulated devices rather than 350 syscalls.

This is what I build, and the case for it is straightforward: the moment you run agent code on behalf of other people, one tenant reaching another is a company ending event rather than a bad afternoon. A kernel boundary is the only thing that actually prevents it.

The practical benefit that matters most in agent workflows is not only security, it is statefulness. A container torn down between steps loses the installed packages and the filesystem, so the agent redoes work. A persistent isolated machine keeps the environment and lets the agent behave like a developer with a workstation.

This is also where session limits bite. Most hosted sandbox products cap a session in hours, so a long running agent gets cut off mid task. A Cube has no session ceiling: the same machine covers a thirty second tool call and a service you leave running for a year.

Separate hardware

Air gapped or network segmented physical machines. Correct for genuinely hostile workloads, and overkill for everything else.

The controls that matter more than the runtime

The isolation layer gets the attention. These matter more in practice.

Network egress policy. The single highest value control. An agent that cannot reach the internet cannot exfiltrate anything, cannot install a malicious package, and cannot be told to call a webhook. If it needs package registries, allowlist those specific hosts rather than opening egress entirely. Most agent workflows work fine with a proxy allowing npm, PyPI, and your git host.

Credential scoping. Never give the agent your personal credentials. A scoped token, with the minimum permissions, that expires. If the agent needs to push code, give it push access to one repository, not your account. Read the token's permission list and cut it in half.

No secrets in the environment. Agents read environment variables constantly, and they end up in logs, in prompts, and in model context. Mount secrets as files with restrictive permissions, or inject them at the point of use.

Resource limits. Memory, CPU, PIDs, disk. An agent in a retry loop will happily consume everything available, and you will find out through an OOM kill with no diagnostic information.

A timeout on everything. Agents get stuck. A hard wall clock limit per session prevents a runaway loop from running overnight.

Audit the commands. Log every command the agent executed, with output. When something goes wrong you need the sequence, and this is the difference between a five minute review and an archaeology project.

A reasonable default

For an individual developer working on their own code, the boring answer is fine to start with: a container with --network none where possible, an allowlisted egress proxy where not, scoped credentials, resource limits, and a session log. That covers the mistake threat.

Move up to a kernel boundary as soon as the answer to "whose code is this" stops being "mine". Running agents for users, executing untrusted pull requests, or building a product where customers bring their own workloads all put you in a different threat model, and at that point the shared kernel is the thing standing between two customers.

The mistake I see most often is treating this as a decision to make later. Agent tooling is easy to adopt incrementally, and the isolation question tends to get asked after the workflow is already load bearing. It is much cheaper to put the boundary in at the start, and provisioning an isolated machine per agent run is now a single API call.

Related reading: the six bugs coding agents write most often and why AI generated code passes tests and still breaks production.