Skip to content

Agent lab

A disposable Docker sandbox for running CLI coding agents and local models without installing them on the workstation. Its main use in this repository is producing real-install fixtures: an agent runs against a scratch home inside the container, the home is exported as a fake disk image, and the collector reads it in image mode as it would a mounted volume.

Nothing from the host is mounted into the container. The agent's home is a named volume (agent-home, mounted at /home/agent). The container runs as the unprivileged user agent (uid 1000) with every capability dropped, no-new-privileges, a tmpfs /tmp, and limits of 512 processes and 4 GB of memory. The only things that cross the boundary are the API keys you choose to pass in and the export you ask for. The image is node:22-bookworm with git, ripgrep, sqlite3 and Python. It starts in /home/agent/work/sample, a small git repository with a README.md, AGENTS.md and calc.py, so project discovery has a working directory to find.

Requirements

Docker Engine or Docker Desktop with Compose v2. On WSL2, enable Docker Desktop's integration for this distro. The NVIDIA Container Toolkit is only needed for GPU inference in Ollama; lab.sh adds the GPU override when nvidia-smi works on the host and leaves it out otherwise.

Quick start

cd lab
./lab.sh build                      # Claude Code, Codex CLI and Gemini CLI by default
cp .env.example .env && $EDITOR .env  # keys; .env is gitignored
./lab.sh up
./lab.sh run claude                 # work for a few turns, then exit
./lab.sh run codex
./lab.sh collect                    # export, collect, analyze
./lab.sh down                       # keeps the home volume for next time
Command What it does
build [--ollama] Build the agent-lab:latest image; with --ollama, also pull the Ollama image.
up [--ollama] Start the sandbox detached; with --ollama, also the Ollama service.
shell Open bash in the running sandbox as agent.
run CMD [ARGS...] Run one command in the running sandbox, for example run claude.
pull MODEL Pull a model into the running Ollama service.
collect [DIR] Export the home and run the collector and analyzer on it; see below.
down Stop and remove the containers, keeping the volumes.
clean Remove the containers, both volumes and the agent-lab:latest image; see Cleaning up.
help Print the command summary.

Every command but help needs docker and Compose v2 and stops with a message when they are missing. Once the Ollama container exists, build, up, down and clean include the Ollama service without --ollama.

collect needs the sandbox container to exist, not to be running. It writes to DIR, by default lab/out/<UTC stamp>/:

image/        home/agent/..., etc/passwd, root/.ollama (when the Ollama container exists)
collection/   the collector's archive, manifest, summary, log and .sha256
analysis/     timeline.jsonl, sessions.jsonl and inventory.json from the analyzer

The export goes through docker cp, so times, modes and symlinks survive and symlinks are copied as links. The collector runs in image mode with -q against image/. The analyzer runs only when python3 is on the host. When no transcript can be parsed yet, collect says so and still keeps the collection.

Read the manifest for secret: true and skipped_excluded rows, compare what the agent wrote against research/<agent>.md, and lift record shapes from the exported home into analyzer fixtures. The export holds whatever credentials the agents wrote, so lab/out is gitignored. Delete it when finished, and replace any real token in a record lifted into a fixture.

Choosing agents

The image installs npm packages globally, outside the home, so the export holds agent state and no agent binaries. Pick a different set at build time:

NPM_AGENTS="@anthropic-ai/claude-code @github/copilot @qwen-code/qwen-code opencode-ai" ./lab.sh build
PIP_AGENTS="aider-chat" ./lab.sh build

Both variables can also live in lab/.env. Copy .env.example to .env and fill in the keys you need; the file is gitignored and every variable in it is passed into the sandbox. Keys already exported in your shell (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, GH_TOKEN, OPENROUTER_API_KEY) pass through as well. Use throwaway keys. OAuth device-code logins work from the container; browser-redirect logins generally need a published port.

Electron tools such as Cursor, Windsurf, Antigravity and VS Code need a display and are out of scope here. Validate those from source.

Local models

./lab.sh up --ollama
./lab.sh pull qwen2.5-coder:7b
./lab.sh run aider --model ollama/qwen2.5-coder:7b

OLLAMA_HOST inside the sandbox already points at the Ollama service. Models live in a named volume, so the host never grows a ~/.ollama. collect exports Ollama's state under image/root/.ollama, which exercises the collector's model-blob exclusions against real weights.

What this does not test

A container home is not a host account. Live-mode user enumeration is not exercised; use a throwaway WSL distro or VM for that. The layout is Linux only, so macOS Library and Windows AppData paths still need validation from source.

Docker on WSL2 shares the kernel with the distro. The sandbox is a good guard against clutter and accidental damage and a reasonable one against a hostile tool, not a substitute for a VM when you are running something you distrust. The container has unrestricted outbound network; Anthropic's devcontainer reference for Claude Code shows an allowlisting firewall if you need one.

Cleaning up

./lab.sh down                     # stop, keep volumes
./lab.sh clean                    # remove containers, volumes and image; lab/out stays
rm -rf out                        # the exports, once you have what you need

clean runs docker compose down -v --rmi local, then removes the agent-lab:latest image with docker image rm when it exists, since --rmi local skips images with a custom tag and compose.yaml tags the sandbox image. The pulled ollama/ollama image stays; remove it with docker image rm ollama/ollama:latest if you want the space back.