Launch coding agents (or a plain shell) in isolated Docker containers, with the current working directory mounted as the workspace.
Overview
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_FILE env var, or via the --config-file CLI 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.
Requirements
Python 3.12+
Docker (CLI must be available on PATH)
Installation
pip install phx-paddock
Or with uv:
uv tool install phx-paddock
Quick Start
Drop into a plain bash shell inside the current directory:
paddock --image=ubuntu:24.04 --agent=false
Run Claude Code in an isolated container:
paddock --image=my-claude-image --agent=claude
Print the assembled docker run command without executing it:
paddock --image=ubuntu:24.04 --agent=false --dry-run
Configuration
TOML files
Place 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 = true
true 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"
Config fields
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) |
Build sub-table
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 |
Environment variables
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 file
volumes 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".
CLI flags
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 --continue
Agents
claude
Runs claude inside the container. Mounts ~/.claude from the host to /root/.claude:rw so authentication and configuration persist between sessions.
false (shell)
Runs /bin/bash. Useful for exploring the container environment or running ad-hoc commands without a coding agent.
Adding agents
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().
Docker Image
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.
Licence
MIT — see LICENCE.txt.
Metadata
Release files for phx-paddock 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| phx_paddock-0.4.0.tar.gz | 27.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| phx_paddock-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.9 kB
Release files / phx_paddock-0.4.0.tar.gz
| Download URL | phx_paddock-0.4.0.tar.gz |
|---|---|
| Size | 27.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
550e1de570daa448a1edb22c910026dbf0ea5014375189c1611fe1ca84c984fe
|
|
BLAKE2b-256 checksum How to use checksums |
a041f880f508ee159400bf863ebd961d962a3f3d1c7bccbd9bbb8ce84b4098e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / phx_paddock-0.4.0-py3-none-any.whl
| Download URL | phx_paddock-0.4.0-py3-none-any.whl |
|---|---|
| Size | 38.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b3c66ac47c76a50500518faa4f90dfccc91c92217fac426fff521c87f7c43d02
|
|
BLAKE2b-256 checksum How to use checksums |
8b54eb55c70aecd30cf03785fc7a4374ed82b256b9e4b42ab67e281ad25749ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|