Skip to content

Environment variables

Paddock is configured from the environment: every setting is read once at startup (packages/server/src/config.ts), normalised, and frozen. This page is the canonical list of every variable the server reads, its default (taken from the code, not guessed), and what it does.

For a runnable starting point, copy .env.example to .env and adjust. Authentication is summarised below but documented in full in AUTH.md.

Two helpers do almost every read:

  • envOr(name, fallback) — the raw (untrimmed) value if non-blank, else the literal fallback. Only the blank check is trimmed; the returned value keeps any surrounding whitespace.
  • envOpt(name) — the trimmed value, or unset (undefined) when blank.

Consequences worth knowing:

  • Blank is unset. A whitespace-only value (PADDOCK_X="") yields the default, not an empty string.
  • Booleans accept 1 / true / yes (case-insensitive) as true — except PADDOCK_KEEPER_NATIVE_PROMPT, which is on by default and only 0 / false / no turns it off.
  • Unknown enum values fall back to the default rather than failing startup (e.g. an unrecognised PADDOCK_AUTH_MODE becomes none).
  • Paths are resolved to absolute and canonicalised (symlinks resolved) so Claude Code session discovery can find transcripts.

VariableDefaultRequiredPurpose
PADDOCK_DATA_DIR./datanoData root. All paths below default to subdirectories of this — set it and everything cascades. Holds projects, scratch, generated herdctl config, and state.
PADDOCK_CONFIG<data>/paddock.config.yamlnoPath to the optional YAML instance-config file — the base layer beneath every variable on this page. Resolved against the bootstrap data dir when unset; a missing file there is fine (env-only deployments are unaffected), but an explicitly-set path that doesn’t exist is a startup error, so a typo can’t silently boot an instance with none of your settings. See Config file (YAML).
PADDOCK_PROJECTS_DIR<data>/projectsnoRoot that contains per-project directories (each is a keeper’s working dir).
PADDOCK_SCRATCH_DIR<data>/scratchnoWorking directory for one-off / scratch chats.
PADDOCK_STATE_DIR<data>/.herdctlnoherdctl state directory.
PADDOCK_HERDCTL_CONFIG<data>/herdctl.yamlnoPath to the generated herdctl.yaml the FleetManager loads (Paddock owns/regenerates it).
PADDOCK_WEB_DISTpackages/web/distnoBuilt SPA served in production (resolved relative to the server module).
PORT4000noHTTP/WS listen port.
HOST127.0.0.1noBind host. Safe by default: defaults to loopback, so a fresh run is network-closed. PADDOCK_HOST is an alias. Set to 0.0.0.0 (all interfaces) only behind auth or a proxy — see the guard below.
PADDOCK_DANGEROUSLY_ALLOW_OPENfalsenoEscape hatch for the open-server guard: allow a non-loopback bind with no authentication (PADDOCK_AUTH_MODE=none). Accepts 1/true/yes. Without it, that combination refuses to start; with it, the server boots but logs a loud one-line warning. Leave unset unless you truly intend an unauthenticated server on a routable interface.
CLAUDE_HOME~/.claudenoClaude home used for session/transcript discovery.

Safe-by-default binding. Paddock runs code and spends Claude tokens, so it refuses to expose itself carelessly. The bind host defaults to 127.0.0.1 (loopback only), and binding a non-loopback host (e.g. 0.0.0.0) while authentication is none fails closed at startup — mirroring the jwt-without-JWKS check. The container images still bind 0.0.0.0 by design, but they are not exempt from that check — a container run needs an auth mode or PADDOCK_DANGEROUSLY_ALLOW_OPEN=1, or it won’t start at all.

The default changed in v0.44, which is breaking if you relied on the old 0.0.0.0. See Binding & network exposure for what counts as loopback, the exact guard conditions, the container story, and how to fix an upgraded instance you can no longer reach.

PADDOCK_CONFIG__* is not implemented. There is no generic PADDOCK_CONFIG__foo__bar → nested-herdctl-key override mechanism in this tree. (The similarly-named window.__PADDOCK_CONFIG__ is a browser global the server injects into index.html to carry branding to the SPA — not an env var.)

Provider-agnostic; the default (none) is fully open. See AUTH.md for modes, provider examples, and secret handling — this table is only the knobs.

VariableDefaultRequiredPurpose
PADDOCK_AUTH_MODEnonenonone | trusted-header | jwt. Unknown → none.
PADDOCK_AUTH_USER_HEADERX-Forwarded-Userno(trusted-header) Header carrying the username.
PADDOCK_AUTH_EMAIL_HEADERno(trusted-header) Header carrying the email.
PADDOCK_AUTH_GROUPS_HEADERnoHeader carrying group membership (comma/space-split in trusted-header mode).
PADDOCK_AUTH_JWT_HEADERAuthorizationno(jwt) Header carrying the token. Authorization strips a leading Bearer .
PADDOCK_AUTH_JWKS_URLjwt(jwt) IdP JWKS endpoint used to verify the signature. Required when PADDOCK_AUTH_MODE=jwt — startup fails without it.
PADDOCK_AUTH_JWT_ISSUERno(jwt) Expected iss claim (validated when set).
PADDOCK_AUTH_JWT_AUDIENCEno(jwt) Expected aud claim (validated when set).
PADDOCK_AUTH_USERNAME_CLAIM(auto)no(jwt) Claim to read the username from. Default tries preferred_usernameemailsub.
PADDOCK_AUTH_GROUPS_CLAIMgroupsno(jwt) Claim to read groups from.

Management API tokens (PADDOCK_MCP_TOKEN_*)

Section titled “Management API tokens (PADDOCK_MCP_TOKEN_*)”

The external Management API at /mcp has no PADDOCK_* variables of its own — the whole managementApi block is config-file-only. The environment’s job is to hold the client tokens, which the file only ever references:

managementApi:
clients:
my-laptop:
auth:
ref: env:PADDOCK_MCP_TOKEN_MY_LAPTOP
Terminal window
PADDOCK_MCP_TOKEN_MY_LAPTOP=pdk_my-paddock_1a2b3c…
VariableDefaultRequiredPurpose
PADDOCK_MCP_TOKEN_<CLIENT>(per configured client)The bearer token for one managementApi.clients entry. The name is a convention, not a built-in — the variable read is whatever the client’s auth.ref names, and Paddock’s own error messages suggest this shape, uppercasing the client id and replacing every non-alphanumeric character with an underscore. Minimum 24 characters; prefer pdk_<instanceId>_<secret> so the token is bound to one instance. Unset, blank, or too short ⇒ that client is dropped with a warning.

env:VAR_NAME is the only supported form of auth.ref, and an inline token: or secret: in the YAML is a hard config error — the config file is git-tracked. Deliver these like any other runtime credential: from a secrets manager or a secrets file, not a committed .env.

Opt-in, and off on a plain instance: mounting it publishes a map of the whole HTTP surface, so it’s a deliberate choice. When enabled the instance serves a branded Swagger UI whose security schemes reflect its own auth mode. See OpenAPI & Swagger for the whole surface, and /api/ for the always-available published reference for the latest release.

VariableDefaultRequiredPurpose
PADDOCK_OPENAPI_ENABLEDfalse (OFF)noMount the Swagger UI + the raw spec. Accepts 1/true/yes/on — note this one also takes on, which the other boolean knobs don’t. When off, none of the routes exist.
PADDOCK_OPENAPI_PATH/open-apinoRoute prefix the UI mounts under. Normalised to a leading slash with no trailing slash, so open-api/ and /open-api are the same thing. The raw spec follows it: <path>/json plus a <path>.json alias.

Defaults preserve today’s look; set these to tell several instances apart.

VariableDefaultRequiredPurpose
PADDOCK_BRAND_NAMEPaddocknoWordmark + browser tab title.
PADDOCK_BRAND_LOGO🐎noAn emoji/glyph, or a URL/path to an image (rendered as <img>).
PADDOCK_BRAND_ACCENT#c2603cnoAccent color (hex) for primary buttons + the logo chip.

Off unless configured; then a mic button appears in the composer. Mirrors HushPod’s whisper config so both can share a backend. See DEV.md.

VariableDefaultRequiredPurpose
PADDOCK_WHISPER_MODEoff (or remote if an endpoint is set)nooff | remote | local. Unknown → off.
PADDOCK_WHISPER_ENDPOINT(remote)OpenAI-compatible base URL, e.g. http://whisper.local:8385/v1 (/audio/transcriptions is appended). Its presence flips the default mode to remote.
PADDOCK_WHISPER_API_KEYno(remote) Optional bearer token for the endpoint.
PADDOCK_WHISPER_MODELbasenoWhisper model (tiny/base/small/…; .en variants for English-only).
PADDOCK_WHISPER_LANGUAGEnoOptional spoken-language hint (e.g. en); unset ⇒ auto-detect.
PADDOCK_WHISPER_MAX_UPLOAD_BYTES26214400 (25 MiB)noMax accepted dictation upload size.
VariableDefaultRequiredPurpose
PADDOCK_KEEPER_DRIVE_MODEsessionnoBox-wide default for how keeper turns are driven. session (the built-in default since v0.36) enables cross-turn autonomy (ScheduleWakeup / /loop) and token-by-token streaming; batch is one-shot per turn. A per-project driveMode overrides this at dispatch. Unknown → default.
PADDOCK_MODELS(every catalog model)noComma-separated allow-list of built-in catalog model ids (e.g. claude-opus-5,claude-sonnet-5) the model picker and the per-project default may offer. Unset ⇒ every catalog model is offered. Unknown, blank and duplicate ids are dropped silently, and if nothing valid survives the full catalog is offered again — an instance never ends up offering zero models. A per-project list can narrow this further, never widen it. See Model allow-lists.
PADDOCK_KEEPER_NATIVE_PROMPTtruenoKeeper and scratch agents use the native Claude Code system prompt + CLAUDE.md hierarchy. Set 0/false/no for the terse Paddock “replace” prompt (e.g. an instance with no CLAUDE.md).
PADDOCK_SELF_MCPfalsenoGive keepers the read-only self-management MCP (mcp__paddock_manage__*: enumerate projects/chats, read another chat’s transcript). Never injected on scratch turns.
PADDOCK_SELF_MCP_WRITEfalsenoAdditionally give keepers the self-management write tools (create_chat, fork_chat, send_message, fork_chat_batch). Only honored when PADDOCK_SELF_MCP is also on (write implies read).
PADDOCK_SELF_MCP_PROJECTSfalsenoAdditionally give keepers the self-management project tool (create_project) — provisioning a whole new project, cloning a repo when repo-backed. Gated separately from the other write tools because it creates instance-level state and clones a caller-supplied git URL. Only honored when PADDOCK_SELF_MCP and PADDOCK_SELF_MCP_WRITE are also on.
PADDOCK_MAX_SPAWN_DEPTH1noHow deep a spawn tree may grow before spawned children stop receiving the self-management MCP: a spawned turn at depth d gets it (including the write tools, so a child can send_message back to its parent) only while d ≤ this value. 0 means no spawned child ever gets it. A per-project maxSpawnDepth overrides this at dispatch; an out-of-range value falls back to the default rather than failing startup. Only meaningful when the write self-MCP is on — spawning needs those tools.
PADDOCK_SCHEDULE_MUTATIONfalsenoAllow schedules to be created / edited / deleted programmatically at runtime (the Schedules REST routes and the trigger MCP tools). Off by default, so a plain instance’s schedules can only change by editing project.yaml. Schedules declared statically in project.yaml are armed either way. Accepts 1/true/yes. See Scheduling & the schedule gates.
PADDOCK_HOOKS_MCPfalsenoInstance default for the hook/trigger-management tools (list_triggers / set_trigger / remove_trigger) — a keeper declaring and editing its own event hooks and schedules. Off by default; a per-project hooksMcpEnabled in project.yaml overrides it. Only honored when the self-management write MCP is also on; when off the tools are absent (not present-but-refusing). Accepts 1/true/yes.
PADDOCK_BROWSER_MCP(off)noWhen =1, inject a headless-Chromium Playwright MCP into keepers (browse/screenshot).

Unstick a keeper that hangs when a background task is killed at the turn boundary. See Keeper-chat recovery for the full story; each knob has a per-project recovery override in project.yaml.

VariableDefaultRequiredPurpose
PADDOCK_RECOVERY_SURFACEtrue (ON)noLayer 2. Surface a killed/stopped background-task notification as a “keeper is idle” affordance with a one-click Continue button. Accepts 1/true/yes.
PADDOCK_RECOVERY_AUTODRIVEfalse (OFF)noLayer 3. Automatically re-drive a hung keeper — Paddock detects the killed task and injects the nudge on its own (debounce + retry-cap guarded). Off by default (it acts unattended and costs a turn).
PADDOCK_RECOVERY_DEBOUNCE_MS5000noLayer 3: quiet window (ms) after a killed task before auto re-drive fires. Non-negative integer, else the default.
PADDOCK_RECOVERY_MAX_RETRIES1noLayer 3: per-session cap on auto re-drives (no poke-loops). Non-negative integer, else the default.
PADDOCK_RECOVERY_LIMBO_MS0 (off)noLayer 2 backstop: surface a kept-alive session as stuck after this many ms of silence following a killed task. 0 disables it. (Backstop timer ships in a follow-up — config only for now.)

Gate the composer’s file/image upload (v0.38). All four knobs also take a per-project attachments override in project.yaml (each field inherits the instance default when unset), resolved at request time. See Sending files & images for the feature.

VariableDefaultRequiredPurpose
PADDOCK_ATTACHMENTS_ENABLEDtrue (ON)noMaster switch for inbound composer uploads. When off, the upload endpoint 403s and the composer hides its picker / drop / paste affordances. Accepts 1/true/yes.
PADDOCK_ATTACHMENTS_MAX_FILE_SIZE_MB25noPer-file size cap in MB (1 MB = 1024×1024 bytes). A larger file is rejected before it’s written. Must be a positive integer, else the default.
PADDOCK_ATTACHMENTS_MAX_FILES_PER_MESSAGE10noHow many files a single message may carry. Enforced client-side (tray cap) and server-side (per upload request + at send). Positive integer, else the default.
PADDOCK_ATTACHMENTS_ALLOWED_TYPES* (allow all)noComma-separated allow-list of MIME patterns (image/*, application/pdf) and/or extensions (.csv, .pdf). A file passes if its MIME matches any pattern or its extension matches any extension entry; the sentinel * allows everything. A hygiene/UX guardrail, not a security boundary (client-provided types, no magic-byte sniffing).
VariableDefaultRequiredPurpose
PADDOCK_GIT_AUTHOR_NAMEPaddocknoAuthor name for commits the server makes on the backing store.
PADDOCK_GIT_AUTHOR_EMAILpaddock@localhostnoAuthor email for those commits.
PADDOCK_GITHUB_CLIENT_ID(for GitHub auth)GitHub OAuth client id enabling the device-flow connect. Without it the GitHub-auth feature reports “not configured”; invoking a flow throws.

Per-file token budgets the post-turn sweeper keeps its three curated files under. These bound the context every chat in a project pays for: CHANGELOG.md and OVERVIEW.md are injected into the project-context preload, and CLAUDE.md auto-loads on every turn. The sweeper is told each budget so it prunes and de-duplicates to fit, and the server enforces it as a backstop. Each one also takes a per-project curation override in project.yaml, field by field.

VariableDefaultRequiredPurpose
PADDOCK_CURATION_OVERVIEW_MAX_TOKENS2000noBudget for OVERVIEW.md, which the sweeper regenerates wholesale each time.
PADDOCK_CURATION_CHANGELOG_MAX_TOKENS8000noBudget for CHANGELOG.md. The biggest lever — it’s the largest of the three and it rides in the preload.
PADDOCK_CURATION_CLAUDEMD_MAX_TOKENS6000noBudget for the curated-notes section of CLAUDE.md. Mind the name: the variable says CLAUDEMD but the config-file key is curation.claudeMaxTokens.

Each must parse to a positive integer; anything else (zero, negative, non-numeric, blank) falls back to the default rather than failing startup.

VariableDefaultRequiredPurpose
PADDOCK_SWEEP_MIN_INTERVAL_MS300000 (5 min)noMinimum interval between post-turn per-project sweeps. Must parse to a finite number ≥ 0, else ignored (falls back to the 5-min default).
PADDOCK_SPIKE_TRIGGER(off)noDev harness only (spike.ts): when =1, fire a real keeper trigger instead of a dry run. Not used by the running server.
VariableDefaultRequiredPurpose
CLAUDE_CODE_OAUTH_TOKENconditionalClaude Max auth for the CLI runtime (the default). Read from the server’s environment and passed through to the spawned claude CLI; never written to config. Provide this or ANTHROPIC_API_KEY.
ANTHROPIC_API_KEYconditionalClaude auth for the SDK runtime (API pricing). Alternative to CLAUDE_CODE_OAUTH_TOKEN.
LOG_LEVELinfonoFastify/pino log level (fataltrace).

Claude credentials are consumed by the runtime (the claude CLI subprocess or the SDK), not read directly by Paddock server code — but the server process must have one in its environment for keeper turns to run.

Read by the Vite build/dev server (packages/web), not the backend:

VariableDefaultRequiredPurpose
PADDOCK_DEV_PORT5173noVite dev-server port (hot-reload mode).
PADDOCK_PROXY_TARGEThttp://localhost:4000noBackend origin the Vite dev server proxies /api + /ws to (WS target derived by swapping httpws).
VITE_API_BASE(same-origin)noBuild-time: point the SPA at a non-default API origin.
VITE_WS_BASE(same-origin)noBuild-time: point the SPA at a non-default WebSocket origin.