agent-sandbox

July 6, 2026 · View on GitHub

OCI image for running AI coding agents — Claude Code, Codex, Gemini CLI, OpenCode, and pi-agent-rust — inside a hardened, reproducible environment with three runtime options: rootless Podman, microsandbox microVMs, or Docker.

Quick start

No init needed. Persistence is automatic out of the box.

Podman (preferred default — rootless, hardened):

just build && just claude

microsandbox (microVM isolation, secret scoping, network policy):

just build && just msb-claude

Docker:

just build && just docker-claude

Running

microsandbox

microsandbox provides hardware-level VM isolation with millisecond boot times. API keys are scoped to their provider endpoints. Network is public-only by default (blocks private/loopback ranges).

TargetNetworkUse case
just msb-claudepublic-onlyDefault — safe for most work
just msb-claude-openallow-allAgents that need local services
just msb-claude-offlinenoneAir-gapped code review
just msb-codexpublic-onlyOpenAI Codex
just msb-pipublic-onlypi-agent-rust
just msb-pi-openallow-allpi-agent-rust with local services
just msb-pi-offlinenonepi-agent-rust air-gapped
just msb-shellpublic-onlyInteractive shell
just msb-shell-openallow-allShell with local network access

Lifecycle:

just msb-status          # List running sandboxes
just msb-stop            # Stop the named sandbox
just msb-reset           # Destroy it (irreversible)
just msb-exec -- <cmd>   # Run a command in the running sandbox

See docs/network-policy.md for details on network policies.

Podman

just claude              # Claude Code
just codex               # OpenAI Codex
just pi                  # pi-agent-rust
just shell               # Interactive zsh

Runs with --cap-drop=ALL --read-only --security-opt=no-new-privileges automatically.

The image runs as agent UID/GID 1000 by default. The Podman recipes also use --userns=keep-id:uid=1000,gid=1000 --user agent, which maps your invoking host user to agent inside the container while preserving the image's USER agent behavior. Bind-mounted files should be owned by you on the host. Disable or replace the user namespace mapping with PODMAN_USERNS=...; for example, PODMAN_USERNS= just claude.

Docker

just docker-claude       # Claude Code
just docker-codex        # OpenAI Codex
just docker-pi           # pi-agent-rust
just docker-shell        # Interactive zsh

Same hardening as Podman, minus SELinux labels and Podman user namespace mapping. Docker does not remap UIDs by default, so files created through bind mounts may appear as UID/GID 1000 on the host unless your host account also uses 1000.

For a Docker-local image whose agent UID/GID matches the invoking user:

just docker-build-user
just docker-claude

just docker-build-user overwrites the local IMAGE:IMAGE_TAG tag, defaulting to agent-sandbox:latest, so the Docker recipes use the rebuilt image automatically.

Env var overrides

VariableDefaultPurpose
CONTAINER_RUNTIMEpodmanRuntime: podman, docker, or msb
IMAGEagent-sandboxImage name
IMAGE_TAGlatestImage tag
REGISTRYghcr.io/butterflyskiesRegistry for push/pull
HOME_VOL./homeExternal home directory (optional)
AGENT_HOME_BOOTSTRAP1Entry-point home-template seeding; set 0 to disable
AGENT_HOME_TEMPLATE/opt/agent-home-templateTemplate path used by the entrypoint
PODMAN_USERNSkeep-id:uid=1000,gid=1000Podman --userns value; empty disables
MSB_CPUShost/2CPU count for msb
MSB_MEMORYhost/2Memory for msb
MSB_NAMEagent-sandboxNamed sandbox for msb
MSB_NETWORK_POLICYpublic-onlymsb network: public-only, allow-all, none

Example:

MSB_NAME=my-project just msb-claude
HOME_VOL=/data/agent-home just claude
PODMAN_USERNS= just claude
IMAGE_TAG=20250418 just docker-claude

Entrypoint

The image uses /usr/local/bin/agent-entrypoint, a small zsh dispatcher:

InvocationBehavior
podman run agent-sandboxRuns claude
podman run agent-sandbox --versionRuns claude --version
podman run agent-sandbox shellRuns zsh
podman run agent-sandbox shell -lc 'echo ok'Runs zsh -lc 'echo ok'
podman run agent-sandbox codexRuns codex
podman run agent-sandbox zshRuns zsh

Persistence

Three modes — choose based on your workflow. See docs/persistence.md for details.

1. Built-in (default — no setup required)

microsandbox: The named sandbox (--name agent-sandbox) keeps /home/agent alive across msb exec calls. No volume needed. Files persist until just msb-reset.

Podman/Docker without HOME_VOL: Container is not run with --rm, so it persists after exit. Re-attach with podman start -ai <id> or just run again.

2. External volume — image home template (just init-home)

Extracts the image's /opt/agent-home-template into a local directory. Shell config, .tool-versions, asdf runtimes, npm globals, and home-level defaults are copied so replacing /home/agent with a volume does not hide the staged tools.

just init-home           # Extracts to ./home (errors if non-empty)
just init-home /data/my-home   # Custom path

Then edit identity files:

vi home/.gitconfig
cp ~/.config/gh/hosts.yml home/.config/gh/

Run with the volume:

HOME_VOL=./home just claude

The entrypoint also seeds missing files from /opt/agent-home-template into a mounted $HOME on startup using rsync --ignore-existing. Existing files in your volume are left alone, except the image-owned home .tool-versions is refreshed so asdf selects versions installed by the current image. Set AGENT_HOME_BOOTSTRAP=0 to disable this bootstrap.

3. Skeleton volume (just init)

Creates a minimal directory structure — no tool copies from the image, just the folders. Lighter weight; bring your own dotfiles.

just init                # Creates ./home with the skeleton layout

When to use each:

ModePersistenceSetupBest for
Built-in (msb)Until msb-resetNoneDaily use with microsandbox
Built-in (container)Until container removedNoneQuick one-off sessions
init-homeVolume-backedOne init-homePortable home, image dotfiles layer
init (skeleton)Volume-backedManual setupMinimal, BYO everything

Security

microsandbox

Secret scoping — API keys are bound to their provider's domain. A key leaking from one agent cannot be used against another provider:

KeyAllowed host
ANTHROPIC_API_KEYapi.anthropic.com
OPENAI_API_KEYapi.openai.com
GEMINI_API_KEYgenerativelanguage.googleapis.com
GOOGLE_API_KEY*.googleapis.com

Violations are blocked and logged (--on-secret-violation block-and-log).

Network policies — default public-only blocks private ranges (RFC 1918), loopback, and link-local. Use msb-claude-open for local service access or msb-claude-offline for no network. See docs/network-policy.md.

Resource auto-scaling — msb allocates host/2 CPUs and host/2 memory automatically. Override with MSB_CPUS / MSB_MEMORY.

Podman / Docker hardening

All just recipes apply these flags automatically:

FlagEffect
--cap-drop=ALLDrop all Linux capabilities
--security-opt=no-new-privilegesPrevent setuid/setgid escalation
--read-onlyImmutable root filesystem
--tmpfs /tmp,/var/tmp,/runWritable scratch space only

The container process runs as the non-root agent user. In the default image, agent is UID/GID 1000. Rootless Podman recipes additionally pass --userns=keep-id:uid=1000,gid=1000 --user agent so that bind-mounted files are written as your host user while the process remains agent/1000 inside the container.

See docs/uid-mapping.md for notes on UID mapping with bind mounts.

What's in the box

CategoryContents
AI agentsClaude Code (native), Codex, Gemini CLI, OpenCode, pi-agent-rust
Sandboxmicrosandbox (msb) CLI + runtime
RuntimesNode.js 24 LTS, Python 3.14, Go, Java (GraalVM 21), Ruby 4, Zig, Bun (via asdf)
Build toolsRust (stable) + cargo tools, Gradle, Maven, pnpm, uv, cmake, ninja, meson
VCSgit, git-lfs, git-crypt, jj (Jujutsu), gh CLI
CloudGoogle Cloud CLI (gcloud), AWS CLI v2 (aws), Azure CLI (az)
Shellzsh + zinit + starship + fzf-tab + atuin + zoxide + direnv
Editorsneovim (default), nano
CLIripgrep, fd, bat, eza, fzf, jq, just, hyperfine, tokei, bottom, dust, chezmoi, step-cli

Building & versioning

just build                          # Local build, tags :latest (or $IMAGE_TAG)
just docker-build-user              # Docker-local rebuild with agent UID/GID matching you
just release                        # CalVer build: tags :latest + :YYYYMMDD, pushes both
just push                           # Push current tag to $REGISTRY
REGISTRY=ghcr.io/myorg just release # Push to a custom registry

OCI labels stamped at build: org.opencontainers.image.version, org.opencontainers.image.revision, org.opencontainers.image.created.

Local rebuilds benefit from cargo cache mounts (--mount=type=cache) and Podman layer cache (--layers) automatically.

Supply chain security

All tool installations use pinned versions with SHA256 checksum verification where possible.

ToolInstall methodPinnedIntegrity check
Rust toolchainDirect binary downloadrustup-init SHA256sha256sum verify
Cargo crates (16)cargo install --locked @versionExact versionscargo lockfile
uvGitHub release tarballVersion + SHA256sha256sum verify
chezmoiGitHub release binaryVersion + SHA256sha256sum verify
zoxideGitHub release tarballVersion + SHA256sha256sum verify
opencodeGitHub release tarballVersion + SHA256sha256sum verify
pi-agent-rustGitHub release tarballVersion + SHA256sha256sum verify
microsandboxGitHub release tarballVersion + SHA256sha256sum verify
Claude CodeVendored installer scriptExact versionInstaller verifies binary SHA256 from manifest
Codex, Gemininpm install -gExact versionsnpm registry signatures
gh, eza, step-cliapt with signed reposDistro package versionGPG-signed apt repos
gcloud, azapt with signed reposDistro package versionGPG-signed apt repos
AWS CLI v2Direct zip downloadVersion + SHA256sha256sum verify
asdf pluginsasdf-plugin-managerGit SHA pinnedExact commit checkout
asdf runtimesasdf installExact versionsPlugin-specific verification

Remaining risks

  • npm package dependency graphs can still move. Codex and Gemini CLI package versions are pinned, but npm transitive dependency resolution still depends on the registry unless lockfiles are introduced.
  • Third-party apt signing keys are fetched at build time over TLS without pinning.
  • apt-get upgrade introduces drift. Image content depends on build date.
  • The image is large. Expect 3–5 GB with the full build toolchain, cloud CLIs, and multiple runtimes.
  • No seccomp/AppArmor profiles are provided. The image runs as agent (uid 1000) with passwordless sudo. Restrict capabilities at the container runtime level (the justfile does this automatically).

Updating versions

All pinned versions live in Containerfile ARG declarations and scripts/install-tools.sh. See VERSIONS.md for the full validation methodology and update procedure.

License

Dual-licensed under MIT or Apache-2.0, at your option.