Appearance
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.

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

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
| Option | Good for | Costs you | When to pick it |
|---|---|---|---|
| Plain Docker, locked down | Most agent code-execution use cases | A few extra flags to remember | Default 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 code | More setup, slower startup, extra infra to run | Multi-tenant products where strangers' code runs on your infra, or compliance requires it |
| No sandbox (direct exec on host) | Nothing you should ship | Your machine | Never 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| Flag | What it stops |
|---|---|
--rm | Anything the agent wrote inside the container disappears the moment it exits, no leftover artifacts to clean up or leak. |
-m 512m | Caps memory. Without this, a runaway loop can consume all your host's RAM and take down everything else running on the machine. |
--cpus 1 | Caps CPU, so a busy-loop can't starve the rest of your system. |
--network none | The 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-only | The 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
| Mistake | How you notice it | The fix |
|---|---|---|
| Container runs as root | You didn't set USER appuser in the Dockerfile | Add a non-root user; rebuild the image |
| Agent's runaway process kills your machine | Host slows to a crawl or OOM-kills unrelated processes | Add -m and --cpus limits, you should never run agent code without them |
| Agent "phones home" or hits an API you didn't expect | Unexpected outbound traffic in docker network inspect | Run 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 one | Agent behaves differently between runs with no code change | Add --rm so every run starts from a clean container |
| Output written somewhere you didn't expect | Files show up outside your intended workspace folder | Add --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 succeedIf 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.