Commands
Bare paddock, and paddock with any flag that is
not -h or --version, both mean choose.
$ paddock --help
usage: paddock [-h] [--version]
{choose,launch,run,attach,profiles,gc,collect,logs,init} ...
The `paddock` command: the popup chooser by default, and the same jobs without
questions.
positional arguments:
{choose,launch,run,attach,profiles,gc,collect,logs,init}
choose ask what to open (the default)
launch start a session from a saved profile, no questions
run run a session in this terminal, with no herdr and no
new tab
attach put a new tab on a running session
profiles list saved profiles
gc collect sessions whose tabs are all closed (every
command does this first)
collect end one session now: stop its sandbox and drop it from
the registry
logs where paddock logged what it did, and the end
init bind the chooser to prefix+s in herdr's config
options:
-h, --help show this help message and exit
--version, -V print the version and exit
| Command | Positional | Flags |
|---|---|---|
| choose | none | --dry-run print what would happen, do nothing --cwd CWD use this directory, not the current one --attach open on the list of live sessions, not on the form |
| launch | profile as listed by paddock profiles |
--dry-run --cwd CWD share this host directory, read-write, overriding shared_dir--backend BACKEND srt, or msb for a microVM. Default srt |
| run | profile optional; with none it asks, which needs a terminal to ask in | --dry-run --backend BACKEND as above --attach session join a running session in this terminal no --cwd |
| attach | session id or name | --dry-run --cwd CWD where the new tab opens, not a permission --shell a plain shell inside the sandbox instead of the agent |
| profiles | none | none |
| gc | none | none |
| collect | session id or name | none |
| logs | session optional; that session's pane log instead of paddock's own | none |
| init | none | --dry-run --undo put the newest backed-up herdr config back |
--cwd is a permission on launch, and only a directory on attach.
On launch it is the one host directory the sandbox may read and write. On
attach it only says where the tab opens, because the session's permissions
were settled when it was created.
Two ways to run it
Same profile, same policy, same launch script. What differs is who owns the terminal.
In herdr
Sessions and tabs- Wire it up
paddock init, once. It backs up~/.config/herdr/config.toml, writes a managed block, and asks herdr to reload.--undoputs it back.- Keys it binds
- prefix+s runs
paddock
prefix+shift+s runspaddock choose --attach
prefix+comma herdr's settings, moved to free the key
prefix+c herdr's own new tab, untouched - Sessions
- One session, many tabs. Tabs are labelled
sbx:<name>, orsbx:<name> (shell). Sessions outlive the popup and survive a herdr restart. - Ends when
- The last tab closes. Collection is lazy, at the next paddock command.
paddock gcforces it.
Standalone
One terminal, one session- Run it
paddock run offline-shell paddock run claude-default paddock run offline-shell --backend msb paddock run
- Bind it to a key
- tmux:
bind-key s display-popup -E -w 80% -h 70% paddock run
- kitty:
map ctrl+shift+s launch --type=overlay paddock run
- Sessions
- No tabs and no attach UX: those are herdr's.
paddock run --attach <session>joins one already running. - Ends when
- The run ends and the terminal is yours again.
paddock collect <session>ends one by hand, which a microVM needs only if the terminal was killed outright.
Chooser keys
What ? shows in the chooser.
| Key | What it does |
|---|---|
| up down, k j | move between fields, or between items |
| enter | open the field, take the item, press the button |
| esc | back out one level, keeping every answer, as the Back row does |
| ctrl-c | cancel the popup, at any depth |
| a digit | jump straight to that field, on the form |
| space | tick, in a checklist |
| tab | to the box under a checklist, and back to the list |
| / | filter a long list |
| a n | tick all, tick none, in a checklist |
| L | launch |
| s | save these answers as a profile |
| ? | this list |
Escape means two things, on purpose. On the form it cancels, because nothing is before it. On the confirm screen it is the Back button, and on every list and checklist it backs out one level keeping what you did there.
Profile fields
One JSON file per profile, in
~/.config/paddock/profiles/. The filename stem wins over
name. A field of the wrong type rejects the whole file rather than half
applying it.
| Field | Type | Default | What it decides |
|---|---|---|---|
| name | str | "custom" | Profile name. No empty string, no /, no leading dot |
| agent | str | "claude" | Key into the agent registry |
| tools | list[str] | ["git","rg","curl"] | Binaries symlinked into the sandbox PATH. "*" hands over the host's own PATH. Ignored on msb: the image is the tool selection |
| include_system_path | bool | true | Append /usr/bin:/bin, so a shell and coreutils work |
| network_presets | list[str] | ["anthropic","github"] | Keys into the preset table in § 06 |
| extra_domains | list[str] | [] | Domains typed into the Also allow box, added verbatim |
| shared_dir | str | "" | The one host directory, read-write. Empty means an isolated scratch workdir instead |
| skills | list[str] | [] | Skills that exist inside the sandbox at all. "*" is every installed one |
| mcp | list[str] | [] | MCP servers written into the generated .mcp.json |
| deny_read | list[str] | ["~/.ssh", | Denied both ways on srt |
| extra_allow_write | list[str] | [] | Writable paths beyond the workdir, the run dir and /tmp |
Built in
| Profile | Agent | Tools | Network |
|---|---|---|---|
| claude-default | claude | git rg fd jq curl node npm npx uv python3 | anthropic, github, npm, pypi/uv |
| offline-shell | shell | git rg fd jq | none |
Agent fields
One JSON file per agent, in ~/.config/paddock/agents/. An
entry with no command is unusable and is skipped.
| Field | Type | What it decides |
|---|---|---|
| name | str | Display name. Falls back to the filename stem |
| command | str | The executable run inside the sandbox, with its flags |
| api_domains | list[str] | Domains folded into the allowlist whatever the user ticks. This is where localhost:<port> goes |
| required_tools | list[str] | Tools the command cannot start without. Shimmed alongside it on srt, and an agent whose required tools are missing cannot be picked |
| auth_read_paths | list[str] | Only this agent's own credentials, copied into the synthesized config dir |
| config_write_paths | list[str] | The agent's real config dirs. Denied both ways once a synthesized dir exists |
| image | str | OCI image for the msb backend. Unused by srt. An agent with no image is refused on msb |
| install | str | How the guest installs command when the image does not ship it. Run once per session, msb only. Blank means the image is expected to have it |
Built in
| Key | Command | API domains | Needs | Image and install |
|---|---|---|---|---|
| claude | claude | api.anthropic.com, *.anthropic.com | none | node:22-slim npm install -g @anthropic-ai/claude-code@2.1.239 |
| codex | codex | api.openai.com, chatgpt.com, auth.openai.com | node | none |
| opencode | opencode | opencode.ai, *.opencode.ai, models.dev, api.anthropic.com, api.openai.com | none | none |
| aider | aider | api.openai.com, api.anthropic.com, openrouter.ai | none | none |
| gemini | gemini | generativelanguage.googleapis.com, cloudcode-pa.googleapis.com, oauth2.googleapis.com | none | none |
| shell | $SHELL, or /bin/sh | none | none | none |
The localhost:<port> convention
A domain is split on its last colon, and the suffix counts as a port only
if it is a decimal between 1 and 65535. A loopback host with a port names that one port;
a bare localhost means every port.
# ~/.config/paddock/agents/local-model.json
{
"name": "Aider on the local model",
"command": "aider --no-auto-commits",
"image": "python:3.12-slim",
"install": "pip install --quiet aider-chat",
"api_domains": ["localhost:11434"]
}
On msb that becomes one rule,
allow@host:tcp:11434, and the guest boots with
OPENAI_BASE_URL and OLLAMA_HOST pointed at
host.microsandbox.internal. Both variables are set only when the profile
resolves to exactly one loopback port. On srt the port is not honoured: Seatbelt's
loopback rule takes no port, so the grant is every service on this machine.
Network presets
| Preset | What it opens |
|---|---|
| anthropic | api.anthropic.com, *.anthropic.com |
| openai | api.openai.com, chatgpt.com, auth.openai.com |
| github | github.com, *.github.com, *.githubusercontent.com |
| npm | registry.npmjs.org, *.npmjs.org, *.npmjs.com |
| pypi/uv | pypi.org, files.pythonhosted.org, *.pythonhosted.org, astral.sh |
| go | proxy.golang.org, sum.golang.org |
| crates.io | crates.io, *.crates.io, static.crates.io |
| homebrew | formulae.brew.sh, *.brew.sh, ghcr.io, pkg-containers.githubusercontent.com |
| local services (localhost) | localhost, 127.0.0.1. Portless, so it opens every service listening on this machine's loopback, whatever port |
| everything | Every domain, on any port. msb only, as --net-default allow. srt refuses it and says to use msb |
An agent's own api_domains are added whatever you tick, so picking an agent is granting the hosts it signs in to. Tick nothing else and that is all a session can reach; pick the shell agent and it reaches nothing.
Environment
What paddock reads
| Variable | Default | What it moves |
|---|---|---|
| PADDOCK_CONFIG_DIR | ~/.config/paddock | Where profiles/ and agents/ live |
| PADDOCK_STATE_DIR | ~/.local/state/paddock | Run dirs, the session registry and the logs |
| PADDOCK_LOG_FILE | <state>/logs/paddock.log | Moves paddock's own log file |
| PADDOCK_LOG | warning | What reaches stderr: debug, info, warning, error, critical |
| HERDR_ACTIVE_WORKSPACE_ID | unset | Which herdr workspace the popup is in. Keys the last-profile memory |
| SHELL | /bin/sh | The shell agent's command, and what a shell tab runs |
| TMPDIR | unset | If set, added to the srt writable paths, because the sandbox keeps it |
| PATH | /usr/bin:/bin | Used as the sandbox PATH when the profile ticked every tool |
An empty value falls back to the default. Setting
PADDOCK_STATE_DIR= gives you ~/.local/state/paddock, never the
current directory.
What the sandbox is given
| Variable | Set when | Value |
|---|---|---|
| CLAUDE_CONFIG_DIR | agent is claude | The synthesized config dir: the run dir's config/ on srt, /paddock-config in an msb guest. It is the only agent that can be redirected today |
| DISABLE_AUTOUPDATER | agent is claude | 1 |
| OPENAI_BASE_URL | msb, and exactly one loopback port | http://host.microsandbox.internal:<port>/v1 |
| OLLAMA_HOST | msb, and exactly one loopback port | http://host.microsandbox.internal:<port> |
| PATH | srt | The shim dir, plus /usr/bin:/bin when include_system_path |
| PS1 | shell tabs | paddock: in front of your own prompt, so the one tab that could be mistaken for an ordinary shell says what it is |
Nothing else crosses. Only HOME, USER,
LOGNAME, SHELL, TERM, LANG,
LC_ALL and TMPDIR are carried through from the host, plus the
proxy variables srt needs.
The install script
| Variable | What it does |
|---|---|
| PADDOCK_REF | Install that branch or tag instead of the default branch |
| PADDOCK_YES | 1 answers yes to the prompts, for unattended installs |
paddock