Tooling & Integration

Sandboxing Agent Environments for Reproducibility, Not Just Safety

David Guzenburg/ / 8 min read

An agent that installs a package to fix a build has changed your machine. Do that thirty times and your environment is somewhere nobody can reproduce.

containerssandboxingreproducibilitydeveloper experience

Isolation you actually need

The security case for sandboxing agents is covered elsewhere in this series. This article is about the operational one, which is separate and often more persuasive to the people who have to approve the work: a sandboxed agent is easier to run, easier to reproduce and easier to clean up after.

Three practical problems it solves, none of them about attackers.

The objection you will hear

The first response to any sandboxing proposal is that it will slow people down. It is worth taking seriously rather than dismissing, because it is usually true of a badly built sandbox and it is the reason most attempts get abandoned.

The friction is concentrated in three places, and each has a fix: cold start (cache volumes and a pre-built image), missing tools (put them in the image rather than making people install them), and awkward invocation (one script, no arguments). Address those and the objection disappears, because the sandbox stops being slower than not using one.

What does not work is mandating it while leaving the friction in place. People will run agents outside the sandbox, quietly, and you will have a policy that describes something nobody does.

Four levels, and what each costs

LevelIsolatesStartupGood for
Same machine, separate userHome directory, credentialsNoneSolo work on your own code
ContainerFilesystem, processes, networkSecondsThe default for most teams
Dev container / devboxAs above, plus a declared toolchainSeconds, after a buildTeams wanting reproducibility
Disposable VMKernel tooTens of secondsUntrusted code, unreviewed servers

Level two is the right default. Level four is for the specific case of running something you have not reviewed — an unknown dependency, a new MCP server — where a container escape is a realistic concern rather than a theoretical one.

A working container setup

FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
      git ripgrep jq curl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

RUN useradd -m -u 1000 agent
USER agent
WORKDIR /work

# Toolchain pinned in the image, not installed at runtime
COPY --chown=agent requirements-dev.txt /tmp/
RUN pip install --user --no-cache-dir -r /tmp/requirements-dev.txt

ENV PATH="/home/agent/.local/bin:${PATH}"

The pinned toolchain is the part that makes this reproducible. An agent that cannot install things is an agent whose environment matches everyone else's, which removes an entire category of "works for me".

#!/bin/bash
# One isolated session against the current worktree.
set -euo pipefail

NAME="agent-$(basename "$PWD")-$$"

docker run --rm -it \
  --name "$NAME" \
  --mount type=bind,src="$PWD",dst=/work \
  --workdir /work \
  --network agent-net \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --memory 4g --cpus 2 --pids-limit 512 \
  --tmpfs /tmp:rw,noexec,nosuid,size=512m \
  agent-sandbox:latest "$@"

Resource limits are doing operational work here, not security work. An agent that spawns an unbounded number of processes, or a test suite that consumes all available memory, degrades the whole machine. --pids-limit and --memory keep a runaway session from taking your laptop with it.

The mounts to argue about

Every mount is a decision, and the pressure is always toward adding more because each one fixes something real.

MountVerdict
The working directoryRequired
A named volume for package cachesFine, and a large speed win
~/.gitconfig, read-onlyFine — name and email only
~/.sshNo. Use a scoped deploy key as a file
~/.aws, ~/.config/gcloudNo. Inject a short-lived token as an env var
The Docker socketNo. This is root on the host
The whole home directoryNo, and it is worth asking why it was proposed

The cache volume in row two deserves a mention because it removes the main argument against containers. Without it, every session re-downloads dependencies and people abandon the sandbox out of impatience. With it, startup is fast enough that nobody notices.

docker volume create agent-pip-cache
# then, in the run command:
#   --mount type=volume,src=agent-pip-cache,dst=/home/agent/.cache/pip

What breaks when you containerise

Three things reliably stop working on the first attempt, and knowing them in advance saves an afternoon.

SymptomCauseFix
Files owned by root on the hostContainer runs as root; bind mount preserves UID --user "$(id -u):$(id -g)"
Git refuses to operate on the mountOwnership mismatch triggers safe.directory Match the UID, or add the path to safe.directory in the image
Agent cannot reach the model APIBridge network without DNS or egress Allow the provider host explicitly on the network policy

The first is the one that causes lasting annoyance, because you discover it later when something outside the container cannot write to files the agent created. Setting the UID at the start avoids the whole class.

Networking

A user-defined bridge network, never --network host. Host networking exposes every service bound to localhost — frequently unauthenticated, precisely because they are local-only.

If the agent needs a local service, put that service on the same bridge network and reach it by container name. Slightly more setup, and it keeps the boundary intact rather than dissolving it for convenience.

Devcontainers, briefly

If your team already uses the devcontainer standard, you have most of this and should reuse it rather than building a parallel setup.

{
  "name": "agent",
  "build": { "dockerfile": "../Dockerfile.agent" },
  "remoteUser": "agent",
  "runArgs": ["--cap-drop=ALL", "--security-opt", "no-new-privileges",
              "--memory=4g", "--pids-limit=512"],
  "mounts": [
    "source=agent-pip-cache,target=/home/agent/.cache/pip,type=volume"
  ],
  "containerEnv": { "AGENT_SANDBOX": "1" }
}

The advantage is not technical, it is organisational: one definition serves both human and agent sessions, so it stays maintained. A separate agent-only image drifts, because nobody uses it daily and nobody notices when it breaks.

AGENT_SANDBOX=1 is worth including. It gives scripts a way to detect the environment — useful for refusing operations that should never run inside a sandbox, or for adjusting output verbosity.

Making it the path of least resistance

A sandbox people bypass is not a sandbox. The determining factor is almost always startup time and friction, not policy.

Get those right and the sandbox becomes the easy option, at which point nobody has to be persuaded to use it. That is the only version of this that survives contact with a deadline.

Takeaway

Sandbox for reproducibility and cleanup, not only for security — it is the argument that persuades. Pin the toolchain in the image, set resource limits so a runaway session cannot take the machine, mount caches so startup is fast, and refuse the four mounts that dissolve the boundary. A sandbox people bypass is not a sandbox.

Keep reading
Codex vs Claude

An Open Client and a Closed Model: Four Things the Licence Buys

A permissive licence on an agent CLI is worth reading the prompt assembly, auditing the sandbox code, embedding without procurement, and a fork you should probably not take. It buys nothing about the model service, its pricing or its terms.

Codex vs Claude

Local Execution: It Worked in the Session Is the New Works on My Machine

The correctness half of running an agent in your own shell: undeclared toolchains, aliases that rewrite commands, interactive prompts with no answer, and accumulated session state. What to declare in the repository and what to leave ambient.

Security Engineering

Shell Execution and Blast Radius: What an Agent Can Actually Reach

Granting shell access grants everything the shell reaches. What that includes on a typical laptop, four levels of containment, and why command allow-lists don't hold.

Tooling & Integration

Build System Integration: Your Toolchain Is the Agent's Feedback Loop

Why build quality bounds agent output, four properties that matter, the failure modes specific to agents, and why you should verify your check command actually fails on failure.

← Reviewing Agent-Authored Code: The Author Cannot Answer Questions  ·  Types as the Contract: A Decision Procedure Downstream of a Generator →

All tooling & integration articles  ·  Every article