Runner Image
agents-sandbox builds a Docker image for each sandbox. The image contains the selected coding agent, Node.js, and common CLI tools. Projects can extend the image with their own tooling.
The image is built to rebuild fast and stay lean. Its tools — the base CLI tools, Node, the selected agent, and the
optional Docker engine — live in separate build stages whose layers are reused unchanged, so editing your project’s
Dockerfile body does not re-run the tool installs, and the same tool layers are shared across every project on the machine. Node
is present only for the agent that needs it, and the image build’s downloads need no curl or tar from your base — they
run on the tool base.
Building and managing images
Build the image:
agents-sandbox build # builds if image is missing or base changed
agents-sandbox build -r # force rebuild
Preview the exact Dockerfile that would be built (without invoking docker):
agents-sandbox build dockerfile # default agent, no docker
agents-sandbox build dockerfile --docker # with the Docker engine block
agents-sandbox build dockerfile --agent-version 1.2.3 # pin the agent version in the preview
List cached images:
agents-sandbox image list
The image name includes the checkout slug and Docker image hash, so changes to the Dockerfile automatically trigger a rebuild. When the resulting image digest differs from the image the existing project VM was booted from, the VM is recreated on the next run so it uses the new image (the home volume is preserved).
Runner images are tagged per agent. The -latest tag is namespaced by the agent, so each agent’s image can be
identified and updated independently. The Docker tag keeps the agents-sandbox/runner- prefix so it stays
distinguishable in the system Docker daemon, while the microsandbox image reference is prefix-free:
Docker tag: agents-sandbox/runner-<slug>:<agent>-latest
microsandbox reference: <slug>:<agent>-latest
For example, with a checkout slug of my-project, the opencode image is tagged
agents-sandbox/runner-my-project:opencode-latest and referenced in microsandbox as my-project:opencode-latest; the
pi image is tagged agents-sandbox/runner-my-project:pi-latest and referenced as my-project:pi-latest.
An image loaded by an earlier version sits under the old prefixed microsandbox reference
(agents-sandbox/runner-<slug>:<agent>-latest). agents-sandbox reuses that reference whenever it still holds the
current content, so an upgrade never reimports an unchanged image; changed content is imported under the new prefix-free
reference, silently migrating the project.
Skip-build when unchanged
The Docker build is skipped when the baked org.agents-sandbox.dockerfile-id label matches the current content. This
label is a hash of the rendered Dockerfile, the build-context files it installs (the /etc/profile.d PATH merge
script and the CA-trust helper), and the agent version, so an image already built from the exact same inputs is reused
instead of being rebuilt.
Each tool stage pins its version as an ARG default in the rendered Dockerfile (NODE_VERSION, the agent’s
<AGENT>_VERSION, DOCKER_VERSION). The version is therefore part of the build content, so bumping it changes the
build identity and invalidates that stage’s install layer — the agent layer re-runs on an agent upgrade while the node
and Docker layers stay cached — regardless of how the builder tracks version build arguments.
With --log-level verbose, the launcher reports why a build happens on every rebuild path. Before building, each image
also records its individual inputs as org.agents-sandbox.identity.* labels (project Dockerfile hash, agent version, and
Docker-in-Docker), so the verbose line names the changed input — for example rebuilding runner image
…: project Dockerfile changed; agent version changed — or forced by --rebuild / runner image not present locally.
A reuse logs reusing runner image …: content identity unchanged; an image without these identity labels (built by an
older version) is reported as a generic content change.
Content-verified load
Before skipping a load, EnsureLoaded verifies the microsandbox cache content — the config digest (compared against
the Docker image ID) or the dockerfile-id label. If the cached content no longer matches, the image is reloaded so the
VM always boots from the correct image. At verbose level the load states whether the image was absent from the cache or
its cached content was stale.
The load resolves the reference it uses: when the legacy prefixed reference already holds the current content, that reference is kept (no reimport); otherwise the Docker image is loaded under the prefix-free reference. Every following VM creation, label read and log line uses the resolved reference.
Pruning images
Images can be pruned via the prune command or, more targeted, the image prune subcommand:
agents-sandbox image prune --dry-run # see what's stale
agents-sandbox image prune # actually remove them (prompts; or pass --yes)
See Pruning for the confirmation prompt, --yes, and --dry-run behavior.
Pruning retains the -latest images per agent per live project and any image a kept VM still references, reclaiming
every other ref — pre-redesign digest refs no sandbox uses, orphaned surplus images, and every ref of projects (slugs)
that no longer have a live VM. It also runs microsandbox’s native image-data prune so manifests and layers orphaned by a
runner-image refresh are reclaimed after no VM references them. See Commands for details on
the
prune command.
agents-sandbox also auto-prunes all resources that are ephemeral, unused or haven’t been in use for more than 30 days by default. Cached runner images and home volumes are only pruned once they are older than the threshold, so a recently used project keeps its image and home state across restarts. See Sandboxes for more information and Configuration for how to configure auto-pruning.
Home directory
When rebuilding an image, all state in the VM’s home directory (/home/dev/) would get lost. Therefore, the home
directory is copied to a volume and mounted to the running VM, so it survives a rebuild.
When an image change actually leads to a rebuild, agents-sandbox prompts you to keep, migrate, or reset that volume;
see Home volumes for the choices and the non-interactive default. volume
migrate, volume reset, and volume edit remain available for manual management.
When a rebuilt image uses the same runner-image tag, agents-sandbox refreshes the microsandbox cache by loading the new Docker image directly under that tag. It does not remove the old cached manifest first, because a stopped project VM can still reference it. Unused image data is reclaimed later by image pruning.
One image per checkout
agents-sandbox builds a single runner image per checkout. The rendered Dockerfile is composed of per-tool multistage
stages with fixed parents, merged into the final stage with COPY after the user body:
agents-sandbox-base— the embeddeddebian:trixie-slimtools block for a managed base, or your whole custom base (FROMimage) for a custom base. For a managed base (FROM .../runner-base...), the final stage’sFROMis replaced in place with the embedded tools block; for a custom base, any earlier build stages in your project Dockerfile are preserved before it — so multi-stage project Dockerfiles are supported.agents-sandbox-toolbase— the internal stage the node, agent, and docker stages build on: a pinneddebian:trixie-slimpluscurl,ca-certificates, andtar. It is not part of the runner’s lineage, so your base needs none of those binaries and the tool layers cache across bases.agents-sandbox-node(only for agents that need node) — installs Node.js into/opt/agents-sandbox. It is emitted only when the selected agent requires node (today onlypi, an npm-installed node bundle);opencode,opencode2, andclaude-codeinstall native binaries and their images contain no node/npm.agents-sandbox-agent(conditional) — installs the selected agent into/opt/agents-sandbox. It starts from the node stage when one was emitted, otherwise from the tool base, and creates/opt/agents-sandbox/binitself. npm agents (pi) install withnpm install -g --prefix /opt/agents-sandbox <pkg>@$VERSION; the rest copy the binary produced by their upstream installer into/opt/agents-sandbox/bin.agents-sandbox-docker(optional) — installs the Docker static binaries into/opt/agents-sandbox/bin.agents-sandbox-runner—FROM agents-sandbox-base, creates thedevuser, runs your Dockerfile body without the tool trees available, thenCOPYs the tool trees in.
Each tool layer caches independently of the user body, the base, and each other. Editing the user body no longer re-runs
the Node/Docker/agent installs, the same tool layers are reused across projects on one machine, and a base that already
provides a tool is left to that base (the runner’s PATH prefers base binaries).
Every tool lands under /opt/agents-sandbox; its binaries are exposed from /opt/agents-sandbox/bin, which the runner
appends to PATH (append, so a base-provided binary keeps precedence). The agent attach and the interactive shell run
/bin/bash -l, and /etc/profile on Debian and most distributions resets PATH, so the appended
/opt/agents-sandbox/bin (and any project ENV PATH entry) would not survive a login shell. To keep the composed PATH
effective there, the image records its composed PATH in the AGENTS_SANDBOX_IMAGE_PATH environment variable and installs
/etc/profile.d/agents-sandbox-path.sh. /etc/profile sources /etc/profile.d/*.sh after its PATH reset, so the script
re-appends any AGENTS_SANDBOX_IMAGE_PATH entry that is missing from the reset PATH (existing entries keep their
position; nothing is duplicated). mkdir -p /opt/agents-sandbox/bin always runs so the COPY source exists. The image
always ends with USER dev and WORKDIR /workspace.
The runner also installs microsandbox’s application certificate trust helper as /usr/local/bin/msb-trust. It is for
applications whose TLS trust is separate from the guest’s system store — a JDK’s cacerts, Chrome/Chromium’s NSS
store — which matters as soon as a secret turns on TLS interception. The helper is Apache-2.0 work, so its license is
installed at /usr/share/licenses/msb-trust/LICENSE. See
Secrets for which applications already work, and
the Java recipe for a worked example.
The base image contract
By default the base tools block starts from debian:trixie-slim and installs the recommended CLI tools: git,
ripgrep, jq, yq, curl, wget, xz-utils, file, gawk, less, lz4, moreutils, net-tools, parallel,
recode, uuid, and iptables.
A project Dockerfile whose FROM is any other image is treated as a custom base: your final FROM becomes the base
stage the runner builds on, while the node, agent, and optional docker stages build on the tool base, independent of your
image. For a custom base, the earlier build stages in your project Dockerfile are kept before the base stage. The custom
base must meet these requirements:
- shadow-utils providing
groupadd,useradd, andusermod(used to create thedevuser and docker group). - A POSIX shell — the
devuser’s login shell is set to the first ofbash,zsh,shfound, falling back to/bin/sh. - A login profile that sources
/etc/profile.d/*.sh— the runner installs/etc/profile.d/agents-sandbox-path.shto restore the composedPATHin login shells, so a base whose profile does not source/etc/profile.dwill not get the mergedPATH. - For Docker only: the runtime prerequisites
iptables,git,ps, andxz. These are checked after your Dockerfile body runs, so they may be installed in the body or an earlier stage. If one is missing, the build fails and names the missing package. The engine download itself runs on the tool base, so your image needs nocurlortar. - For the daemon agents (
opencode,opencode2): the in-VM tools the launcher uses —curl,nohup, andpkill.curlreaches the daemon (the health probe, session and worktree calls, and notifications),nohup/pkillstart and stop it. These are checked after your Dockerfile body runs, so they may be installed in the body or an earlier stage; if one is missing, the build fails and names the tool.piandclaude-coderun no daemon and need none. - The recommended CLI tools listed here are documented for your convenience, not as requirements — as a custom base you install your own.
- A base that already provides docker, node, or the agent is left alone (idempotency), and a pre-created
devuser is tolerated.
Bases that do not provide these out of the box are not supported without a project Dockerfile that installs
them — for example Alpine (busybox adduser/addgroup, no shadow-utils or bash) and distroless/ scratch
(no shell or package manager).
When a base image already contains the selected agent, the image records the agent as user-provided and
agents-sandbox does not manage its upgrades. This works by PATH precedence, so a base-provided tool must live
outside /opt/agents-sandbox: that prefix is the tool-owned one, and the runner’s tool COPY overwrites any
same-named file a base placed inside it. If that provenance has no recorded version and a later rebuild no longer
contains the agent, the image builder resolves a real release version before installing it; user-provided is
provenance
metadata, not an agent release version. A matching user-provided image is reused without resolving a release first.
User context
The dev user (host UID/GID) is created as the first instruction of the final stage, and the image always ends with
USER dev active. If the host GID is already taken by another group in the base image (e.g. macOS staff/20 vs
dialout), the dev group is created with the next free GID. Your Dockerfile body runs in the final stage, so it may
switch to USER dev — but earlier build stages in a multi-stage Dockerfile run as root:
FROM debian:trixie-slim
USER root
# Install your project's toolchain as root, e.g. via apt install
RUN apt-get update && apt-get install -y python3 && rm -rf /var/lib/apt/lists/*
ENV configuration
ENV definitions in Dockerfiles are applied to running sandboxes. If you need to configure e.g. PATH, just set
ENV PATH=...: in your Dockerfile. The image’s composed PATH is restored into login shells by the
/etc/profile.d/agents-sandbox-path.sh merge, so a project ENV PATH entry stays effective in shell and in !
commands, not only in non-login agent-executed commands.
Docker-in-Docker
Enable the Docker engine (Docker-in-Docker) in the runner image with the --docker flag (on build, run, shell, or
build dockerfile) or the docker: true config key, or the AGENTS_SANDBOX_DOCKER environment variable. The
deprecated aliases --dind, the dind config key, and AGENTS_SANDBOX_DIND still work with lower precedence and print a
one-time warning. A project Dockerfile still starting FROM .../runner-base-dind:latest keeps working and implies it, but
warns on every run that ensures the runner image (including cached reuse); the newer FROM .../runner-base-docker:latest
implies it silently.
The Docker stage installs the engine from a docker static binary tarball. The vfs storage driver is forced inside
the VM at start-up — the image never writes /etc/docker/daemon.json, so your edits to it are preserved. If your
daemon.json already sets "storage-driver": "vfs", agents-sandbox leaves the file alone and starts dockerd without
the flag (passing both makes dockerd refuse to start); if it sets any other driver, start-up fails with an error, because
the microsandbox filesystem requires vfs. buildx and docker compose are not installed — install them in your
project Dockerfile if you need them. The static tarball’s platform is selected by uname -m (x86_64/aarch64).
With a custom base, a thin COPY --from=agents-sandbox-docker plus an adopt step is appended after your Dockerfile body
by default, so steps after it see docker (the heavy download stays in the cacheable Docker stage). To run your own steps
immediately after the engine is installed, place a # agents-sandbox:docker comment line in the final stage where the
block should be injected (the legacy # agents-sandbox:dind marker is still recognized, but warns on every ensure):
FROM ubuntu:24.04
# The Docker runtime prerequisites are checked after this body runs, so install
# them here. The engine download runs on the tool base, so curl/tar are not
# needed in your image.
RUN apt-get update && apt-get install -y iptables git procps xz-utils && rm -rf /var/lib/apt/lists/*
# agents-sandbox:docker
# Steps that need docker, e.g. buildx and docker compose
RUN install -Dm755 /usr/local/bin/docker-buildx /usr/libexec/docker/cli-plugins/docker-buildx
Node and the agent
The agents-sandbox-node stage installs Node.js (v24.21.0, official tarball) into /opt/agents-sandbox, and it is
emitted only when the selected agent requires node — today only pi. The agents-sandbox-agent stage installs the
selected agent into /opt/agents-sandbox. Both build on the tool base, so they do not depend on your base; whether the
base already provides node, the agent, or dockerd is resolved after the user body by the /etc/agents-sandbox/agent-source
and /etc/agents-sandbox/docker-source provenance files (tool | user), and a base-provided binary wins over the tool’s
copy (the runner’s PATH prefers base binaries). The pinned node stays on the v24 line:
the v26 tarballs gained a hard libatomic.so.1 runtime dependency the managed base does not provide, while v24 has no
such dependency on either target architecture, so a custom base needs no extra library to run the tool-installed node.
Four agents are built in: opencode, opencode2 (default, the opencode 2 beta), pi, and claude-code. Only pi
needs node: its published package is an npm-installed #!/usr/bin/env node bundle, so it installs through
npm install -g --prefix /opt/agents-sandbox @earendil-works/pi-coding-agent@$PI_VERSION. The other three ship
self-contained native binaries and install through their upstream installers — opencode and opencode2 via
https://opencode.ai/install and https://opencode.ai/v2/install, claude-code via https://claude.ai/install.sh —
copying the binary into /opt/agents-sandbox/bin; their images contain no node/npm. All four resolve their latest
version for an unpinned build — opencode via its GitHub releases endpoint, opencode2 via the opencode update API
(/update/api/latest/cli/npm, the installer’s own source), pi via pi.dev, and claude-code via the npm registry’s
latest dist-tag — as a host-side HTTP query, never
inside the image. JS-based LSP servers are expected to run on the agent’s embedded JS runtime (Bun) or on node that a
project installs in its own Dockerfile.
Labels and provenance
Each runner image carries these labels:
org.agents-sandbox.managed=trueorg.agents-sandbox.agent=<name>org.agents-sandbox.base=<ref>@sha256:<digest>
There is no tool-version label.
Upgrades
The agent version is pinned at image build time and the agent’s runtime autoupdate is disabled per agent, so the agent binary in a sandbox is stable across runs. Instead of reading the version from an image label, the version is detected on first boot and recorded for upgrade checks.
By default the latest release available at build time is used for agents installed by agents-sandbox. Pin an explicit
version on the build command with --agent-version:
agents-sandbox build --agent-version 0.5.0
agents-sandbox build # uses the latest release
How to upgrade: Rebuild the runner image with agents-sandbox build, or pass --agent-version <version> to pin a
release. On run/shell, when a newer agent release exists than the version baked into the image, the launcher offers to
rebuild the image (interactive) or prints a notice advising agents-sandbox build (non-interactive). The prompt names both
the available release and the version currently baked into this project’s image, so you can tell exactly what a rebuild
would change. When no version has been recorded for the project yet (for example after upgrading agents-sandbox), the
launcher reads it once from the project’s existing image, so an unpinned run still asks before changing the image and an
offline check keeps the existing image instead of rebuilding. --rebuild forces a clean
rebuild but still offers the upgrade; it only bypasses the build cache, it does not pin the version. When agent-source=user, the
tool never checks for upgrades.
--agent-version applies when agents-sandbox installs the selected agent. It does not replace an agent already supplied
by a custom base or project Dockerfile — for a user-provided agent the pin is ignored. If a user-provided agent has no recorded
version, a rebuild that needs to install the missing binary resolves a normal release version rather than passing the internal
provenance value to the installer.
--agent-version is available on build, run, and shell. On run/shell it pins the version baked into the image and
skips the upgrade check. The deprecated --opencode-version alias remains build-only.