Skip to content

Running Agent Code Safely

An agent that can write and run code is only as safe as the box you run it in. If your agent can execute Python, shell commands, or install packages on its own, that code needs to run somewhere it can't touch your real files, your real network, or your host machine, even if the agent was never given bad instructions on purpose. This module covers the practical setup: a Docker container an agent can't escape, configured the way you'd actually run it in production.

Gnome mascot

What you'll learn

  • Why "it's in a container" isn't automatically safe by default
  • The four settings that actually stop an agent from doing damage
  • When a container is enough, and when you need something stronger

1. The problem

Garden Gnome sandboxing terminal

A standard Docker container isn't a locked box, it's a process with a curtain around it. By default it can still use unlimited memory and CPU, run as root, write anywhere inside itself, and reach the internet. None of that matters for a normal web app. All of it matters when the "user" writing code inside the container is an LLM that can be tricked, confused, or simply wrong.

The failure mode isn't usually a sophisticated attacker, it's an agent that writes an infinite loop, installs a package with a post-install script you didn't review, or (via prompt injection from a document it read) gets talked into running something it shouldn't. The container needs to survive that without taking your machine down with it.

2. Options & when to use each

OptionGood forCosts youWhen to pick it
Plain Docker, locked downMost agent code-execution use casesA few extra flags to rememberDefault choice, covers the vast majority of "let the agent run some Python" needs
gVisor / Firecracker (stronger isolation)Running genuinely untrusted, third-party, or high-risk codeMore setup, slower startup, extra infra to runMulti-tenant products where strangers' code runs on your infra, or compliance requires it
No sandbox (direct exec on host)Nothing you should shipYour machineNever for agent-generated code

For nearly every internal tool or single-user product, a locked-down Docker container is the right call. Stronger isolation (gVisor, Firecracker microVMs) exists and is worth knowing the name of, but it's a jump in operational complexity you should only make if you're running code from people you don't trust, at scale.

3. Build it

A minimal, safe image

Create a Dockerfile that sets up a non-privileged system user and removes root access, ensuring that even if the agent-written code contains vulnerabilities, it cannot escalate privileges inside the container environment:

dockerfile
FROM python:3.11-slim

# Keep the image small and avoid stale package lists
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Never run agent code as root, a compromised root process can touch
# anything else the container's filesystem exposes
RUN useradd --create-home --uid 1000 appuser
USER appuser
WORKDIR /home/appuser

COPY --chown=appuser:appuser requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .
CMD ["python", "run_agent_task.py"]

Running it locked down

The image alone isn't the safety boundary, how you run it is:

bash
docker run \
  --rm \
  -m 512m \
  --cpus 1 \
  --network none \
  --read-only \
  -v "$(pwd)/workspace:/home/appuser/workspace:rw" \
  agent-sandbox:latest
FlagWhat it stops
--rmAnything the agent wrote inside the container disappears the moment it exits, no leftover artifacts to clean up or leak.
-m 512mCaps memory. Without this, a runaway loop can consume all your host's RAM and take down everything else running on the machine.
--cpus 1Caps CPU, so a busy-loop can't starve the rest of your system.
--network noneThe agent's code cannot reach the internet or your internal network at all. If it needs to make API calls, do that outside the container and pass results in, don't give the sandbox its own network access unless it truly needs it.
--read-onlyThe container's own filesystem can't be modified. Only the explicitly mounted workspace volume is writable, so you know exactly where any output will land.

If you need stronger isolation

Swap the runtime, keep everything else the same:

bash
docker run --runtime=runsc --rm -m 512m --cpus 1 --network none \
  -v "$(pwd)/workspace:/home/appuser/workspace:rw" \
  agent-sandbox:latest

--runtime=runsc runs the container through gVisor instead of the default runtime, the container's code no longer talks to your host's kernel directly, so a kernel-level exploit that would normally break out of a container can't. It costs you some I/O speed and a slightly slower startup. Reach for it when the code you're running isn't just "an agent working on my behalf" but genuinely untrusted input from someone else.

4. What goes wrong

MistakeHow you notice itThe fix
Container runs as rootYou didn't set USER appuser in the DockerfileAdd a non-root user; rebuild the image
Agent's runaway process kills your machineHost slows to a crawl or OOM-kills unrelated processesAdd -m and --cpus limits, you should never run agent code without them
Agent "phones home" or hits an API you didn't expectUnexpected outbound traffic in docker network inspectRun with --network none unless the agent specifically needs network access, and if it does, scope it to exact hosts
Leftover files from a previous run affect the next oneAgent behaves differently between runs with no code changeAdd --rm so every run starts from a clean container
Output written somewhere you didn't expectFiles show up outside your intended workspace folderAdd --read-only and mount only the one directory you want writable

5. Confirm it worked

Run the container and check its actual permissions:

bash
# Confirm it's not running as root
docker run --rm agent-sandbox:latest whoami
# should print: appuser

# Confirm memory limit is enforced, this should get OOM-killed, not crash your host
docker run --rm -m 64m agent-sandbox:latest python -c "x = bytearray(200_000_000)"
# exits with code 137 (OOM killed) ,  check with: echo $?

# Confirm network is actually blocked
docker run --rm --network none agent-sandbox:latest \
  python -c "import urllib.request; urllib.request.urlopen('https://example.com', timeout=3)"
# should raise a connection error, not succeed

If all three behave as described, your sandbox boundary is doing its job.

Next step: pair this with Defending Against Prompt Injection , a locked-down container stops the agent's code from causing damage, but it doesn't stop the agent from being talked into the wrong action in the first place. You need both.