Skip to content

Repository files navigation

🐎 Paddock

Your Claude Code chats, hosted and organized by project.
Persistent, resumable Claude Code sessions with a web UI — from your desk or your phone.

CI Latest release Docker image Built on herdctl

DocsQuickstartManagement APIConfigurationHow it worksherdctlIssues


Paddock — running and unread chats across every project, a live turn, a sub-agent's nested steps, an inline diff, Claude opening its own chats, files rendered in the conversation, fork and rewind, triggers, run history, the git Changes tab, and the same instance on a phone

Why Paddock

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.

Highlights

  • 🗂️ 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.yaml from 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

Quickstart

One command, from anywhere:

npx @edspencer/paddock -o

That 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.)

Always-on: your own laptop

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 | status

A 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.

Always-on: Docker

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:latest

PADDOCK_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 with PADDOCK_AUTH_MODE=none unless you explicitly set PADDOCK_DANGEROUSLY_ALLOW_OPEN. None of that is a concern for a local npx run, which is reachable only from your own machine. (The Management API is the one surface that authenticates itself rather than delegating to your proxy.)

A tour

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.

Projects grid — Paddock, herdctl, Warren, and more, each a project with its own chats, files, and changelog

Each project keeps dozens of persistent, resumable chats — searchable, forkable, archivable.

A project's chat list with dozens of real, resumable chats

Chat with Claude — real tool calls and subagents stream in, with a live context + cost meter.

A chat with Read and Grep tool blocks and a 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):

A long session showing a 42% context-window fill and running API cost

Repo-backed projects
Clone an external repo as the project's working directory — its own CLAUDE.md, branches, and PR flow apply.

New Project modal with a Git repository URL field
Rendered project files
Markdown, Mermaid, code, images, PDF and video render inline; pin files as tabs.

A markdown file rendered in the Files tab
Slash-command autocomplete
Type / to discover and run the project's skills.

Slash-command autocomplete menu
Per-project settings
Identity, model, permission mode, links, curation budgets, and drive mode — deep-linkable.

The per-project Settings tab

…and it all works from your phone.

Paddock running on a phone-sized screen

Drive it from outside

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 projects
claude 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_triggers and read_chat — nothing else. This is not timidity: any write scope can start a turn, and Claude has Bash. 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.type accepts 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. /mcp is credential-gated independently of PADDOCK_AUTH_MODE and of any reverse proxy, so it stays closed even on an instance running auth.mode: none, and a bad token gets a 401 rather than a login redirect no MCP client could follow.
  • It fails closed. The endpoint 404s entirely until you've configured both clients and a publicUrl; if every client's token resolves to nothing, it goes back to 404ing rather than opening up.

Full setup, the scope grammar, and the per-tool reference: Management API and the self-management MCP.

Configuration

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: false

Three 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.

What this instance shares with your Claude Code

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.json

Defaults 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.

Multiple instances

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.

How it works

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:

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.

Development

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 :7233

The 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.

License

MIT © 2026 Ed Spencer.

About

No description, website, or topics provided.

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages