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.

Running via npx and have no checkout to copy that file from? The CLI’s own flags cover the common cases without any environment at all — --port, --host, --data-dir, --here. Run npx @edspencer/paddock --help for the full list; every PADDOCK_* variable below still works if you export it first.

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_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, 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 an agent’s working dir).
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).
PORT7233noHTTP/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_CONFIG_DIR<dataDir>/claude-homenoWhere Paddock’s own Claude home goes — the directory whose projects/<encoded-cwd>/ folders hold Claude Code’s session transcripts, and the value handed to Claude Code as its config dir. Paddock always owns this directory (#691): the data dir is a single relocatable root, and the user’s ~/.claude is a read-only source Paddock bridges config out of but never runs as. This variable is honoured (rather than ignored) because it is Claude Code’s own, and herdctl deliberately refuses to clobber an operator-set value (herdctl#423) — if Paddock disagreed with it, the SDK would write transcripts to one tree while herdctl read from another, and chats would list from one directory and open empty from another (#588). A claudeHome: key in the config file sits beneath it. Paddock refuses to start if this resolves to your own ~/.claude — that is the one value that re-welds every concern to a single lever and breaks agent memory (an agent cannot write to any path with a .claude component, #690). To share your real transcripts, use claude.transcripts: host instead; it shares the files without moving the home. Resolved once at startup into PaddockConfig.claudeHome (resolveClaudeHome() in config.ts) and threaded to both consumers: Paddock’s transcript relocation and import detection (ensureProjectChats in transcripts.ts, AdoptableIndex in adoptable.ts), and the engine, as FleetManagerOptions.claudeHomePath (herdctl.ts). Note that Claude Code scopes its credential store to whether this is set at all, so a keychain login made against the default home is not visible under Paddock’s — which is what PADDOCK_CLAUDE_CREDENTIALS (below, default host) exists to undo. Paddock warns at boot when it can find no credential source at all.
PADDOCK_CLAUDE_TRANSCRIPTSownnoWhose session transcripts this instance uses (#691) — the env override for the claude.transcripts key. own keeps them in each project’s .chats/, inside the data dir. host shares your real ~/.claude/projects/<encoded-cwd>/ folder live, in both directions: a Paddock chat and a claude --resume in the same directory are the same file. Under host, deleting a chat releases it rather than removing it — it is your history, not Paddock’s copy (#689). See the config file.
PADDOCK_CLAUDE_CREDENTIALShostnoWhose Claude Code login this instance uses (#691) — the env override for the claude.credentials key, and the one key in that block whose default is host rather than own. host uses the login already on this machine: on macOS the Keychain entry a plain claude /login wrote, elsewhere your ~/.claude/.credentials.json (symlinked into Paddock’s home, never copied). own uses only what is inside Paddock’s own Claude home — a token in the environment, or a CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login. The default is host because reading a login writes nothing, while isolating it by default produces an instance that boots cleanly and fails every turn with “Not logged in” (#683). Mechanically, host sets CLAUDE_SECURESTORAGE_CONFIG_DIR="" in the environment the runtime gets: Claude Code scopes its secure storage to that variable instead of CLAUDE_CONFIG_DIR when it is defined, and the empty value selects the unsuffixed service name — so the login is shared without Paddock’s Claude home moving anywhere. Set the variable yourself to a non-empty value and Paddock honours it over this key.
PADDOCK_CLAUDE_INSTRUCTIONSownnoWhose user-level instructions this instance loads (#691) — the env override for the claude.instructions key. Governs your ~/.claude CLAUDE.md, agents/, commands/ and plugins/: inert content the model reads or invokes by name, none of which runs a command on its own. own loads none of them; host symlinks all four in, which is what every version before 0.62 did unconditionally. It is also the gate for plugins. The symlink alone never made one work — enablement lives in enabledPlugins in the user settings source, which Paddock’s agents do not load — so since 0.63 Paddock enumerates the host’s installed plugins and passes them to the engine as session plugins, which are enabled by default. host here is what turns that on; PADDOCK_CLAUDE_MCP_SERVERS then decides whether a plugin’s MCP servers come with it. This default is a reversal with a real cost — a curated ~/.claude/CLAUDE.md stops reaching your agents, silently — and it is the default anyway so that “own everywhere means nothing outside the data dir is read or written” is a guarantee rather than a footnote. Paddock names this key at startup when it finds files it is not loading. Each project’s own CLAUDE.md is unaffected either way.
PADDOCK_CLAUDE_HOOKSownnoWhether this instance runs the host machine’s Claude Code hooks (#691) — the env override for the claude.hooks key, and the only lever in the block that governs code execution rather than data. Hooks are shell commands ~/.claude/settings.json binds to tool use and session lifecycle; before 0.62 they were inherited unconditionally, so every hook you had configured ran inside every Paddock turn with no way to stop it. own drops them; host symlinks your settings.json in whole. Because that file is a mixed bag (hooks and permissions, model, statusLine, enabledPlugins), own cannot be a symlink decision: Paddock writes its own settings.json carrying your other keys with hooks removed, regenerated at each startup — so a restart is what applies an edit to yours. A settings.json you put in Paddock’s own home is recognised by hash and never overwritten. An unparseable source plants nothing rather than falling back to the symlink. Scope: this means no host hooks, not no host commandsapiKeyHelper, awsAuthRefresh, statusLine and friends are still inherited.
PADDOCK_CLAUDE_MCP_SERVERSownnoWhose MCP servers this instance’s project agents get (#691) — the env override for the claude.mcpServers key. own attaches only the servers Paddock provides itself (send_file, the optional self-management tools, the optional browser server); host also attaches the ones you have declared with claude mcp add — the top-level mcpServers of your ~/.claude.json plus any scoped to a project’s own working directory (projects.<abs-dir>.mcpServers). Note the path: MCP servers are declared in ~/.claude.json, a sibling of ~/.claude rather than a file inside it, which is why they were the one thing Paddock’s config bridge structurally could not reach. Paddock reads that file and passes the servers to the runtime; it never symlinks or writes it, because Claude Code keeps mutable state there (per-project trust, approvals) that is yours. Read once, at startup — add a server and restart Paddock to pick it up — and the boot log names every server it attached. Since 0.63 (herdctl 5.32.0) a server’s type and headers are carried through verbatim, so a bearer-authenticated server keeps its header and an sse server is connected to as SSE; the 0.62 warnings about both are gone. The only host server still dropped is one declaring neither a command nor a url. MCP OAuth tokens live in the same credential store as your Anthropic login — and are keyed on a hash of {type, url, headers}, which is why carrying those fields is what makes PADDOCK_CLAUDE_CREDENTIALS=host work for an OAuth server at all. Plugin-provided MCP servers are covered, but only alongside PADDOCK_CLAUDE_INSTRUCTIONS=host — see the config file page for the truth table.

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 is config-file-first: the whole managementApi block is set in the file, with exactly one environment override

VariableDefaultRestart?What it does
PADDOCK_MANAGEMENT_TRUSTED_PROXIESloopback,linklocal,uniquelocalyesOverrides managementApi.trustedProxies: which peers may be believed when they say a /mcp request arrived over HTTPS (X-Forwarded-Proto) and who it came from. Comma-separated IPs, CIDRs, or the preset names loopback / linklocal / uniquelocal. The environment wins over the file; saying nothing yields the compatibility default above rather than a strict list.

The environment’s other job here 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.

The top-level mcpServers: block — where you declare an MCP server to this instance — is likewise config-file-only, and borrows the same indirection. Anywhere it expects a string (command, an args entry, an env value, url, a headers value), env:VAR_NAME reads that value from the environment instead:

mcpServers:
notion:
command: npx
args: ["-y", "@notionhq/notion-mcp-server"]
env:
NOTION_TOKEN: env:NOTION_TOKEN
Terminal window
NOTION_TOKEN=ntn_…

The variable name is entirely yours — Paddock reads whatever the reference names, with no PADDOCK_ convention, because these are third-party servers’ own variables. An unset or blank one drops that server with a warning naming the variable, rather than starting it without its credential. Nothing Paddock logs or serves ever contains a value from this block.

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_DRIVE_MODEsessionnoBox-wide default for how 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_NATIVE_PROMPTtruenoAgents 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 Claude the read-only self-management MCP (mcp__paddock_manage__*: enumerate projects/chats, read another chat’s transcript).
PADDOCK_SELF_MCP_WRITEfalsenoAdditionally give Claude 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 Claude the self-management project tools (create_project, promote_project) — provisioning a whole new project, or promoting an existing managed (notebook) project into an unmanaged one backed by a repo — cloning a caller-supplied URL either way. 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) — Claude 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_ENVIRONMENT_PROMPT(Paddock’s built-in text)noText appended to every keeper turn’s system prompt, telling the agent it renders into a browser as GitHub-Flavored Markdown rather than into a terminal. Any value replaces the built-in text entirely. See below, and the environment prompt.
PADDOCK_BROWSER_MCP(off)noWhen =1, inject a headless-Chromium Playwright MCP into the agent (browse/screenshot).

The environment prompt is the one place blank is not unset

Section titled “The environment prompt is the one place blank is not unset”

PADDOCK_ENVIRONMENT_PROMPT breaks the “blank is unset” rule at the top of this page, on purpose: an empty value is how you opt out, so there has to be a difference between “unset” and “set to nothing”.

Terminal window
# unset → Paddock's built-in two-rule prompt is appended
PADDOCK_ENVIRONMENT_PROMPT="Link every Jira key as a URL." # → that, instead
PADDOCK_ENVIRONMENT_PROMPT="" # → nothing appended

Because it is defined-ness rather than emptiness that decides, an exported-but-empty PADDOCK_ENVIRONMENT_PROMPT still shadows the config file — and the Settings screen correctly renders the field read-only in that case. PADDOCK_BROWSER_MCP behaves the same way, for the same reason.

The value is used verbatim: no trimming, no escaping. Leading indentation and trailing newlines survive.

Unstick a chat that hangs when a background task is killed at the turn boundary. See 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 “Claude is idle” affordance with a one-click Continue button. Accepts 1/true/yes.
PADDOCK_RECOVERY_AUTODRIVEfalse (OFF)noLayer 3. Automatically re-drive a hung chat — 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).
VariableDefaultRequiredPurpose
CLAUDE_CODE_OAUTH_TOKENconditionalClaude Max plan auth. Read from the server’s environment and passed through to the claude process the runtime spawns; never written to config. Provide this or ANTHROPIC_API_KEY.
ANTHROPIC_API_KEYconditionalClaude API-key auth (API pricing). Alternative to CLAUDE_CODE_OAUTH_TOKEN.
CLAUDE_SECURESTORAGE_CONFIG_DIR(set by Paddock)noWhere Claude Code scopes its secure credential store — it uses this instead of CLAUDE_CONFIG_DIR whenever it is defined. This is the mechanism behind claude.credentials: under the default host, Paddock sets it to the empty string, which selects the unsuffixed keychain service name and so shares the login a plain claude /login wrote; under own it unsets the variable, so the store falls back to Paddock’s own Claude home. An operator-set non-empty value wins over the key in either mode, and Paddock reports that it is honouring yours at startup. (An empty value is not treated as yours — that is exactly what host writes.) Set it yourself only if you keep credentials somewhere neither host nor own describes. It is also where MCP OAuth tokens live (under an mcpOAuth key in the same store), so it moves those too.
LOG_LEVELinfonoFastify/pino log level (fataltrace).
HERDCTL_LOG_LEVELinfono@herdctl/core’s own logger (the [fleet-manager] / [CLIRuntime] lines), which pino’s level cannot reach. Paddock routes these through a handler that cuts the reconstructed claude argv out of agent-failure messages — a claude -p command line carries the whole system prompt and is noise in a log (#684). Set this to debug to get the full command back.
PADDOCK_QUIETnoSet by the paddock CLI unless --verbose. Collapses a recognised, non-fatal background failure (no login, no credit, no claude on PATH) to one actionable line instead of a stack trace; an unrecognised failure always keeps its full detail. A level alone could not do this — these are logged at error, above every threshold.

Which auth you use is independent of the runtime — either credential works on both the SDK runtime (chats) and the CLI runtime (the sweeper, triggers, driveMode: batch). Credentials are consumed by the runtime, not read directly by Paddock server code — but the server process must have one in its environment for 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:7233noBackend 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.