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_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). If you bind a non-loopback host (e.g. 0.0.0.0) while authentication is none, startup fails closed with a clear message — mirroring the jwt-without-JWKS check. Fix it by putting a real auth mode (trusted-header/jwt) or a reverse proxy in front (no flag needed), keeping the bind on loopback, or — only if you genuinely want an open server — setting PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 (boots with a warning).

Inside a container the network namespace is the isolation boundary and Docker can’t reach 127.0.0.1 inside the container, so the image keeps binding 0.0.0.0; the safe posture there is carried by the deploy recipe’s port-publish (e.g. -p 127.0.0.1:4000:4000), not this app-level guard.

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.

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://192.168.1.200: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_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_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.
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.