paddock

v0.2.0

Reference

Every command, flag, profile field, and key binding.

§ 01

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
Flags, per subcommand
CommandPositionalFlags
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
profilesnonenone
gcnonenone
collectsession id or namenone
logssession optional; that session's pane log instead of paddock's ownnone
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.

§ 02

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. --undo puts it back.
Keys it binds
prefix+s runs paddock
prefix+shift+s runs paddock 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>, or sbx:<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 gc forces 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.
§ 03

Chooser keys

What ? shows in the chooser.

The keys
KeyWhat it does
up down, k jmove between fields, or between items
enteropen the field, take the item, press the button
escback out one level, keeping every answer, as the Back row does
ctrl-ccancel the popup, at any depth
a digitjump straight to that field, on the form
spacetick, in a checklist
tabto the box under a checklist, and back to the list
/filter a long list
a ntick all, tick none, in a checklist
Llaunch
ssave 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.

§ 04

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.

Fields, types and defaults
FieldTypeDefaultWhat it decides
namestr"custom"Profile name. No empty string, no /, no leading dot
agentstr"claude"Key into the agent registry
toolslist[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_pathbooltrueAppend /usr/bin:/bin, so a shell and coreutils work
network_presetslist[str]["anthropic","github"]Keys into the preset table in § 06
extra_domainslist[str][]Domains typed into the Also allow box, added verbatim
shared_dirstr""The one host directory, read-write. Empty means an isolated scratch workdir instead
skillslist[str][]Skills that exist inside the sandbox at all. "*" is every installed one
mcplist[str][]MCP servers written into the generated .mcp.json
deny_readlist[str]["~/.ssh","~/.aws","~/.gnupg","~/.config/gh"]Denied both ways on srt
extra_allow_writelist[str][]Writable paths beyond the workdir, the run dir and /tmp

Built in

ProfileAgentToolsNetwork
claude-defaultclaudegit rg fd jq curl node npm npx uv python3anthropic, github, npm, pypi/uv
offline-shellshellgit rg fd jqnone
§ 05

Agent fields

One JSON file per agent, in ~/.config/paddock/agents/. An entry with no command is unusable and is skipped.

Fields and what they decide
FieldTypeWhat it decides
namestrDisplay name. Falls back to the filename stem
commandstrThe executable run inside the sandbox, with its flags
api_domainslist[str]Domains folded into the allowlist whatever the user ticks. This is where localhost:<port> goes
required_toolslist[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_pathslist[str]Only this agent's own credentials, copied into the synthesized config dir
config_write_pathslist[str]The agent's real config dirs. Denied both ways once a synthesized dir exists
imagestrOCI image for the msb backend. Unused by srt. An agent with no image is refused on msb
installstrHow 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

KeyCommandAPI domainsNeedsImage and install
claudeclaudeapi.anthropic.com, *.anthropic.comnonenode:22-slim
npm install -g @anthropic-ai/claude-code@2.1.239
codexcodexapi.openai.com, chatgpt.com, auth.openai.comnodenone
opencodeopencodeopencode.ai, *.opencode.ai, models.dev, api.anthropic.com, api.openai.comnonenone
aideraiderapi.openai.com, api.anthropic.com, openrouter.ainonenone
geminigeminigenerativelanguage.googleapis.com, cloudcode-pa.googleapis.com, oauth2.googleapis.comnonenone
shell$SHELL, or /bin/shnonenonenone

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.

§ 06

Network presets

Presets and the domains they open
PresetWhat it opens
anthropicapi.anthropic.com, *.anthropic.com
openaiapi.openai.com, chatgpt.com, auth.openai.com
githubgithub.com, *.github.com, *.githubusercontent.com
npmregistry.npmjs.org, *.npmjs.org, *.npmjs.com
pypi/uvpypi.org, files.pythonhosted.org, *.pythonhosted.org, astral.sh
goproxy.golang.org, sum.golang.org
crates.iocrates.io, *.crates.io, static.crates.io
homebrewformulae.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
everythingEvery 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.

§ 07

Environment

What paddock reads

VariableDefaultWhat it moves
PADDOCK_CONFIG_DIR~/.config/paddockWhere profiles/ and agents/ live
PADDOCK_STATE_DIR~/.local/state/paddockRun dirs, the session registry and the logs
PADDOCK_LOG_FILE<state>/logs/paddock.logMoves paddock's own log file
PADDOCK_LOGwarningWhat reaches stderr: debug, info, warning, error, critical
HERDR_ACTIVE_WORKSPACE_IDunsetWhich herdr workspace the popup is in. Keys the last-profile memory
SHELL/bin/shThe shell agent's command, and what a shell tab runs
TMPDIRunsetIf set, added to the srt writable paths, because the sandbox keeps it
PATH/usr/bin:/binUsed 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

VariableSet whenValue
CLAUDE_CONFIG_DIRagent is claudeThe 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_AUTOUPDATERagent is claude1
OPENAI_BASE_URLmsb, and exactly one loopback porthttp://host.microsandbox.internal:<port>/v1
OLLAMA_HOSTmsb, and exactly one loopback porthttp://host.microsandbox.internal:<port>
PATHsrtThe shim dir, plus /usr/bin:/bin when include_system_path
PS1shell tabspaddock: 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

VariableWhat it does
PADDOCK_REFInstall that branch or tag instead of the default branch
PADDOCK_YES1 answers yes to the prompts, for unattended installs