Paddock is a secure, lightweight, project-specific sandboxed runtime container environment designed to run AI coding assistants (such as OpenCode and Gemini CLI) in complete, virtualized isolation from your host system.
Unlike complex multi-bind sandboxes, Paddock uses a single plain Containerfile and a Pristine Home Directory strategy to provide 100% transparent and standard host bind-mounting under a highly hardened, VM-level virtualization boundary.
- Linux Host with KVM support (microVM sandboxing is Linux-only).
- Rootless Podman container engine.
- The
kruncontainer runtime (orlibkrun) and thepastanetwork isolator.
- Simple Command Interface: A single, easily-remembered shell command
./paddock.sh(or nativepodman container runlabel) to build and run sandboxes. - MicroVM Virtualization (
krun): Shakes off standard shared-kernel container namespace boundaries, executing the sandbox cleanly inside its own KVM-backed microVM usinglibkrunand thekrunruntime. - Tightly Hardened Sandbox: Out-of-the-box system-wide hardening:
- Strips all Linux capabilities (
--cap-drop ALL). - Disables all privilege escalation paths (
no-new-privileges). - Mounts the system root filesystem as read-only (
--read-only). - Secures
/tmpinside an in-memory, size-limited, non-executable filesystem (tmpfswithnoexec,nosuid,nodev). - Restricts runaway processes or fork-bombs (
--pids-limit).
- Strips all Linux capabilities (
- Pasta Network Isolation: Combines isolated network namespaces via modern
pastawithpassttranslation annotations (krun.use_passt=1) for high-performance and secure network sandboxing. - MicroVM Resource Capping: Restricts the guest VM from consuming host resources using libkrun annotations:
- Caps the guest's available RAM (
krun.ram_mib). - Restricts the number of virtual CPU cores (
krun.cpus).
- Caps the guest's available RAM (
- Secure Workspace Mounting: Automatically mounts the current host directory at
/home/ai/sandboxinside the container. - Persistent CLI Configs: Standard host bind-mounting to the unprivileged
/home/aifolder preserves credentials, shell histories, and CLI caches across container runs without named volumes. - Clean System Defaults: Environment prompts and standard tools are declared system-wide, keeping
/home/aicompletely empty in the image.
The concrete limits (RAM, vCPU count,
/tmpsize, PID cap) are intentionally not reproduced here. They are baked into the image at build time bypaddock.shitself (seerun_label()), overridable viaPADDOCK_RAM_MIB,PADDOCK_CPUS,PADDOCK_PIDS_LIMITandPADDOCK_TMP_SIZE.runpicks up a changed env var automatically on its next invocation.
paddock/
├── profile/
│ ├── entrypoint.sh # Secure guest VM privilege-dropping entrypoint
│ └── Containerfile # Builds 'paddock:latest'
├── packaging/ # OBS/RPM packaging (spec, changes)
├── docs/ # Setup guide and branding assets
├── paddock.sh # Simple execution/orchestration shell script
├── sops-filter.sh # Git filter that keeps tracked secrets sops-encrypted
├── test_paddock.sh # Automated mock-based test suite
└── README.md
./paddock.sh buildbuild unconditionally (re)builds the image; run it again any time you edit the Containerfile or
entrypoint.sh. run builds automatically too, but only when the image is missing or its baked-in
run label is out of date (e.g. a PADDOCK_* env var changed); it does not pick up a Containerfile
edit on its own, so run build explicitly after one. Extra arguments are forwarded to
podman build, e.g. ./paddock.sh build --no-cache rebuilds from scratch (add --pull=always to
also refresh the base image).
The image can be personally customized without touching a tracked file: a
~/.local/share/paddock/Containerfile, if present, takes precedence over the shipped
profile/Containerfile. It still builds and runs as paddock:latest regardless of which
Containerfile backs it, and still gets the same paddock.sh-baked run label (see below) as the
shipped Containerfile. This works whether paddock.sh was checked out from git or installed
system-wide.
The image installs both opencode and the Gemini CLI from the distro repositories, alongside
the Google Cloud CLI. No build-time selection is required.
Because the Google Cloud CLI is published only for
x86_64, the image is x86_64-only.
To run your project inside a sandbox:
./paddock.sh runThis automatically:
- Prepares a persistent local home folder on the host at
~/.local/share/paddock/home. - Delegates the launch to the image's
LABEL runviapodman container runlabel(or runspodman rundirectly when custom options or commands are provided), mounting the current directory at/home/ai/sandbox. - Starts an interactive bash shell as the non-root
aiuser in that workspace.
You can also land in a specific workspace subdirectory or pass a command to run directly:
./paddock.sh run -w myproject # Land in workspace subdirectory
./paddock.sh run opencode -c # Run command directly instead of shell
./paddock.sh run -w myproject opencode -c # Run command inside subdirectoryTo reach a server inside the sandbox from the host, publish a port. -p publishes 127.0.0.1:4096; -P is passed verbatim to podman run --publish (repeatable):
./paddock.sh run -p opencode serve --hostname :: --port 4096 # opencode server on 127.0.0.1:4096
./paddock.sh run -P 127.0.0.1:18080:8080 python3 -m http.server 8080 --bind 0.0.0.0Servers must bind 0.0.0.0 (IPv4) or :: (IPv4 and IPv6) inside the sandbox, not 127.0.0.1. -p always binds the host side to IPv4 loopback; with -P, a spec without a host address (e.g. 8080) listens on all host interfaces, so give 127.0.0.1: explicitly. The opencode server is password-protected; the distro build has no browser UI, so connect with opencode --server. Publishing is per launch and never part of the baked run label. See docs/setup.md to make opencode's shared service listen on :: and port 4096 by default.
Commands are always routed through a login shell, ensuring /etc/profile and login profile scripts are sourced so environment variables are initialized.
The sandbox home can live in a bare git repository, so deleting it and starting over restores your CLI configuration, skills, agents and shell setup:
./paddock.sh init # create ~/.local/share/paddock/home.git
./paddock.sh git add .bashrc # run git on the home; only added paths are tracked
./paddock.sh git commit -m "Add bashrc"
rm -rf ~/.local/share/paddock/home # start over...
./paddock.sh init # ...recreate the empty home
./paddock.sh git checkout -f # ...and bring the tracked files back
./paddock.sh init git@host:me/home.git # on another host: clone, then git checkout -fFiles with secrets can be tracked sops-encrypted. paddock.sh git needs git 2.54 or newer. See docs/setup.md.
The image carries a LABEL run, baked in by paddock.sh at build time (see "Key Features" above), which is the single definition of every mount and security flag. ./paddock.sh run simply invokes it (when launched without options or a command), so running Podman directly is equivalent and needs no checkout of this repository:
podman container runlabel run paddock:latestThe label deliberately omits --name, so Podman assigns a unique container name on each launch and
you can run sandboxes from several directories concurrently. Use podman ps to find the generated
name.
To prevent container file masking (hiding pre-installed files inside the image), Paddock ensures /home/ai in the image is completely empty:
- Custom terminal prompts are initialized globally via
starshipin/etc/bash.bashrc. - Any shell commands run inside write persistent caches natively to the host's
~/.local/share/paddock/home, which is bind-mounted directly to/home/ai.
Additionally, to work correctly inside virtualized krun environments (where guest kernels boot standard entrypoints as root by default), Paddock includes a secure /usr/local/bin/entrypoint.sh privilege-dropper. Before dropping privileges, this script re-applies noexec,nosuid,nodev to /tmp (krun mounts it inside the guest without those flags), then registers the ai user's environmental properties and uses setpriv to drop all VM-level permissions down to the unprivileged ai user before spawning your shell.
Paddock includes an automated mock-based integration test suite. You can run it on any Linux environment to verify script behavior and container argument generation without needing podman fully installed:
./test_paddock.shPaddock is released under the terms of the Apache License, Version 2.0 (Apache-2.0).