Skip to content

Configuration

Shepherd is configured entirely through environment variables (read in src/config.ts). Per-deployment overrides go in ~/.shepherd/env (KEY=value lines), read by the systemd unit if present.

Variable Default Purpose
SHEPHERD_PORT 7330 HTTP/WS listen port
SHEPHERD_HOST 127.0.0.1 Bind address; loopback-only by default (set 0.0.0.0 to expose all NICs)
SHEPHERD_AGENT_INGRESS_PORT SHEPHERD_PORT + 1 (e.g. 7331) Pinned loopback port for the auth-exempt agent-ingress listener (agent hook callbacks plus the agent control plane: the build-queue/epic-draft routes and the per-session MCP endpoint). Stable so the URL baked into a live agent’s --settings/--mcp-config survives restarts/deploys; validated at startup against collisions with the main port, served port, or preview range. Set 0 for an ephemeral port (the pre-pinning behavior)
SHEPHERD_DB ~/.shepherd/shepherd.db SQLite session store path
SHEPHERD_BACKUP_DIR ~/.shepherd/backups (next to the DB) Destination dir for the automated hourly SQLite backups (Linux backup timer); see Operating Shepherd
SHEPHERD_REPO_ROOT ~ (home) Repos must live under this root (spawn is confined to it)
SHEPHERD_ALLOWED_HOSTS localhost,127.0.0.1,::1,[::1] Comma-separated origin hostnames allowed for writes + WS (CSRF/CSWSH guard). Two sets of hosts are appended automatically at boot, so this var is not fully authoritative over the effective allowlist: (1) the Shepherd Capture extension’s two fixed IDs (the published Web Store item and the pinned unpacked dev build), so a stock install accepts captures with no pairing step; and (2) every Tailscale-served host that fronts this HUD’s port — the node’s own tailnet name (a direct tailscale serve) and any Tailscale Service front (e.g. svc:shepherdshepherd.ts.net), discovered from tailscale serve status at startup (#1645). Only a host that does not appear in tailscale serve status — a non-Tailscale reverse proxy or custom-DNS front — still needs a manual entry here. Preview-port origins stay rejected regardless (the guard’s preview-range check runs before the hostname check)
SHEPHERD_PASSWORD (auto-generated) Single-operator login password. When set it’s authoritative — argon2id-hashed and re-seeded into the persisted hash every boot. Unset → the persisted hash is reused, or (first boot) a strong password is generated, hashed, persisted, and printed to the log once. The browser exchanges it for an HMAC-signed session cookie that gates every HTTP route plus the /events + /pty WebSocket channels
SHEPHERD_COOKIE_SECRET (generated + persisted) HMAC secret that signs the session cookie. Set it to pin a stable secret across DB resets; rotating it invalidates every outstanding session (the all-sessions kill-switch)
SHEPHERD_TOKEN (none) Optional operator bearer for CLI/curl/machine clients: when set, Authorization: Bearer <token> is accepted as an alternative to the session cookie. Browser operators use the password login instead; spawned agents don’t use this (they reach the server over the loopback ingress). This is the deployment-provisioned option — for a client you install rather than deploy (a launcher extension, the Capture extension, a cron job), prefer minting a named token in the HUD under Settings → Access, which needs no restart and can be revoked one client at a time (#2082). A minted token also carries a scoperead, submit or full — chosen when you create it and fixed afterwards, so a read-only client cannot spawn sessions or reach a terminal (#2083). SHEPHERD_TOKEN itself is unscoped: it is the break-glass credential and always has full reach. Both are accepted at once
HERDR_BIN herdr Path to the herdr binary
HERDR_SESSION default herdr session name
HERDR_SOCKET_PATH (derived from HERDR_SESSION) Unix-socket path for herdr’s native JSON-RPC API. When unset it’s derived: a non-default HERDR_SESSION uses its own per-session socket (~/.config/herdr/sessions/<name>/herdr.sock); the default session uses herdr’s top-level socket (~/.config/herdr/herdr.sock). An explicit value normally wins — except when Shepherd runs inside a herdr pane (HERDR_ENV=1) and the value was inherited from that pane while a non-default HERDR_SESSION is set: the explicit HERDR_SESSION then wins (Shepherd prefers its per-session socket and warns), so a dev/test instance can’t silently attach to the parent pane’s herd (#1596). Set SHEPHERD_HERDR_IGNORE_SESSION=1 to keep the inherited socket. Consulted by the socket driver and, via process.env, by every spawned herdr CLI
SHEPHERD_HERDR_SOCKET 0 (off) Opt-in: talk to herdr over its native Unix-socket JSON-RPC API instead of shelling out to the herdr CLI for every call (issues #1529, #1553, #1567). Covers the async read surface plus the entire async write surface — the spawn/teardown/rename writes (start/stop/relabel/closeTab) and send (writing text to an agent’s PTY). Only the synchronous list/read/tabs/panes still shell out, because a sync call can’t await a socket round-trip without blocking the event loop. It does not by itself move the browser terminal onto the socket — that is a separate, still-default-off sub-flag (SHEPHERD_HERDR_SOCKET_TERMINAL, below). Default-off because the socket protocol is still preview-unstable; the driver falls back to the CLI on any protocol mismatch, so enabling it is reversible
SHEPHERD_HERDR_SOCKET_TERMINAL 0 (off) Interim sub-flag of SHEPHERD_HERDR_SOCKET: set 1 to stream the browser terminal of agent sessions over herdr’s socket terminal session control instead of the node-pty helper (each /pty connection attaches directly to the resolved pane; a per-terminal failure falls back to node-pty for a short cooldown so a bad attach doesn’t strand the session). Default-off because that stream is a screen-diff/redraw protocol: xterm builds no scrollback and never sees the app’s mouse mode, so mobile swipe + desktop wheel scrolling stop working. A live probe on herdr 0.7.3 (#1639) found Claude Code honours PageUp but Codex honours no scroll lever at all, so flipping this on would make a Codex session’s off-screen transcript unreachable. With it off, agent terminals stay on node-pty (scrollable for both providers) even while the socket driver runs everything else. Clean-terminal sessions are the exception: an agentless pane can’t be attached with herdr agent attach, so those always ride the socket bridge regardless of this flag
SHEPHERD_HERDR_IGNORE_SESSION 0 (off) Escape hatch for the in-pane session/socket conflict (#1596): when Shepherd runs inside a herdr pane and a non-default HERDR_SESSION disagrees with the pane-inherited HERDR_SOCKET_PATH, it normally prefers the session’s own socket. Set to 1 to suppress that override and keep the inherited socket (attach to the parent pane’s herd), ignoring the HERDR_SESSION hint
SHEPHERD_FORGES ~/.shepherd/forges.json Path to the git-host config
SHEPHERD_PLUGINS_DIR ~/.shepherd/plugins (next to the DB) Directory scanned at boot for server-side plugins (private/out-of-repo extensions). Lives alongside the state DB so plugins survive bun run update and never leak into the public repo; a missing/empty dir loads nothing. See Server-side plugins
SHEPHERD_SANDBOX_DEFAULT_PROFILE trusted Default sandbox profile for every spawned agent (trusted / standard / autonomous) — see below
SHEPHERD_SANDBOX_EXTRA_HOSTS (none) Comma-separated extra hostnames always allowlisted by the autonomous profile’s egress firewall, on top of the built-in Anthropic + forge hosts (e.g. registry.corp.com,pypi.corp.io for a private package registry). Operator escape hatch; no effect on trusted/standard, which are not network-confined
SHEPHERD_TRUST_ISSUE_AUTHORS 0 (off) Opt-in escape hatch for the fail-closed author-trust gate on autonomous (auto=true) drain. Set 1 to treat issue authors as trusted on forges that can’t supply a GitHub-style authorAssociation (non-GitHub — Gitea/local), where autonomous drain would otherwise be silently disabled. Does not relax the gate on GitHub, where author trust is verifiable — a GitHub miss or untrusted author still refuses. See the Security page
SHEPHERD_TRIM_AUTO_CONTEXT true Trim the per-turn context of auto-spawned (drain) agents (optional plugins, bundled skills and your personal ~/.claude/skills disabled per-spawn; the repo’s own skills stay available). Interactive sessions untouched. Set false/0/off if drain quality regresses
SHEPHERD_REVIEW_TIMEOUT_MS 600000 (10 min) Hard deadline for a single critic run (session critic and standalone PR critic) before it is abandoned with an error verdict. Clamped to 600003600000; an unparseable value falls back to the default. Env-only, deliberately not a UI knob: raise it for a repo whose PRs genuinely outgrow 10 minutes, because the critic restarts from scratch on every retry — so a PR that can’t finish inside the deadline is permanently un-reviewable rather than slowly reviewed
SHEPHERD_USAGE_HOLD_ENABLED true Queue newly submitted tasks instead of spawning them while account usage is high (auto-released as usage falls). Set 0/false to always spawn immediately
SHEPHERD_USAGE_HOLD_PCT 80 Hold threshold: when the higher of the 5-hour / weekly usage window reaches this percent, new tasks are held. Range 0100
SHEPHERD_USAGE_HOLD_AUTO_RELEASE true When on, the ~30 s sweeper auto-starts held tasks once usage drops back below the threshold. Set 0/false to keep held tasks queued until the operator starts (or discards) each one manually from the held-tasks popover. Turning the gate off entirely (SHEPHERD_USAGE_HOLD_ENABLED=0) still flushes everything regardless of this flag
SHEPHERD_USAGE_DOWNGRADE_ENABLED false Companion to the usage hold: when on, every newly spawned agent (main task agents and the role agents) runs on SHEPHERD_USAGE_DOWNGRADE_MODEL instead of its configured model once usage reaches the downgrade threshold — work keeps flowing, just cheaper. Opt-in (no behavior change when off); set 1/true to enable
SHEPHERD_USAGE_DOWNGRADE_PCT 70 Downgrade threshold: when the higher of the 5-hour / weekly usage window reaches this percent, new spawns are downgraded. Range 0100; default 70 is deliberately below SHEPHERD_USAGE_HOLD_PCT (80) so usage downgrades first and only later holds
SHEPHERD_USAGE_DOWNGRADE_MODEL haiku Model the downgrade routes spawns to while active — a default-model setting (auto / default / <alias>)

Detecting the dev servers agents start is platform-specific (#1912). On Linux Shepherd reads /proc live. On macOS it runs one lsof call per refresh and serves every probe from that short-lived snapshot; previews there stay loopback-only, and stopping one from the UI works but is bounded — the snapshot must be within SHEPHERD_PREVIEW_KILL_MAX_AGE_MS and the candidate process is re-checked live before any signal, otherwise the stop is refused and reported as such (see the platform table in Getting started). On any other platform there is no detection backend, so previews never bind. The Preview detection row in Settings → Diagnose reports which case a host is in — see Operating Shepherd.

Variable Default Purpose
SHEPHERD_PREVIEW_PORT_BASE 8001 First port in the live-preview range (one port per agent preview)
SHEPHERD_PREVIEW_PORT_COUNT 16 Size of the preview range and max concurrent previews
SHEPHERD_PREVIEW_SWEEP_MS 4000 Cadence (ms) of the dev-port detection sweep across active sessions. On macOS it also paces the lsof snapshot refresh (coalescing window: half the cadence) and sets how old that snapshot may get before it stops being allowed to prove a port is gone2 × cadence + 4 s; past that, sweeps skip rather than tear a bound preview down
SHEPHERD_PREVIEW_KILL_MAX_AGE_MS 10000 How old the macOS lsof snapshot may be and still authorize a preview-stop signal. Deliberately independent of the sweep cadence: reusing that bound would let a tuned-up cadence widen the window in which stale data may authorize a SIGKILL. Past it, a stop is refused (and reported as such) rather than sent. No effect on Linux, which reads live /proc
SHEPHERD_PREVIEW_AUTO_SERVE true Dynamically register/unregister tailscale serve mappings as previews bind/tear down; set 0 to map the range manually
SHEPHERD_PREVIEW_IDLE_STOP_MS 0 (disabled) When > 0, an idle previewed dev server with no proxy traffic for this many ms is stopped to reclaim RAM (no auto-wake; suggested 1800000 = 30 min). On macOS each signal is gated on SHEPHERD_PREVIEW_KILL_MAX_AGE_MS plus a live re-check of the candidate process; when either fails nothing is signalled — the escalation ladder stays put and logs once per session instead of burning SIGTERM → SIGKILL
Variable Default Purpose
SHEPHERD_NODE_COMPILE_CACHE (disk dir) Node compile-cache dir (kept off the /tmp tmpfs)
SHEPHERD_TMP_INODE_PCT 80 Inode-sweep threshold (% of /tmp inodes) — also the warning band of the Temp filesystem inodes Diagnose row (row bands stay ordered: >95 raises the error band too; outside (0, 100] the row falls back to 80, the sweep still honours it)
SHEPHERD_TMP_STALE_HOURS 24 Scratch staleness cutoff
SHEPHERD_TMP_SWEEP_DIR (default tmp root) Override the swept tmp root

See Operating Shepherd for the host-level /etc/fstab belt.

A background sweep (#1144) that SIGKILLs a process only when it both (a) carries the archived session’s SHEPHERD_SESSION_ID in its /proc/<pid>/environ (provenance — an agent, or a descendant that inherited the marker, spawned it) and (b) belongs to a session whose row is present and archived (the agent is definitively done). Attribution is by env marker, not working directory, so it survives cd, backgrounding, and worktree deletion — and an operator’s own processes (which never carry the marker) can never be candidates. The CPU/age pair below is a performance prefilter that keeps the sweep’s /proc/<pid>/environ reads near zero, not a safety floor.

Every signal Shepherd sends — the reaper’s and the other kill paths’ alike — is additionally bracketed against pid recycling (#1925): the process’s /proc/<pid>/stat start time is captured, the facts that qualified it (cwd, comm) are re-read, and the start time is checked again, so a pid the kernel handed to an unrelated process in the meantime is never hit. It fails closed — a candidate that can’t be bracketed is not signalled, and it also stops being offered, so the operator is never shown a leftover the reap would refuse. The one residual is inherent: start time has 10 ms granularity, so same-tick pid reuse is indistinguishable.

Variable Default Purpose
SHEPHERD_REAP_RUNAWAY armed Reaper mode. armed (the default — any unset/unrecognised value) SIGKILLs qualifying orphans; observe runs every gate but only logs (never signals); 0/off disables the sweep entirely
SHEPHERD_REAP_RUNAWAY_MIN_CPU 0.8 CPU prefilter: fraction of one core, averaged over the process’s whole lifetime, a candidate must have burned before it can be reaped. Clamped to 0.051 (a set-but-empty value clamps rather than dropping the gate)
SHEPHERD_REAP_RUNAWAY_MIN_AGE_S 300 Minimum process age (seconds) before a candidate can be reaped — the floor that keeps a freshly restored session’s briefly-archived row from being reaped. Clamped to a hard 60s minimum (up to 24h)

Main agent terminal renderer (research preview)

Section titled “Main agent terminal renderer (research preview)”

Every spawned claude runs on Claude Code’s classic renderer by default — Shepherd’s poller/blocked classifier scrape the rendered viewport and the web terminal forwards xterm keystrokes, both of which assume the classic prompt. The operator can opt the main agent session (satellites always stay classic) into Claude Code’s opt-in fullscreen renderer. The choice applies to newly spawned/resumed sessions only and is also configurable from the Settings panel (persisted in the SQLite settings table); the env vars below seed a fresh DB.

Variable Default Purpose
SHEPHERD_TUI_FULLSCREEN 0 (off) Set 1 to opt the main agent session into Claude Code’s fullscreen renderer (research preview). Implies SHEPHERD_TUI_DISABLE_MOUSE.
SHEPHERD_TUI_DISABLE_MOUSE 0 (off) Set 1 to disable Claude Code mouse capture for the main agent session, so fullscreen mouse-capture escape sequences don’t leak into the web terminal’s keystroke stream.

Opt-in, default-off. Configurable from the Settings panel (persisted in the SQLite settings table); the env var below seeds a fresh DB.

Variable Default Purpose
SHEPHERD_UPNEXT_SKIP_CLI_PICKER 0 (off) Set 1 to make Up Next quick-start launch with the operator’s default coding CLI instead of opening the “Choose coding CLI” picker, even when more than one CLI is ready. Default off preserves the picker behavior.

Session revival (herdr daemon-restart recovery)

Section titled “Session revival (herdr daemon-restart recovery)”

When the herdr daemon restarts, it re-creates each pane as a bare shell while the agent process behind it is gone — a “stranded” husk whose conversation is no longer live. Shepherd detects these and surfaces them (a daemon-restart toast plus a herd banner with a revive all action, which force-resumes every stranded session). It can also revive them autonomously. Opt-in, default-off; configurable from the Settings panel (persisted in the SQLite settings table), and the env var below seeds a fresh DB.

Variable Default Purpose
SHEPHERD_AUTO_REVIVE 0 (off) Set 1 to seed autonomous auto-revive on for a fresh DB. When on, only the default-account complement of stranded sessions is auto-revived (account panes keep recovering via reDriveAccount); each revive is bounded so a persistently-refused session gives up rather than re-firing every sweep. Operators can still trigger a manual revive all from the HUD regardless of this flag (#1630)

Shepherd injects Claude Code lifecycle hooks into each spawned agent that POST to a restricted loopback ingress, giving the HUD push updates (tool activity, notifications, sub-agent roster, turn-Stop timing) on top of the 1 s poller — never instead of it. The path is fail-open: each hook is synchronous with a 5 s budget, so an unreachable or hung endpoint (e.g. an autonomous/egress agent whose netns route is down) simply times out and the poller stays authoritative.

Two independent stages, each an env override on a code default:

  • Ingest (SHEPHERD_HOOKS_INGEST) — injection + ingest route + ring-buffer/logging + the sub-agent roster fan-out. Observe-only: it never mutates session status. Default on as of the post-soak flip; set SHEPHERD_HOOKS_INGEST=0 to disable (the kill switch).
  • Signals (SHEPHERD_HOOKS_SIGNALS) — feed matched hook events into the poller’s signal pipeline. Still opt-in; meaningful only when ingest is also on (with ingest off, no events arrive to feed, and Shepherd warns and treats signals as off).
Variable Default Purpose
SHEPHERD_HOOKS_INGEST 1 (on) Inject observe-only lifecycle hooks into spawned agents (ingest route, ring buffer/logging, sub-agent roster fan-out). No status consumption; additive + fail-open. Set 0 to disable entirely (kill switch)
SHEPHERD_HOOKS_SIGNALS 0 (off) Set 1 to forward matched hook events into the poller’s signal pipeline. Meaningful only when SHEPHERD_HOOKS_INGEST is also on

Separate from the ingest hooks above: a local PreToolUse hook on the Bash tool that denies two hazards at the call site instead of warning about them in every agent’s standing prompt — a bare git stash (the stash stack is shared across worktrees) and a worktree-add or dependency install under a tmpfs root. The refusal carries the explanation, so the agent learns why only when it matters. It runs as a local command hook (not the fail-open HTTP ingest transport) so the deny still holds for unattended, sandboxed sessions, and it is bound into the bwrap membrane so it exists inside the sandbox too. Claude spawns only — Codex spawns have no such mechanism and keep the equivalent prompt notices resident.

Variable Default Purpose
SHEPHERD_TOOL_GUARD 1 (on) Inject the PreToolUse Bash guard into Claude spawns. Set 0 to disable (kill switch) — turning it off puts both hazard notices back into the composed system prompt, so no guidance is lost

Documentation automation (PR-gated doc agent)

Section titled “Documentation automation (PR-gated doc agent)”

Opt-in, default-off. When enabled, a manual trigger (POST /api/doc-agent?repo=<path>) spawns a tightly-scoped Claude Code agent that diffs recent source changes against the hand-written docs and edits the enumerated prose pages in place. It is granted read-only git (git diff/log/show/status) for grounding plus file edits, but has no git mutation, gh, or network access, so it can neither commit nor push; the trusted server stages the in-scope doc files, commits, and publishes them for human review (never an auto-merge) — either by folding the doc commit into an already-open code PR or by opening a standalone doc-update pull request (see Automated cadence below).

Phased soak (observe → act, mirroring SHEPHERD_HOOKS_INGESTSHEPHERD_HOOKS_SIGNALS). Roll the feature out in two stages so you can watch what it would do before it touches a remote:

  1. Observe — set SHEPHERD_DOC_AGENT=1 alone. The agent runs and edits on every trigger, and the server computes the staged doc diff, but finalize is log-only: it opens no PR and runs no push. Each would-be publish is logged as a one-line [doc-agent] OBSERVE: <repo> would … (<n> files): … — either would open a doc-update PR (fresh path) or would push docs onto PR # (pre-merge re-target). Soak here until the logged diffs look correct.
  2. Act — additionally set SHEPHERD_DOC_AGENT_ACT=1 to escalate to actually opening PRs. This flag is meaningful only when Phase-0 (SHEPHERD_DOC_AGENT) is also on. A fresh enable therefore opens no PR until you explicitly opt into act.

Each spawn is recorded as a durable reviewer_spawns row (kind: "doc_agent") for cost attribution, and the boot reconcile re-adopts a run interrupted by a restart (a surviving worktree whose summary is already written is finalized rather than discarded) and reaps any orphaned remote shepherd/docs-update-* branch left by a crash between push and PR-open.

Automated cadence. With the same flag on, three triggers run in addition to the manual one (a per-repo in-flight guard means at most one run per repo at a time):

  • Pre-merge re-target (the default — one PR carries both code and docs). A settled-idle sweep watches every Shepherd-managed session whose code PR is open, CI-green, and has a doc-relevant (feat/config) title. Once such a PR has stayed idle long enough (a ~120 s debounce, so a still-churning PR is never touched), the doc agent checks a worktree out at the PR’s head, edits the in-scope docs, and — in act mode — pushes the doc commit straight onto that PR’s own head branch (never a force-push) instead of opening a second shepherd/docs-update-* PR. If the code PR merges/closes mid-run or the push can’t fast-forward, it falls back to a single standalone PR, so the docs land exactly once.
  • Nightly — once per local day per repo that has the docs tree, at/after SHEPHERD_DOC_AGENT_NIGHTLY_HOUR (default 3). It first freshens the repo’s default branch from origin, then spawns a run only if the branch advanced since the last doc-agent run — quiet days cost a cheap fetch but no agent spawn. This is the reliable catch-all: it picks up any landed change, including fix: commits, config-only changes, and human/non-session or non-conventional merges (e.g. epic-landing PRs).
  • Merge-triggered — a fallback fast-path for when no pre-merge re-target ran: when a Shepherd-managed session’s PR merges to the default branch and its title is a feat/config conventional-commit subject, a standalone doc-update run is considered immediately. If a pre-merge re-target already claimed (and pushed docs onto) that PR, this trigger defers so no duplicate PR is opened. A doc-relevant fix: is intentionally not caught here — it’s covered by the nightly sweep instead. config (type or scope) is a forward-looking allowance and may not yet appear in a given repo’s history. Non-conventional or untitled merges simply fall through to nightly.
Variable Default Purpose
SHEPHERD_DOC_AGENT 0 (off) Set 1 to enable the doc agent (Phase-0 observe): manual trigger, nightly + merge-triggered cadence, and the boot reconcile. Finalize is log-only (no PR) until SHEPHERD_DOC_AGENT_ACT is also set
SHEPHERD_DOC_AGENT_ACT 0 (off) Phase-1 act. Set 1 to escalate finalize to actually commit, push, and open the pull request. Meaningful only when SHEPHERD_DOC_AGENT is also on
SHEPHERD_DOC_AGENT_CLI inherit Agent CLI for the doc-agent spawn: inherit follows the global default provider, or pin claude / codex. Seeds a fresh DB; persisted + UI-configurable
SHEPHERD_DOC_AGENT_MODEL default Model for the doc-agent spawn: default follows the global default model, or pin a <model alias>. Seeds a fresh DB; persisted + UI-configurable
SHEPHERD_DOC_AGENT_EFFORT low Reasoning-effort tier for the doc-agent spawn: default follows the CLI’s own effort, or pin a tier (low / medium / high / xhigh / max). Seeds a fresh DB; persisted + UI-configurable
SHEPHERD_DOC_AGENT_NIGHTLY_HOUR 3 Local hour (0–23) at/after which the nightly sweep evaluates each repo; invalid values fall back to 3

Opt-in, default-off, and fully inert when off. Once per local day Shepherd scores four bands over its own health data and escalates by tier: tier 1 logs the reading, tier 2 spawns a read-only diagnosis agent that drafts a backlog issue which the trusted server files against Shepherd’s own repo — never a managed repo, so nothing lands in someone else’s backlog. The agent itself never touches a forge: it writes a JSON draft in a disposable worktree and nothing else. Tier 3 skips the issue and opens a pull request — see below.

Band Measures Window Tier 1 Tier 2
critic_error_rate Share of outcome-bearing review spawns that errored (produced no verdict) 7 days, min sample 10 0.15 0.30
incident_spike signals per kind — needs both an occurrence count and a distinct-session count, so one thrashing task can’t trip it. The reply kind is excluded (operator corrections are high-volume by design) 7 days ≥ 10 occurrences and ≥ 3 sessions ≥ 25 occurrences and ≥ 5 sessions
first_pass_collapse Per-repo first-pass review rate — direction is inverted, a lower rate is worse 30 days, min sample 8 0.60 0.40
dead_code_drift Auto-fixable dead-code findings in Shepherd’s own checkout (fallow dead-code). Point-in-time, no window and no minimum sample. Declares a tier-3 fix class, so its tier-2 breach is promoted to tier 3 now ≥ 1 finding ≥ 3 findings

A band below its minimum sample reports “below min sample” rather than a misleading number. Every band’s live value is persisted and surfaced on the Delivery lens whether or not it breached — the starting thresholds are calibrated guesses, and observed values are what let you retune them.

Spend bounds. At most one band action per sweep — a tier-2 diagnosis spawn or a tier-3 fix, whichever band is more severe; the rest wait for the next day. After a run completes — published, skipped or errored — its band is suppressed for 14 days. The cooldown anchors on the run, not on a published issue or PR, precisely so observe mode (which publishes nothing) still suppresses. A still-open issue or PR from the band’s last run extends the suppression past the cooldown.

A band may declare a pre-approved fix class: a remediation mechanical enough that no judgement is needed, so the loop produces the diff itself and opens a PR instead of asking you to read a drafted issue first. Exactly one class exists — dead_code, on dead_code_drift.

No agent is involved. The fix is fallow fix’s verbatim output, so a tier-3 run costs no tokens and has no prompt to be injected into. The run:

  1. creates a branch worktree shepherd/maintain-fix-<8hex> off origin/<default branch>;
  2. runs bun install --frozen-lockfile in the root, ui/ and extension/load-bearing: without installed dependencies fallow cannot resolve imports and reports live code as dead;
  3. re-measures in that pristine checkout and stands down if there is nothing to fix (the sweep’s reading came from the live checkout, which can carry uncommitted edits);
  4. runs bunx fallow@<pinned> fix --yes --no-create-config;
  5. verifies fail-closed — the type-check of every package the diff touches must pass, and a re-run of fallow dead-code must report no auto-fixable findings left. Per package, not just the root: bun run typecheck is tsc against a tsconfig that excludes ui, extension, site and docs-site, while fallow analyses all of them, so a root-only gate would pass vacuously for a fix under ui/src. The root uses bun run typecheck; ui and extension use their own bun run check. The run also refuses to commit when the fix touched any package.json or lockfile (fallow fix removes unused dependencies too, and that needs a lockfile regen a background loop must not do), or anything under site/ or docs-site/, whose dependencies are not installed in the fix worktree and which therefore cannot be verified at all;
  6. commits --no-verify, pushes, and opens the PR.

Any failed gate opens nothing, records an error outcome and throws the branch away. Nothing is ever auto-merged. If openPr fails after the push, the branch is deleted from the remote rather than left orphaned.

Phased soak (observe → act → pr), mirroring the doc agent:

  1. ObserveSHEPHERD_MAINTAIN_LOOP=1 alone. Bands are evaluated, readings persisted, breaches logged and the tier-2 diagnosis spawns, but finalize is log-only: it logs [maintain] <band>: would file issue "<title>" (act is off) and calls the forge never.
  2. Act — additionally set SHEPHERD_MAINTAIN_ACT=1 to escalate finalize to actually opening the labelled issue. Meaningful only when SHEPHERD_MAINTAIN_LOOP is also on.
  3. PR — additionally set SHEPHERD_MAINTAIN_PR=1 to let a tier-3 run open its pull request. Independent of SHEPHERD_MAINTAIN_ACT: arming issue-filing never implicitly arms PR-opening. Without it a tier-3 run still installs, fixes and verifies, then logs would open a PR removing … and discards the branch — so the log names the real diff.

POST /api/maintain/sweep runs an evaluation on demand (it 404s when the flag is off, the same unadvertised contract as the doc-agent route). It skips the hour/presence/once-a-day cadence gates but not suppression. A diagnosis run interrupted by a restart is settled and its worktree reclaimed by the boot reconcile; the breach is re-diagnosed on the next cadence.

Variable Default Purpose
SHEPHERD_MAINTAIN_LOOP 0 (off) Set 1 to arm the loop (observe): band evaluation, tier-1 logging, and the tier-2 read-only diagnosis spawn. The drafted issue is only logged until SHEPHERD_MAINTAIN_ACT is also set
SHEPHERD_MAINTAIN_ACT 0 (off) Act. Set 1 to escalate finalize to actually filing the labelled issue against Shepherd’s own repo. Meaningful only when SHEPHERD_MAINTAIN_LOOP is also on
SHEPHERD_MAINTAIN_PR 0 (off) Tier 3. Set 1 to let a tier-3 fix open a pull request against Shepherd’s own repo. Never auto-merges. Meaningful only when SHEPHERD_MAINTAIN_LOOP is also on, and independent of SHEPHERD_MAINTAIN_ACT
SHEPHERD_MAINTAIN_HOUR 4 Local hour (0–23) at/after which the once-a-day band sweep may run — an hour after the doc agent’s nightly so the two spawns don’t land together. Invalid values fall back to 4
SHEPHERD_MAINTAIN_CLI inherit Agent CLI for the diagnosis spawn: inherit follows the global default provider, or pin claude / codex. Env-only (not persisted or UI-configurable)
SHEPHERD_MAINTAIN_MODEL default Model for the diagnosis spawn: default follows the global default model, or pin a <model alias>. Env-only
SHEPHERD_MAINTAIN_EFFORT default Reasoning-effort tier for the diagnosis spawn: default follows the CLI’s own effort, or pin a tier (low / medium / high / xhigh / max). Env-only
SHEPHERD_MAINTAIN_THRESHOLDS (unset) JSON object deep-merged over the threshold table above, so a recalibration ships without a deploy. Parsed field-by-field and fail-soft: an unparseable value or a typo in one number falls back to that default rather than disarming a band. E.g. {"critic_error_rate":{"tier1":0.2}}. Retunes numbers only — a band’s tier-3 fix class is not overridable, because SHEPHERD_MAINTAIN_PR is the one switch that disarms tier 3

Off until you opt in. Shepherd can emit anonymous, privacy-first usage telemetry (OS, version, arch, locale, engine, and which features are used — never code, file paths, repo names, or personal data) to an Aptabase endpoint, to help prioritise the roadmap. It is server-side and best-effort: a failed send is dropped silently and never surfaces to the operator.

Nothing is sent unless all of these hold: consent is granted, DO_NOT_TRACK is unset, and an App-Key is configured (so the ingestion host resolves). Consent defaults to unset, which surfaces a one-time first-run prompt in the HUD; the per-operator consent state is persisted in the SQLite settings table and is also toggleable any time from the Settings panel. The env vars below seed a fresh DB (SHEPHERD_TELEMETRY_CONSENT) or override the endpoint.

Variable Default Purpose
SHEPHERD_APTABASE_APP_KEY A-EU-2837516646 (Shepherd’s public Aptabase Cloud EU key) Master enable. An Aptabase App-Key is write-only and safe to ship in the client (like a GA measurement ID), so the default lets ordinary installs report once the operator opts in. Forks/self-hosters override with their own key, or set it blank to disable telemetry entirely
SHEPHERD_APTABASE_HOST (derived from the App-Key region) Ingestion host override for self-hosted Aptabase. When unset, the host is derived from the App-Key region prefix: A-EU-…https://eu.aptabase.com, A-US-…https://us.aptabase.com. A self-hosted (A-SH-…) or unknown-region key requires this override, else telemetry no-ops
DO_NOT_TRACK (unset) The console DNT standard. Truthy (1/true) hard-disables telemetry and suppresses the first-run consent prompt, regardless of the persisted consent state
SHEPHERD_TELEMETRY_CONSENT unset Seeds the persisted consent for a fresh DB: unset (prompt on first run), granted, or denied. A UI-set consent in the DB overrides this env seed at boot; unrecognised values are ignored
SHEPHERD_OPERATOR_LANGUAGE en Seeds the operator language for a fresh DB: en (agents write to the operator in English — no change) or de (agents address the operator in German while keeping code, commands, identifiers, logs, commit messages, and GitHub issue/PR text in their original language). A UI-set value in the DB overrides this env seed at boot; unrecognised values are ignored

Shepherd can wrap each spawned claude process in an OS-level filesystem/process sandbox via bubblewrap (bwrap). Three profiles are selectable per-repo in the repo’s Settings panel or globally via SHEPHERD_SANDBOX_DEFAULT_PROFILE:

Profile Sandbox Notes
trusted None Default; today’s behavior. Escape hatch when the membrane causes problems.
standard bwrap membrane Agent confined to its worktree + git object store + read-only ~/.claude; blocks ~/.ssh, ~/.aws, sibling repos, other $HOME dotfiles; clears inherited env secrets. Does not restrict network egress. Opt-in for interactive sessions.
autonomous bwrap membrane + egress allowlist Same membrane as standard, plus network-egress confinement (outbound restricted to an allowlist: Anthropic + the forge host + operator extras). Required for auto=true drain/autopilot sessions.

Egress confinement is tied to the profile, not to auto=true. When no sandbox backend is available, manually spawned sessions degrade to unconfined with an operator-visible banner, and auto=true spawns are refused. The full residual posture — the accepted in-membrane token-readability gap and the prompt-injection posture — is documented on the Security page.

Backend requirements: bwrap installed + unprivileged user namespaces enabled. Shepherd self-tests at startup by running node and git through the real membrane.

A second, separate check asks whether the agent binary itself starts inside that membrane — a launcher that dies there (a version manager rewriting its shims against a read-only bind, say) leaves the self-test green while every confined helper dies at launch. It surfaces as the Agent launch in sandbox row in Settings → Diagnose and refuses the affected wrapped spawns up front instead of letting them hang; it never changes whether the membrane is applied. See Operating Shepherd.

A few runtime toggles live in the SQLite settings table (~/.shepherd/shepherd.db) rather than env — e.g. branchPruneEnabled (hourly cleanup of merged local shepherd/* branches, on by default).