Your Claude Code chats, hosted and organized by project.
Persistent, resumable Claude Code sessions with a web UI — from your desk or your phone.
Docs • Quickstart • Management API • Configuration • How it works • herdctl • Issues
Paddock is a project-first launchpad for herdctl. It turns Claude Code into something you run on a server and reach from a browser: long-lived chats, grouped by project, that persist and resume — instead of a laptop full of terminal tabs you can't get back to from your phone.
A project is a directory plus a project.yaml. Paddock runs Claude Code
in that directory, and the project is the collection of chats you start there —
each one a Claude Code session, persisted on disk and resumable across reloads,
reconnects, and devices — together with per-project settings and an OVERVIEW.md
and CHANGELOG.md that a post-turn sweeper keeps current out of band. There are
two kinds:
- Notebook — a directory in your data repo for planning, notes, and light work.
- Repo-backed — an external git repo cloned as the project's working directory,
so the repo's own
CLAUDE.md, branches, and PR flow apply. The natural unit for doing real engineering.
The instance root is a workspace too, so a chat that belongs to no particular project just lives there — and can be promoted into a project later, keeping its history. The whole UI is responsive — the same launchpad works from a phone.
It also runs without you watching. Turns are fired by schedules, lifecycle events and other chats; background work a chat starts survives the turn that started it and wakes Claude when it lands. And the boundary now opens the other way: an instance exposes itself as an MCP server, so Claude Code on your laptop — or CI, or a peer Paddock — can drive it from outside. Less an app you visit, more a service your other tools talk to.
- 🗂️ Project-first — every project is a set of chats, with its own files, settings, and generated changelog
- 💬 Persistent, resumable chats — server-hosted sessions survive reloads, reconnects, and devices
- ⌨️ Token-by-token streaming — replies, real tool calls, and subagents render live as they run, with rich tool cards (Edit diffs, Bash exit codes, Grep counts)
- 🛰️ Drive it from outside — an external Management API serves the management tools as MCP at
/mcp, so Claude Code on your laptop, CI, or a peer Paddock can list projects, read chats, and (with the scope for it) start turns - ⏰ Triggers & automation — run a turn on a schedule, on a lifecycle event, or on demand; each trigger can carry its own scoped toolset
- 🤖 Self-driving — an opt-in, depth-gated in-process MCP lets Claude list projects, read chats, spawn and fork chats to fan work out across parallel sessions, manage a project's triggers, and — behind a further flag — provision new projects
- ⏳ Background work outlives the turn — a build, deploy, or sub-agent Claude backgrounds keeps running after the turn ends and wakes it with the result
- 📎 Send files & images — pick, drag-drop, or paste into the composer; Claude reads images and PDFs natively
- 📁 Files & Changes — browse rendered project files and review Claude's work as git diffs
- 🧩 Two project types — notebook (data-repo subdir) or repo-backed (clone an external repo as cwd)
- 📱 Works from your phone — the same launchpad, fully responsive
- 🔀 Chat ergonomics — star to pin, mark unread, fork or rewind from any message, queue-while-streaming, stop, search, archive; spawned chats nest under the chat that created them
- 🎛️ Settings, per project and per instance — model, permission mode, curation budgets and more per project; an instance-wide Config screen edits
paddock.config.yamlfrom the UI - 🧠 Claude Opus 5 by default — with Opus 4.8, Fable 5, Sonnet 5 and Haiku 4.5 selectable, and an allow-list if you'd rather offer fewer
- 📈 Token & cost tracking — per-chat context meter and estimated API cost, live
- 🎙️ Voice dictation & slash commands — mic-to-text in the composer,
/-autocomplete for skills - 🔌 Built on herdctl — anything the fleet engine can do, Paddock can wire in
One command, from anywhere:
npx @edspencer/paddock -oThat starts the server on http://127.0.0.1:7233 and opens a browser at it. Data
lives in ~/.paddock; the directory you run it from makes no difference.
A new instance opens on Discover. It reads your Claude Code history, works out
which directories on this machine you have actually been using claude in, and
offers them as projects — with conversation counts, last-used dates and git remotes,
so you can tell them apart. Tick the ones you want, press Import, and instead of an
empty instance you are looking at your own work, resumable. It stays in the sidebar
afterwards.
Nothing is written into your directories. No .paddock/, no .chats/, no
.gitignore edit, no CLAUDE.md — the project record and the copied transcripts
both live in ~/.paddock, and the project just points at the path. Your ~/.claude
transcripts are copied, never moved or deleted, so your terminal claude keeps
working exactly as before.
Needs Node 22+. First run downloads ~250 MB — Paddock drives Claude Code, and
the Agent SDK ships a per-platform binary of that size; later runs reuse the npm
cache. For repeated use, npm i -g @edspencer/paddock beats bare npx.
There are two subcommands, and both are optional detail: bare paddock starts the
server, paddock start is the same thing said out loud, and
paddock service
registers it as a background service that comes back at login. Anything else in
leading position is an error.
The full flag set — they parse the same after a subcommand as without one, and
--help is the canonical short-form reference:
| Flag | Purpose |
|---|---|
-p, --port <n> |
Listen port. Overrides PORT and port:. Use this when 7233 is taken. |
--host <addr> |
Bind address. Overrides HOST. |
-d, --data-dir <dir> |
Data root. Overrides PADDOCK_DATA_DIR. The only flag that picks which instance you get. |
-o, --open |
Open a browser once the server is listening. |
--verbose |
Restore normal logging; the CLI is quiet by default. |
-h, --help / -v, --version |
Usage / version. |
Port 7233 already in use? npx @edspencer/paddock --port 7234. (7233 is also
Temporal's default frontend port, which is the usual collision. The failure is
loud — Paddock names the port and the flag and exits.)
npx is a terminal tab. To keep the same ~/.paddock instance running in the
background instead:
npm i -g @edspencer/paddock
paddock service install # uninstall | statusA launchd LaunchAgent on macOS, a systemd --user unit on Linux. It starts
at login, not at boot — a per-user agent is what lets it read the Claude login
you already have, and on macOS a boot-time daemon structurally cannot (the login
Keychain is unlocked by your password at login). On Linux you also want
loginctl enable-linger $USER, or logging out stops it. Full detail, including
what the generated unit contains and what is still unverified:
Keeping Paddock running on your laptop.
For a server rather than a laptop, run the published image, point it at a data volume, and give it a Claude token:
docker run -d --name paddock -p 127.0.0.1:7233:7233 \
-e CLAUDE_CODE_OAUTH_TOKEN=… `# Claude Max/Pro plan (OAuth)` \
-e PADDOCK_DATA_DIR=/data \
-e PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 `# see below — required inside a container` \
-v paddock-data:/data \
ghcr.io/edspencer/paddock:latestPADDOCK_DANGEROUSLY_ALLOW_OPEN=1 is required here and the container will
refuse to start without it. The image binds 0.0.0.0 (it has to, to be reachable
from outside the container) and Paddock's default auth mode is none, and
Paddock will not bind a routable interface unauthenticated without being told to.
It is safe in this command because -p 127.0.0.1:7233:7233 publishes the port
on loopback only — the container namespace is the boundary. Drop the 127.0.0.1:
and you have handed an unauthenticated, code-executing Paddock to your whole
network, so put an auth mode or a reverse proxy in front of it first.
Then open http://127.0.0.1:7233 and click New Project.
Two images ship from the same source: :latest is the lean base (app plus
git, gh, and the claude CLI), and :devbox layers on a coding-agent
toolbox — PM2-backed preview servers, ffmpeg, a headless Playwright browser,
the Docker CLI, kubectl, python3/uv, jq, rsync. Same app and same
/data layout, so you can swap tags against
one volume. Ready-made self-hosting recipes (Compose, Kubernetes, Proxmox,
a Basic Auth sidecar) live in
paddock-deploy.
docker-compose
services:
paddock:
image: ghcr.io/edspencer/paddock:latest # or :devbox for the coding-agent toolbox
ports:
- "127.0.0.1:7233:7233"
environment:
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN} # Claude Max/Pro (OAuth); or ANTHROPIC_API_KEY for API-key billing
PADDOCK_DATA_DIR: /data
PADDOCK_DANGEROUSLY_ALLOW_OPEN: "1" # required in a container; safe because the port is published on loopback
volumes:
- paddock-data:/data
volumes:
paddock-data:The web UI has no login of its own — run it behind a reverse proxy / auth layer you trust (see AUTH.md). Paddock reads credentials from the environment and from files the host provides; it never stores secrets itself. It also fails closed: an
npx, source or tarball run binds loopback by default, and refuses to start on a routable interface withPADDOCK_AUTH_MODE=noneunless you explicitly setPADDOCK_DANGEROUSLY_ALLOW_OPEN. None of that is a concern for a localnpxrun, which is reachable only from your own machine. (The Management API is the one surface that authenticates itself rather than delegating to your proxy.)
These are real screenshots — Paddock is dogfooded on its own dev stack: Paddock, herdctl (the engine underneath it), and Warren (an agentic PR reviewer) all live here as projects that build one another.
Every project is a set of chats, organized on one page.
Each project keeps dozens of persistent, resumable chats — searchable, forkable, archivable.
Chat with Claude — real tool calls and subagents stream in, with a live context + cost meter.
Built for real, long-running work — persistent sessions track their own context window and estimated cost as they grow (this one's 42% of a 1M-token window):
…and it all works from your phone.
Paddock isn't only something you open in a browser. An instance can expose its
Management API as an MCP server over streamable HTTP at /mcp — so a
Claude Code session on your laptop, a CI job, or a peer Paddock can list
projects, read chats, and, if you grant it, start turns. External callers get
the same toolset Claude receives in-process, so the two surfaces can't
drift.
Clients are declared in paddock.config.yaml, and their tokens are
referenced, never inlined — a literal secret in the config file is a hard
error:
managementApi:
instanceId: my-paddock
publicUrl: https://paddock.example.com
clients:
my-laptop:
auth:
ref: env:PADDOCK_MCP_TOKEN_MY_LAPTOP # value lives in the environment
# no scope ⇒ read-only, across all projectsclaude mcp add --transport http --scope user paddock \
https://paddock.example.com/mcp \
--header "Authorization: Bearer $PADDOCK_MCP_TOKEN_MY_LAPTOP"A few things worth knowing before you widen that scope:
- Read-only is the default, and it should usually stay that way. A client
configured without an explicit scope gets
list_projects,list_chats,list_triggersandread_chat— nothing else. This is not timidity: any write scope can start a turn, and Claude hasBash. Granting write access to the Management API is equivalent to granting remote code execution on the host. Treat such a token like an SSH key, scope it to specific projects, and expect the boot log to warn you by name when a client holds one. - Static bearer tokens are the only credential that works today. OAuth is
not implemented —
auth.typeaccepts only"token", and anything else is a config error. Paddock will publish RFC 9728 protected-resource metadata once an authorization server is configured, but there is no OAuth path to configure one against yet, so mint a token and use it. - It authenticates itself.
/mcpis credential-gated independently ofPADDOCK_AUTH_MODEand of any reverse proxy, so it stays closed even on an instance runningauth.mode: none, and a bad token gets a401rather than a login redirect no MCP client could follow. - It fails closed. The endpoint
404s entirely until you've configured both clients and apublicUrl; if every client's token resolves to nothing, it goes back to404ing rather than opening up.
Full setup, the scope grammar, and the per-tool reference: Management API and the self-management MCP.
An instance is configured by a single paddock.config.yaml, with per-project
overrides in each project's project.yaml. The file lives at
<PADDOCK_DATA_DIR>/paddock.config.yaml (or wherever PADDOCK_CONFIG points),
and it is entirely optional — every key has a built-in default, so an instance
with no file at all is a working instance.
# <PADDOCK_DATA_DIR>/paddock.config.yaml — every key below is optional.
schemaVersion: 1 # the version of THIS format the file is written in
# --- Core ---
port: 7233
host: 127.0.0.1 # loopback by default; a routable bind needs the opt-in below
logLevel: info # trace | debug | info | warn | error | fatal | silent
# --- How turns run ---
driveMode: session # session = SDK runtime (streaming + cross-turn autonomy)
# batch = legacy one-shot CLI runtime
nativeSystemPrompt: true # use Claude Code's native prompt + CLAUDE.md hierarchy
models: # which catalog models the picker offers; omit to offer all
- claude-opus-5
- claude-sonnet-5
# --- Capabilities & safety gates (every switch defaults OFF; maxSpawnDepth to 1) ---
selfMcpEnabled: true # let Claude list/read projects and other chats
selfMcpWriteEnabled: true # + create, fork, message and archive chats
selfMcpProjectsEnabled: false # + provision whole new projects
maxSpawnDepth: 1 # how deep a spawn tree may grow before children lose the self-MCP
hooksMcpEnabled: false # let chats declare and edit their own triggers
browserMcp: false # give chats a headless Chromium browser MCP
# --- The post-turn sweeper that curates OVERVIEW.md and CHANGELOG.md ---
sweepMinIntervalMs: 300000 # at most one sweep per project per 5 minutes
curation:
overviewMaxTokens: 2000
changelogMaxTokens: 8000
# --- Authentication (modes and claims are documented in AUTH.md) ---
auth:
mode: none # none | trusted-header | jwt
# --- Presentation and identity ---
brand:
name: Paddock
logo: "🐎"
accent: "#c2603c"
gitAuthor:
name: Paddock
email: paddock@localhost
# --- Inbound composer attachments ---
attachments:
enabled: true
maxFileSizeMb: 25
maxFilesPerMessage: 10
# --- A Swagger UI at /open-api, generated from the route schemas ---
openapi:
enabled: falseThree more blocks belong in this file and are documented in their own sections:
managementApi: (external /mcp clients),
claude: (what this instance
borrows from the machine), and a sibling mcpServers: declaring MCP servers to
Paddock itself.
Environment variables override the file. Resolution is
built-in default < paddock.config.yaml < environment, so every key above has
a matching PADDOCK_* variable that wins over it — deliberate, so a container can
pin one value at run time without rewriting a mounted file. The complete list is
the environment reference;
.env.example is a runnable starting point.
A handful are worth keeping in the environment regardless — a Docker user needs all of these, and some have no file equivalent at all:
| Var | Default | Purpose |
|---|---|---|
PADDOCK_DATA_DIR |
./data |
Data root — holds projects/, .herdctl/ state, the generated herdctl.yaml, and the config file itself. Setting it cascades all derived paths. (There is a dataDir: key, but the file is located under the data dir, so it can only re-base the derived paths — not say where to find itself.) |
PADDOCK_CONFIG |
— | Explicit path to the config file, instead of the default location. Pointing it at a missing file is a startup error, not a silent fallback. |
PADDOCK_DANGEROUSLY_ALLOW_OPEN |
— | Required to bind a routable interface with auth.mode: none. Without it Paddock refuses to start — it runs code and spends your Claude tokens. No file equivalent by design: it is an explicit act at deploy time, not a stored setting. |
CLAUDE_CODE_OAUTH_TOKEN |
— | Claude auth — Max/Pro plan (OAuth). Credentials are read from the environment and never written to the config file. |
ANTHROPIC_API_KEY |
— | Claude auth — API-key billing. Same. |
PORT / HOST |
7233 / 127.0.0.1 |
Also settable as port: / host:. For PORT the precedence is --port flag, then PORT, then the file. |
The instance Config screen (at /config) edits paddock.config.yaml from the
UI: it writes the file while preserving your comments, shows which fields an
environment variable has pinned — those render read-only, because writing them
would change nothing — and flags when a saved value needs a restart to take
effect.
Paddock always keeps its own Claude home at <data-dir>/claude-home, and
refuses to start if that resolves to your ~/.claude. What it borrows from the
machine is five independent keys in paddock.config.yaml — each own
(Paddock's, isolated) or host (this machine's):
claude:
transcripts: own # own | host — default own
credentials: host # own | host — default host — the one shared by default
instructions: own # own | host — default own — CLAUDE.md, agents, commands, plugins
hooks: own # own | host — default own — settings.json shell hooks
mcpServers: own # own | host — default own — the servers in ~/.claude.jsonDefaults mean nothing outside the data dir is written, and only your login is
read. Worth knowing if you keep a curated ~/.claude/CLAUDE.md: it is not
loaded unless you set instructions: host. Each project's own CLAUDE.md always
applies. A sibling top-level mcpServers: block declares servers to Paddock
itself, for the case where the machine has none to borrow. Full detail:
what Paddock touches on your machine
and the config file reference.
Authentication modes (none / trusted-header / jwt) and secret handling
(GitHub tokens, SSH keys, per-platform mapping) are documented in AUTH.md;
the Management API carries its own credentials, documented separately.
Paddock is one process per data root + port. To run several (e.g. one per area —
open-source / house / homelab), start one process each with its own
PADDOCK_DATA_DIR and PORT, and front them with a reverse proxy that maps a
hostname to each port. Nothing is shared between instances except the host.
Paddock is a thin project layer over the public @herdctl/core FleetManager. It
wires projects, chats, and a git backing store on top; anything the
herdctl CLI/dashboard can do, the library can too.
Chat turns run through herdctl's session runtime (persistent
openChatSession) by default — that's what lets chats resume across reloads,
stream token-by-token, and carry autonomous work (ScheduleWakeup, /loop)
across turn boundaries. Triggers (schedules, lifecycle events, and
reserved webhooks), the self-management MCP, and external /mcp callers all
drive the same startAgentTurn core as a human message does, so anything a
person can start in a chat, an automation can start too. The management
operations sit behind a single policy layer rather than one per transport, so
every access path inherits identical scoping instead of reimplementing it.
packages/server— Fastify + WebSocket backend; wraps the FleetManager + a Project layer (ProjectStore). Serves the built SPA in production.packages/web— React + Vite + Tailwind project-first SPA.
For the full picture, start with the documentation site — paddock.edspencer.net — which covers getting started, deploying, binding and exposure, the Management API, and what's new in each release. In the repo:
- Architecture overview — monorepo shape, the three storage classes, WebSocket/session flow, MCP injection, auth boundary, the sweeper, drive modes.
- Concepts — short explanations of the core ideas: projects, agents, chats are Claude Code sessions, and the sweeper.
- REST + WebSocket API — every
REST route carries a schema, so a live OpenAPI 3 document and a Swagger UI are
available at
/open-apiwhenPADDOCK_OPENAPI_ENABLEDis set. - herdctl integration
— the exact public
@herdctl/coreAPI contract Paddock depends on.
The source for all of the above is markdown in this repo under
website/src/content/docs/. The older copies in
docs/ are a superseded fork — see that file for which of
its contents are still originals.
npm install # install all workspaces
npm run build # build server (tsc) + web (vite)
npm run typecheck # typecheck both packages
npm test # server (unit + integration) + web (component) tests
npm run test:e2e # Playwright journeys (incl. mobile) against the real server + a fake `claude`
# Run locally (two terminals):
npm run dev # server on :7233 (API + WS)
npm run dev:web # Vite dev server, proxies /api + /ws to :7233The E2E suite drives the real server, FleetManager, and CLI runtime; only the
LLM is swapped for a fake claude on PATH (zero Anthropic calls). Opt into a
real-Claude run with npm run test:e2e:live (PADDOCK_TEST_LIVE=1). More detail
in DEV.md and
Testing.
The documentation site in website/ is deliberately outside the root
workspaces — install and build it separately if you're changing docs.
New contributor? Start with CONTRIBUTING.md — conventions, env gotchas, and the changesets flow.
MIT © 2026 Ed Spencer.









