Launch coding agents (or a plain shell) in isolated Docker containers, with the current working directory mounted as the workspace.
Caution!
Pre-release software — here be dragons. APIs, configuration formats, and CLI flags may change without notice in future versions. Bugs and crashes are possible.
paddock assembles and executes a docker run command from a layered
configuration system. Sources are merged in ascending priority — later
sources overwrite earlier ones:
- Project-level TOML (
<workdir>/.paddock/config.toml) - User-level TOML (
~/.config/paddock/config.toml) [projects."<path>"]overrides in the user TOML- Extra TOML file via
PADDOCK_CONFIG_FILEenv var, or via the--config-fileCLI flag — the CLI path replaces the env path, so the two are one source, not two PADDOCK_*environment variables- CLI flags
volumes entries are additive per host path — the same host path set by a
higher-priority source replaces the earlier mapping.
The project-level file is off by default (blocked) — enable it in the user
file's [config.allowlist]; see
project-level configuration and the allowlist.
- Python 3.12+
- Docker (CLI must be available on
PATH)
pip install phx-paddockOr with uv:
uv tool install phx-paddockDrop into a plain bash shell inside the current directory:
paddock --image=ubuntu:24.04 --agent=falseRun Claude Code in an isolated container:
paddock --image=my-claude-image --agent=claudePrint the assembled docker run command without executing it:
paddock --image=ubuntu:24.04 --agent=false --dry-runPlace a config.toml at ~/.config/paddock/ (user-level) or
<project>/.paddock/ (project-level). Both are optional, and the
project-level file is off by default until you opt in from your user config:
[config.allowlist]
project_toml = truetrue is the blanket grant; a list such as project_toml = ["volumes"]
permits only the keys it names. See
project-level configuration and the allowlist
for what each grant hands a committed file.
A config file looks like this:
agent = "claude"
image = "my-claude-image:latest"
network = "my-docker-network"
[volumes]
"/host/path" = "/container/path:ro"
[build]
dockerfile = "images/Dockerfile"
context = "."
policy = "daily"
[build.args]
AGENT = "claude"
PYTHON_VERSION = "3.13"| Field | Type | Description |
|---|---|---|
agent |
string or false |
Agent key ("claude") or false for shell |
image |
string |
Docker image to run (required) |
network |
string (optional) |
Docker network to attach the container to |
volumes |
{host: container} map |
Extra bind-mounts; container path may end
in :ro or :rw (bare path defaults to
:ro) |
build |
sub-table (optional) | Image auto-build settings (see below) |
| Field | Type | Description |
|---|---|---|
dockerfile |
string |
Path to the Dockerfile (required if build table is present) |
context |
string (optional) |
Docker build context path |
policy |
"always" / "daily" /
"if-missing" / "weekly" |
When to rebuild the image |
args |
{name: value} map (optional) |
Build-time --build-arg values |
Six config fields can be set via an environment variable, by uppercasing the
field name and prefixing it with PADDOCK_. Nested keys are joined with
_:
PADDOCK_AGENT=claude
PADDOCK_BUILD_CONTEXT=.
PADDOCK_BUILD_DOCKERFILE=images/Dockerfile
PADDOCK_BUILD_POLICY=daily
PADDOCK_IMAGE=my-claude-image
PADDOCK_NETWORK=my-docker-network
PADDOCK_CONFIG_FILE=/path/to/extra.toml # loads an additional TOML filevolumes and build.args have no environment-variable form — set them in a
TOML file, or pass --volume / --build-args-KEY=VALUE on the command
line. PADDOCK_BUILD_ARGS is ignored rather than rejected. Any other
unrecognised PADDOCK_* name is a fatal config error, so a typo stops the
run:
[env:foo] Unexpected key "foo".
paddock [FLAGS] [--] [COMMAND...]
--agent AGENT Agent key (e.g. "claude") or "false" for a shell
--build-args-KEY=VALUE Build-time ARG (repeatable)
--build-context PATH Docker build context
--build-dockerfile PATH Path to Dockerfile
--build-policy POLICY Build policy (always|daily|if-missing|weekly)
--config-file PATH Load an additional TOML config file
--dry-run Print the docker command and exit without running it
--image IMAGE Docker image
--network NETWORK Docker network
--quiet Suppress all logging and the docker command printout
--volume HOST:CONTAINER[:MODE] Extra bind-mount (repeatable)
--workdir PATH Host path to use as the workspace (default: CWD)
--workdir is resolved to an absolute real path — symlinks followed —
before it is used for the [projects] lookup and for the mounts.
paddock exits with the container's exit status; --dry-run exits 0 and a
config error exits 1.
Everything after the first positional argument (or after --) is passed
as the container command:
paddock claude --allow-dangerously-skip-permissions --continue
paddock --image=my-claude-image -- --allow-dangerously-skip-permissions --continueRuns claude inside the container. Mounts ~/.claude from the host
to /root/.claude:rw so authentication and configuration persist between
sessions.
Runs /bin/bash. Useful for exploring the container environment or
running ad-hoc commands without a coding agent.
Additional agents can be registered via the paddock.agents entry-point
group in any installed package:
[project.entry-points."paddock.agents"]
my-agent = "mypackage.agents:MyAgent"Each agent must subclass paddock.agents.BaseAgent and implement
get_command() and get_volumes().
A ready-to-use Dockerfile is included in images/. It installs
Python (via the deadsnakes PPA), Node.js, and the selected coding agent.
Build arguments:
| ARG | Default | Description |
|---|---|---|
UBUNTU_VERSION |
24.04 |
Ubuntu base image tag |
AGENT |
none |
claude or none |
NODE_VERSION |
22 |
Node.js major version |
PYTHON_VERSION |
3.13 |
Python version (installed from deadsnakes) |
Build the image manually:
docker build \
--build-arg AGENT=claude \
-t my-claude-image \
-f images/Dockerfile .Or set a [build] table in your config and let paddock build it
automatically according to your chosen policy.
MIT — see LICENCE.txt.