Guide
Advanced Topics
Deeper tooling for sandboxes, file sets, and power-user workflows.
Sandbox
A sandbox is an ephemeral container that runs a single command
and is discarded as soon as it exits. It is the same machinery Overplane uses
internally for agent runs, exposed directly through the
overplane sandbox run command so you can run ad-hoc commands inside
your project's image (or any image) without writing a script or remembering long
docker/podman invocations.
The sandbox abstracts over both Docker and Podman, so the same command behaves identically on either engine. Every run is fresh and isolated: there is no shared state between runs, and the container is removed when the command finishes.
Always-on guarantees
These mounts are applied to every sandbox run:
- The project repository is mounted read-only at
/project. Commands can read your project tree but cannot mutate the host. In a project sandbox, the specs directory is also mounted read-only at/specs. - Two fresh read-write scratch directories are mounted at
/outputand/error. Each run gets new, empty ones; the container's working directory defaults to/output. Files a command writes there are captured when it exits, and--output DIR/--error DIRextract them to host directories after a successful run. - The
overplanebinary is mounted at/usr/local/bin/overplane. The exact binary you invoked is onPATHinside the container, with no image rebuild needed, so tooling that shells out tooverplanejust works.
Running ad-hoc commands
By default, sandbox run uses your project's built image (overplane-<project>:latest) and the runtime configured in
overplane.yaml, so sandbox build must have built the
image first. The command's output is streamed through, and its exit code becomes
overplane's exit code.
# Run a command in the project image. The natural form needs no '--':
# flag parsing stops at the first non-flag token, so '-l' is ls's flag.
overplane sandbox run ls -l
# Use '--' when the command itself starts with a dash.
overplane sandbox run -- some-tool --version
With --image (and optionally --runtime), the
sandbox runs against any image and does not require a project:
overplane sandbox run --image debian:bookworm-slim -- cat /etc/os-release
overplane sandbox run --image alpine --runtime podman -- sh -c 'echo hi' Customizing the environment
There are two layers to customize: the project image,
declared in overplane.yaml under sandbox: and built
with overplane sandbox build, and the individual run, adjusted with sandbox run flags. Image edits take effect on the
next sandbox build; the image is built in cache-friendly
layers, so small changes (an extra package, a new agent version) rebuild
quickly without redoing the whole toolchain.
The sandbox: section
The section is optional: a file without it behaves as if the defaults below
were written out. overplane init generates a fully commented version
of this block. See the
configuration reference for every field and the
image recipes reference for what
each recipe installs.
sandbox:
# Container engine: docker (Buildx/BuildKit 0.13+) or podman (5.x+).
runtime: docker
# Image the sandbox is built from. Its OS family must match setup_recipe.
base_image: debian:bookworm-slim
setup_recipe: debian # debian (apt) or alpine (apk)
# Project-specific OS packages, keyed by package family.
extra_packages:
debian: []
# AI coding agents to install, in install order.
agent_recipes:
- {name: codex, version: latest}
- {name: claude-code, version: latest}
- {name: gemini-cli, version: latest}
- {name: opencode, version: latest}
# Optional capabilities layered into the final image.
feature_recipes:
- {name: playwright}
# Extra host environment variables copied into every run.
env_passthrough: [] Base image and toolchain
setup_recipe: debian (the default, paired with
base_image: debian:bookworm-slim) provisions the image in three
cached layers: base setup (UTC timezone, full apt upgrade, TLS
roots), the developer toolchain, and your project's extra packages. The
toolchain layer installs from official upstream sources where it matters
— Go from go.dev tarballs, Rust via rustup
(stable, minimal profile), Node.js from NodeSource, and uv via pip — plus these common apt packages:
build-essential pkg-config libssl-dev make git ripgrep
rsync jq curl htop procps tini
z3 python3 python3-pip sudo ca-certificates setup_recipe: alpine (paired with base_image: alpine:3) provides best-effort parity using apk: build-base, git,
ripgrep, rsync, jq, htop, procps, tini, z3, python3 with uv, bash, and sudo.
Known gaps versus debian: Go, Node.js, and Rust install from the Alpine
package repositories (musl builds), so their versions track the Alpine
release rather than upstream. All four agent CLIs ship musl-compatible
builds and work unchanged.
Extra OS packages
Add project-specific packages under extra_packages, keyed by
package family; only the family matching your setup_recipe is used.
Extras install in their own image layer, so editing this list never invalidates
the cached toolchain layers:
sandbox:
extra_packages:
debian: [graphicsmagick, sqlite3] # apt names with setup_recipe: debian
alpine: [graphicsmagick, sqlite] # apk names with setup_recipe: alpine Enabling, disabling, and pinning agents
agent_recipes lists the agents baked into the image, chosen from
Overplane's embedded registry: codex (OpenAI Codex),
claude-code (Anthropic Claude Code), gemini-cli
(Google Gemini CLI), and opencode (OpenCode). New projects enable
all four. To disable an agent, remove its entry and rebuild; an empty list (agent_recipes: []) builds a toolchain-only sandbox with no agents. version accepts
an npm dist-tag (latest, preview) or an exact
version; tags resolve on each build and the resolved version is recorded in
the build hash.
sandbox:
# Only Claude Code (pinned) and Codex (tracking latest); the other
# agents are disabled simply by not being listed.
agent_recipes:
- {name: claude-code, version: "2.1.3"}
- {name: codex} # version defaults to latest
Each configured agent's API keys are forwarded from the host automatically
(for example OPENAI_API_KEY/CODEX_API_KEY for codex,
ANTHROPIC_API_KEY for claude-code,
GEMINI_API_KEY for gemini-cli, and OPENROUTER_API_KEY
for opencode) — no env_passthrough entry needed.
Feature recipes
feature_recipes layers optional capabilities into the final image.
Currently the registry offers playwright: the pinned Playwright
CLI plus headless Chromium under /opt/ms-playwright
on debian (distro chromium on alpine, since Playwright publishes no musl browsers).
It is enabled by default in new projects; it is also the slowest layer, so skip
it when you don't need browser testing:
sandbox:
feature_recipes: [] # omit the browser-testing layer entirely Environment passthrough
env_passthrough copies additional host environment variables into
the sandbox, beyond the agent API keys above. Values listed here reach
every container started by overplane sandbox run, so
add secrets deliberately:
sandbox:
env_passthrough: [NPM_TOKEN, GOPRIVATE] Per-run customization
Everything above changes the image; sandbox run flags change a single
run without rebuilding anything:
# Set variables for one run: literal, or copied from the host.
overplane sandbox run --env DEBUG=1 --env-from-host NPM_TOKEN -- make test
# Bind-mount extra host paths (read-write by default; :ro to protect them).
overplane sandbox run --mount ~/datasets:/data:ro -- ls /data
# Network mode, run identity, and working directory.
overplane sandbox run --network none --user 0:0 --workdir /project -- id
# Extract what the command wrote to /output and /error after a run.
overplane sandbox run --output ./out --error ./err -- sh -c 'date > stamp.txt' Structured output
Pass --json to capture the result instead of streaming it. The object
carries the exit code, engine, image, wall-clock duration, optional memory/CPU
usage, and base64-encoded stdout/stderr, so binary
output and separate streams survive intact.
{
"cpu_ms": ..., // omitted when the engine cannot report it
"engine": "docker",
"exit_code": 0,
"image": "overplane-myproject:latest",
"max_rss_bytes": ..., // omitted when unavailable
"stderr": "ZXJyCg==", // base64("err\n")
"stdout": "b3V0Cg==", // base64("out\n")
"wall_ms": 84
}