Operating Shepherd
Run as a systemd user service
Section titled “Run as a systemd user service”Shepherd runs as a systemd user service — as your own user, so it keeps your
claude subscription login, ~/Work, and herdr. The installer sets this up on
Linux. It binds to loopback only (SHEPHERD_HOST=127.0.0.1).
systemctl --user status shepherd # check itsystemctl --user restart shepherd # restart itThe unit runs straight from the working tree, so whatever is checked out is what runs.
Restart from the UI
Section titled “Restart from the UI”You don’t need a shell to restart: Settings → Session → Restart Shepherd → Restart… (searching “restart” in Settings jumps there). The page reloads by itself once Shepherd answers again, usually within a few seconds. Agent sessions keep running — they live in herdr, not in Shepherd.
Tick Also restart the herdr daemon in the confirm dialog to restart herdr too. herdr
goes first, via herdr server live-handoff: its panes move to a fresh server, so sessions
survive and attached terminals reconnect briefly. If the handoff fails, the old herdr server
keeps running and Shepherd restarts anyway.
The same dialog opens from the plugin banner (Restart Shepherd to apply your changes) after a plugin update or uninstall.
Under the hood this is POST /api/restart with { "herdr": true | false }, answered 202
once launched. The restart runs in a detached transient unit, so its output lands in
journalctl --user -u shepherd-restart.
The button only works on the systemd unit itself. A Shepherd started from a terminal or a
dev worktree answers not_systemd — on purpose, so a dev UI can never bounce the production
instance; restart that one by hand. A second click within a minute answers
already_restarting.
If the HUD freezes
Section titled “If the HUD freezes”HTTP, the web terminal and all background work share one event loop. If code ever spins
on it, the whole HUD stops responding — and systemctl --user restart hangs for its full
90 s stop timeout, because a frozen loop never runs its shutdown handler.
The unit arms a watchdog for this. A separate thread pings systemd only while the loop is still turning:
- After 10 s frozen it writes a stall report to
~/.shepherd/shepherd.log: the requests and timers that were in flight, and what finished just before, with timestamps. - After
WatchdogSec(60 s) without a ping, systemd kills and restarts the service.
grep -a 'loop-watchdog' ~/.shepherd/shepherd.log | tail -20 # the stall reportjournalctl --user -u shepherd | grep -i watchdog # the restart itselfThe in-flight entry that started just before the stall is the prime suspect — attach the
report when you file an issue. A later event loop recovered after … line means the block
was long but finite, and the service was never restarted. An install from before the
watchdog picks it up on the next bun run update.
Expose it over the network
Section titled “Expose it over the network”Reach Shepherd over the network by putting it behind a trusted proxy — e.g. Tailscale:
tailscale serve --bg 7330 # → https://<host>.<tailnet>.ts.net proxies to 127.0.0.1:7330A Tailscale-served HUD needs no allowlist step: at startup Shepherd folds
every host that tailscale serve status shows fronting its port into the CSRF
origin allowlist — the node’s own tailnet name (a direct tailscale serve, as
above) as well as a Tailscale Service front (e.g. svc:shepherd →
shepherd.ts.net). Only a front that doesn’t appear in tailscale serve status
— a non-Tailscale reverse proxy or custom-DNS name — still needs its hostname
added to SHEPHERD_ALLOWED_HOSTS.
Access control is layered: the network reach is gated by
tailnet membership, and the app itself is gated by a single-operator
password. The password is exchanged for an HMAC-signed session cookie that
covers every HTTP route plus the live /events and /pty WebSocket channels.
Set it with SHEPHERD_PASSWORD; if you leave it unset, Shepherd generates a
strong one on first boot and prints it to the log once (systemctl --user status shepherd / journalctl --user -u shepherd) — change it by setting
SHEPHERD_PASSWORD in ~/.shepherd/env and restarting. A changed password
rotates the cookie secret, so existing sessions are signed out (unless
SHEPHERD_COOKIE_SECRET pins the secret).
Per-deployment overrides (password, token, repo root, alternate hosts) go in
~/.shepherd/env (KEY=value lines), read by the unit if present. See
Configuration for the full list.
The state dir (~/.shepherd, or the dir of SHEPHERD_DB) is owner-only: 0700, with
shepherd.db (+ SQLite side files) and shepherd.log* at 0600. The server tightens them at every
boot, the unit runs with UMask=0077, and install.sh / bun run update tighten an existing
install. A path that is a symlink or owned by another user is left alone with a warning.
Connect another machine
Section titled “Connect another machine”Another Shepherd, for example the one on your MacBook, can run tasks you start here: connect it under Settings → Machines with its tailnet address and a Full access token created on it. The whole session then runs on that machine, and its sessions show up in your herd. Setup for Linux and macOS, the trust decision, the limits and an end-to-end checklist: Run tasks on another machine.
Deploy a code change
Section titled “Deploy a code change”The unit runs from the working tree, so to deploy local changes in one shot (install deps → build UI → restart → health check):
bun run update # deploy the current working tree (warns if dirty / off main)bun run update --pull # fast-forward main from origin first (skip on a dev==prod box)It is idempotent and safe to re-run — sessions survive the restart (herdr owns the
PTYs). It also refreshes the prebuilt shepherd CLI to the server’s version (skipped
when already current or when SHEPHERD_NO_CLI=1; a missing binary only warns). UI-only changes don’t strictly need it: a fresh cd ui && bun run build is
served on the next request, since the core reads ui/build from disk per request.
Backups & restore
Section titled “Backups & restore”On Linux the installer/provision step also enables a second systemd user
timer — shepherd-backup.timer (OnCalendar=hourly) — that snapshots the SQLite
state DB out-of-process from the server (read-only VACUUM INTO → integrity check
→ gzip → atomic rename → GFS rotation). Snapshots land in ~/.shepherd/backups/
by default (override SHEPHERD_BACKUP_DIR). Backups are owner-only: the backup dir is
kept at 0700 and every file in it at 0600, tightening files an older run left at 0644.
systemctl --user status shepherd-backup.timer # check the timerjournalctl --user -u shepherd-backup # per-run logsdeploy/shepherd-restore.sh # list snapshots, then restore onedeploy/shepherd-restore.sh <file> # restore a specific shepherd-*.db.gzThe server’s daily sweep also runs a read-only staleness probe: on a host
that’s expected to back up, if the newest snapshot is missing or older than 3h it
logs a warning, records a durable backup_stale signal, and sends a best-effort
web-push alert. macOS / core-only hosts have no backup timer and stay silent. Full
details are in the
backups runbook.
Preview detection
Section titled “Preview detection”Live previews depend on Shepherd being able to see the processes an agent starts.
How it looks depends on the host: on Linux it reads /proc live; on macOS it runs
a single lsof call per refresh and answers every probe from that short-lived
snapshot. On any other OS there is no backend at all.
The Preview detection row in Settings → Diagnose makes that visible instead of
leaving a preview that silently never appears. It warns when the backend can’t
run on this host, when the OS isn’t supported, and when the snapshot has gone stale
(the last inspection didn’t finish in time, so previews may be delayed or missing);
it stays ok when detection works. It never escalates to an error — previews are a
convenience, so a broken one shouldn’t turn the health pip red. If the check itself
can’t complete, the row reports that it couldn’t be verified rather than degrading
the pip. Starting a dev server from the UI on a host where detection is dead or
stale still starts it, but you get an alert pointing here instead of a “started”
toast, because the preview can never bind.
Detection also backs sweeps that are more consequential than previews. When process
data is unknown, Shepherd skips rather than acting on it: session liveness stays
unreported (no husk/stranded verdict, no auto-revive) and the boot/daily worktree
reap is skipped — it logs worktree reap skipped (live cwds unknown) and still runs
the package-manager store reclaim, which doesn’t depend on process data. Idle-stop
behaves the same way; see SHEPHERD_PREVIEW_IDLE_STOP_MS in
Configuration.
Agent CLIs managed by mise
Section titled “Agent CLIs managed by mise”If claude or codex is managed by mise, Shepherd works
with that install instead of planting a second one beside it.
Installing. When a claude/codex check is red and mise which <tool> resolves,
the Fix button symlinks the mise binary into ~/.local/bin rather than running the
vendor installer. mise exposes tools through a shims dir that isn’t on the systemd
unit’s PATH, and mise activate only runs in interactive shells — so the binary
existed, it was just unreachable. ~/.local/bin is on the unit’s PATH. Hosts
without mise, and tools mise doesn’t manage, get the vendor installer exactly as before.
Updating. When mise owns the codex on PATH — a shim, an install path, or a
symlink resolving to one — the codex update runs mise upgrade codex and nothing
else. mise upgrade respects the version request in your mise config, so a pinned
codex won’t advance; Shepherd then reports the update as not converged and points you at
mise use -g codex@latest rather than falling back to codex update or npm install -g,
either of which would install a second codex shadowing the mise-managed one.
Claude Code has no Shepherd-side updater (it self-updates), so a mise-managed claude is
upgraded by you: mise upgrade claude.
The Claude Code install row
Section titled “The Claude Code install row”Claude Code self-updates by writing a native install into ~/.local/share/claude/versions
and repointing ~/.local/bin/claude at it. On a host where mise also manages claude, that
native copy shadows the mise-managed one: the two drift apart, and mise upgrade claude
then moves only the install nothing runs.
Two things happen on a host where mise owns the claude Shepherd spawns:
- Shepherd sets
DISABLE_AUTOUPDATER=1on agent spawns, so Claude Code stops re-planting the native copy.mise upgrade claudeis the update path from then on. This only applies when mise really does own what runs — a host without mise keeps self-updating exactly as before, so nothing gets stranded on an old build. - The claude install row in Settings → Diagnose reports the state. It warns when the
claudeonPATHand the one mise manages report different versions (naming both, so you can see which one Shepherd actually spawns); when the versions agree but what runs is still the native copy, which mise can’t advance, so the two will drift again; and when mise really does own what runs but a leftover native tree is sitting on disk — those builds are several hundred MB each. The row is neverokunless the pin is in effect, so it can’t claim a pin that wasn’t applied — though the reverse doesn’t hold: the leftover-tree warning is a pinned state too, since mise does own what runs there and it’s only the old files that need clearing.
Ownership is decided by comparing what claude --version reports against the mise-managed
binary’s own --version, not by inspecting paths — so a shim, a ~/.local/bin symlink and a
launcher script that execs mise x claude all read correctly.
The row has no Fix button by design: repointing ~/.local/bin/claude would clobber a
launcher script you may have put there on purpose, and deleting a several-hundred-MB install
tree is your call, not Shepherd’s. On a host where mise doesn’t manage claude the row is
absent entirely.
Claude Code version and model support
Section titled “Claude Code version and model support”Claude Code validates --model against a model catalog compiled into the binary. Picking a
model newer than the installed CLI is not a slow failure or a quiet downgrade — every spawn dies
with a 400 naming the version it needs:
API Error: 400 Claude Code 2.1.277 does not support this model; version 2.1.280 or newer is required.The claude model support row in Settings → Diagnose warns before that happens: when a model
you have configured — as the global default, a repo default, a per-role model, or on a live epic
run — needs a newer Claude Code than the one on PATH, the row names the model, the version
required and the version running. It stays silent otherwise, including when claude --version
can’t be read: Shepherd would rather say nothing than guess at a version it never established.
Like the install row above it, this one has no Fix button — updating your Claude Code is your
call. On a mise-managed host that’s mise upgrade claude; otherwise claude update. Currently
two models carry a floor, each with its [1m] variant: claude-opus-5-5 at 2.1.280 and
claude-sonnet-5-5 at 2.1.284. Sonnet 5.5 is the softer case: an older CLI doesn’t reject it but
runs it as an unrecognized model, with auto-compact capping the context at 200k tokens.
Claude Mythos (claude-mythos-5-1) is a different kind of limit: not every Claude account can
use it. Shepherd reads which models your logged-in account has from Claude Code’s own cache of
that account’s model list, and offers Mythos in the model pickers only when it is there. If your
account doesn’t have it, it stays hidden, and a task, a saved default or a role that asks for it
is refused with model_unavailable instead of starting. In API-key mode it is always hidden.
Transient API errors
Section titled “Transient API errors”A Claude turn can end on a transient API error instead of a real stop — API Error: Connection lost mid-response… or API Error: 529 Overloaded… once Claude Code’s own retries run out. The
agent then sits idle. Shepherd spots that error as the last message of the transcript and types
a “continue where you left off” message for you, in every Claude session, attended or autopilot (except the Operator Assistant’s own chat):
after 20 seconds, then 60, then 180 if the error keeps coming back. After the third failed retry
it stops and flags the session halted on an error. A turn that ends normally resets the count.
Autopilot holds off while a retry is pending, and takes over as usual if the retry is called off.
Login, usage-limit and other API errors are left to their own handling. Pending retries don’t
survive a Shepherd restart.
Agent launch inside the sandbox
Section titled “Agent launch inside the sandbox”Shepherd’s startup self-test proves that bubblewrap can build a sandbox on this
host — it runs node and git through the real membrane. It deliberately does not
launch the agent binary, because a null verdict there means “run unconfined”, and
folding a launcher fault into it would silently strip the sandbox from exactly the
hosts that can sandbox.
A separate check answers the other question: does claude / codex actually
start inside the membrane? A version manager that rebuilds its shims directory
against a read-only bind, for instance, exits non-zero at launch — so every confined
helper (plan reviewer, PR critic, doc agent, maintain diagnosis, standalone critic) died at launch,
waited out its whole timeout and reported no verdict, while the sandbox row stayed
green.
The Agent launch in sandbox row in Settings → Diagnose reports it, and a broken
verdict also blocks: a wrapped helper spawn is refused up front with a stated
reason instead of hanging to its timeout — per binary, so a broken codex doesn’t
stop claude roles. It is fail-open: only a non-zero exit counts as broken; a
probe that throws or times out is reported as un-inspectable and spawns proceed.
The launcher’s own output holds host paths, so it goes to Shepherd’s log rather than
into the row.
The row is guidance-only — no Fix button, because the repair is host-side
toolchain surgery (which manager owns the binary, which path it rewrites). It is
absent on a host with no sandbox backend (nothing is wrapped) and for an agent CLI
that isn’t on PATH (the claude / codex rows already say so). Hitting Re-run
probes fresh rather than reading a cached verdict, and the refusal path reads the
same result — so a repaired host un-blocks its confined helpers as soon as the row
goes green.
Host tuning — tmpfs inodes
Section titled “Host tuning — tmpfs inodes”Shepherd keeps spawned agents’ Node compile cache off the /tmp tmpfs and runs
an inode-guard sweep on startup + daily that, once /tmp inode use crosses a
threshold, drops the compile cache and stale regenerable tool caches (but never a
live session’s scratch). As a host-level belt on long-uptime hosts, raise /tmp’s
nr_inodes in /etc/fstab:
tmpfs /tmp tmpfs nr_inodes=4194304 0 0The relevant override env vars (SHEPHERD_NODE_COMPILE_CACHE,
SHEPHERD_TMP_INODE_PCT, SHEPHERD_TMP_ENTRY_LIMIT, SHEPHERD_TMP_STALE_HOURS,
SHEPHERD_TMP_ORPHAN_DAYS, SHEPHERD_TMP_SWEEP_DIR) are listed in Configuration.
The Temp filesystem inodes row in Settings → Diagnose surfaces this live: it warns
at SHEPHERD_TMP_INODE_PCT (the same threshold that gates the sweep) and errors at 95% by
default. The bands stay ordered: if you raise the knob above 95 the error band rises with it, so
the row never alarms below the line you set.
Not every filesystem has an inode ceiling to express as a percentage — btrfs, XFS and ZFS allocate
inodes on demand and report a total of zero. There the sweep and the row both fall back
to counting how many leftover top-level entries have accumulated in each temp root, warning at
SHEPHERD_TMP_ENTRY_LIMIT and erroring at ten times it. Both signals are evaluated per root,
and the row reports the worst — a quiet /tmp can sit beside a disk-backed agent temp root that is
filling steadily, and one reading would hide the other. “Worst” is ranked by the state each root
would report, not by a raw ratio, because the two signals reach their warning band at different
fractions of their error band.
The row measures only the roots a sweep can reclaim. Session-scratch roots are excluded: their contents belong to live sessions and cannot be removed by the row’s Fix — so counting them would pin the row at a warning that no action clears.
A live session’s own scratch is still reclaimed, just not by the row: at teardown, and — for
whatever a teardown never got to (a crash, a restart, a removal path that predates the fix) — by a
reconcile that runs inside the same boot + daily sweep. It removes orphaned per-session scratch
and the scratch left behind by transient helper agents — both halves of the latter: the copy under
the agent’s own temp root, and the helper’s mkdtemp working directory sitting directly under the
helper temp root. It is fail-closed throughout: an entry is removed only on positive proof that it
is dead, every live worktree and every live helper cwd is kept, a directory that holds a .git is
refused outright (that is a worktree, and reaping those belongs to the worktree half), an unreadable
repo disqualifies its own entries, and anything unrecognised — including your own interactive
claude sessions’ scratch — is left strictly alone. Where live process data is unknown both helper
rules are skipped entirely, the same stance as the worktree reap. Each run logs what it removed
([tmp-sweep] <phase>: scratch reconcile removed …).
This matters because inode exhaustion is easy to misdiagnose — writes start failing with
“no space” errors while df -h still shows the volume mostly empty. df -i is what shows
the real cause.
Its Fix button runs the sweep immediately, ignoring the usage threshold. It reclaims what Shepherd owns — the compile cache and stale tool caches — so the row can legitimately stay non-OK right after you click it: the two largest consumers when an agent has run a dependency install in the temp filesystem — a leftover git worktree and the forked package-manager (pnpm) store it pinned — are not touched by the on-demand Fix button. They are reclaimed by the background sweep that runs at boot and daily: abandoned agent worktrees are reaped, then the forked pnpm store is partially reclaimed — under inode pressure it unlinks the store content nothing still references and prunes the emptied bucket dirs, while keeping content a surviving worktree still hardlinks. So a store that is still partly pinned frees its unlinked fraction rather than nothing at all.
Host tuning — resource guardrails
Section titled “Host tuning — resource guardrails”Settings → DIAGNOSE includes a Host capacity check. On a systemd-managed
host it warns when Shepherd’s unit has no memory or CPU guardrails
(MemoryMax / MemoryHigh / CPUQuota), so a runaway fan-out of sessions can
starve the box. Add limits to the unit — or to a dedicated slice such as
shepherd.slice — before running many concurrent sessions.
On a user-scoped install the check covers both units: Shepherd’s own unit
and herdr.service, which runs your agent sessions. It stays ok only when
Shepherd is bounded and herdr is not positively unbounded, and it raises a
distinct warning for the case where Shepherd is bounded but herdr is not. A herdr
that isn’t a loaded user unit (absent, masked, or system-scoped) can’t be read, so
it’s excluded from the verdict rather than reported as unbounded. The check also
errors when the kernel reports dangerous live memory/IO pressure (PSI), a cue
to pause or reduce active agent sessions until the host recovers. On non-systemd
or local dev hosts it stays quiet. Because sustained pressure is steady-state, the
background re-check is not accelerated on this error — use the Diagnostics
Re-run button for an on-demand live reading.
Add a limit (one click)
Section titled “Add a limit (one click)”On a user-scoped install with at least 6 GiB of RAM, the warning carries a Fix
button. It proposes a conservative, host-derived pair — MemoryHigh leaving
clamp(15%, 2 GiB, 8 GiB) of RAM headroom, CPUQuota leaving
min(1 core, 15% of cores) for the OS — and shows the exact values and units in a
confirm modal before anything is applied. Applying runs systemctl --user set-property on only the units that are currently unbounded, so a limit you
set deliberately is never overwritten. Like the copy-paste command below, it is
live and persistent with no restart and no interrupted sessions.
It bounds each unit on its own; it does not create the shared cap across Shepherd and herdr that a slice gives you — for that, use the slice setup below.
Add a limit (copy-paste)
Section titled “Add a limit (copy-paste)”Shepherd installs as a systemd user service (shepherd.service), so you can
add a live, persistent guardrail without sudo or a restart. set-property
writes a drop-in under ~/.config/systemd/user.control/ and applies it
immediately:
# Tune the values to the host — e.g. MemoryHigh a few GB below total RAM,# CPUQuota to leave headroom for the OS and other services. 300% = 3 cores.systemctl --user set-property shepherd.service MemoryHigh=6G CPUQuota=300%Setting any of MemoryHigh, MemoryMax, or CPUQuota on a unit marks that
unit bounded. MemoryHigh throttles and reclaims before the harder MemoryMax
OOM-kill ceiling, so it’s the safer first lever.
On a user-scoped install the command above is not enough on its own to clear the
Host capacity warning: agent sessions run under herdr, a separate unit,
so a limit on shepherd.service alone bounds Shepherd but not the sessions that
actually consume the box — and where herdr is a loaded user unit, the check keeps
warning until it is bounded too. Repeat the set-property for it:
systemctl --user set-property herdr.service MemoryHigh=6G CPUQuota=300%For real protection, prefer a shared slice — a per-unit limit on each of the two still lets them add up to twice the ceiling. Put both units in one slice and limit the slice instead:
[Slice]MemoryHigh=12GCPUQuota=600%# ~/.config/systemd/user/shepherd.service.d/slice.conf (same for herdr.service)[Service]Slice=shepherd.sliceSlice= is assigned at unit load, not via set-property, so this pair needs a
reload + restart to take effect (the restart briefly drops the HUD):
systemctl --user daemon-reloadsystemctl --user restart herdr.service shepherd.serviceA system-level install (unit under /etc/systemd/system/) takes the same
properties via sudo systemctl set-property … or an equivalent drop-in there.
herdr restarts and the task limit
Section titled “herdr restarts and the task limit”When herdr starts, it restores every tab saved in ~/.config/herdr/session.json
(or sessions/<name>/session.json when you set HERDR_SESSION) as a fresh shell.
Every agent process dies with the server, so a restarted herdr brings those tabs
back as empty shells. If the tabs include hundreds of leftover Shepherd helper
tabs (reviewers, the namer, the usage probe…), they all spawn at once.
To prevent that, herdr.service runs deploy/herdr-prune-session.ts before herdr
starts. It removes helper tabs from session.json, keeps a copy of the original as
session.json.pre-prune, and logs one [herdr-prune] line to the journal
(journalctl --user -u herdr). The script doesn’t touch your own tabs or session
tabs. It makes no change when:
- the file’s format isn’t one it recognizes,
- a herdr server is still answering on the socket, or
- a process from a previous pane is still running.
If the script fails, herdr still starts.
The unit also sets TasksMax=16384. Every pane and its threads count against
herdr’s task limit, and a low systemd default (2500 on some hosts) can make herdr
crash with pthread_create: EAGAIN.
It also sets LimitNOFILE=65536:524288. Every pane holds open files, and at the
default soft limit of 1024 a burst of tabs can stop herdr’s API with
No file descriptors available. A herdr.service.d drop-in of your own still
overrides this value.
Heavy checks take turns
Section titled “Heavy checks take turns”Tests, type checks and builds that agents start take turns through a host-wide check slot (#3181). Each of those runners already uses every core on its own, so running them one after another costs little throughput. Running several at once is what filled memory and swap until the host stalled.
How a check gets the slot
-
Recognition. For Claude sessions, the
PreToolUsetool guard rewrites a heavy command tonode scripts/heavy-slot.mjs -- '<command>'. Codex sessions have no such hook, so they get PATH shims forbun,cargo,vitestand similar tools (scripts/slot-shim.mjs), which use the same detection.- The built-in list covers
vitest,jest,playwright test,svelte-check,tsc,bun test,cargo build|check|clippy|testandswift build|test. - It also catches package scripts and shell scripts that run one of these.
- Edit the list under Settings → Resources → Counts as heavy.
Anything not recognized runs unchanged.
- The built-in list covers
-
Asking first. The runner asks Shepherd for the slot before the command starts.
- If a slot is free, the command runs as usual.
- If not, what happens depends on When waiting in Settings → Resources. By default the command does not run: the agent gets its place in line and an estimated wait (exit code 75), keeps working, and Shepherd messages it when it’s its turn. The slot is then held for it for five minutes.
- Block for a limited time instead makes the command wait up to the set number of minutes before falling back to that message.
-
Bound to the processes. A slot stays taken until every process the check started has exited, not just the command. Shepherd finds them by the
SHEPHERD_HEAVY_JOBvariable they inherit (on macOS, by process group plus detached children). If a browser or worker is left behind, the slot stays taken. Release in the host popover frees it without stopping anything. -
Pressure brake. No new check starts while the host is short on memory.
- On Linux the brake reads
MemAvailableand memory PSIfull avg10. - On macOS it reads the system’s memory-pressure level.
- It lets go only after the readings have stayed under the limits for the hold time.
- The limits are in Settings → Resources → Pressure brake.
- On Linux the brake reads
Where you see it
- The check-slot chip in the top bar shows slots held and waiting, plus RAM. It opens the host popover: who holds and who waits, pressure readings, memory by origin, and the last check results tied to the revision they ran on.
- The herd panel shows a check-slot strip; on a phone it docks above the action bar.
- Each session card’s now-line shows its own part:
- ▤ Checking, with command, time and memory;
- ◔ Waiting for check slot · Place n, with the memory-pressure reason when the brake holds it.
- Settings → Machines shows each connected machine’s slot.
- Autopilot leaves a waiting session alone.
Defaults and switches. With no configuration the slot count comes from the
hardware: one slot per 16 cores and 32 GiB of RAM, never fewer than one, so a
16-core / 30 GiB host gets one. Settings → Resources changes the master
switch, slot count, wait behaviour, heavy list and brake at runtime. A change
applies to the next start; running jobs keep running. SHEPHERD_HEAVY_SLOT and
SHEPHERD_HEAVY_SLOTS set the defaults those settings fall back to.
From an operator terminal, shepherd slot run -- <command> takes a free slot
the same way. If none is free, it prints the place and estimate and exits 75
without queuing.
Limits.
- Commands the detection doesn’t recognize run unmanaged.
- If the server can’t be reached, the runner runs the command unmanaged rather than blocking the agent.
- A sandboxed session spawned before the update can’t see the runner until it is respawned.
- Heavy runs don’t get a memory limit of their own yet (#3555).
Auto-merge without PR CI
Section titled “Auto-merge without PR CI”Full auto-merge waits for green CI before it merges a PR. Two cases have no CI to wait for, and Shepherd reports both instead of waiting forever.
The repo has no PR CI. Shepherd reads .github/workflows and counts only workflows that check a
PR: pull_request, pull_request_target, merge_group, and push unless it is limited to tags
or to fixed branch names such as [main]. A repo whose workflows only run on schedule,
workflow_dispatch, release and similar counts as having no CI. A workflow file Shepherd cannot
parse counts as CI.
- The automation pane warns under Merge without CI.
- The repo’s chip, its Repos tile and its Automation tab carry a No PR CI mark until you add CI or opt in. The Automation tab explains the hold and offers both fixes.
- The merge train holds the PR. The session banner says no workflow runs on pull requests.
- To land it, add a CI workflow, use Merge without CI on the banner (one PR, with the usual merge confirmation), or turn on Merge without CI in the automation pane. The setting covers every PR in the repo and is off by default.
CI skips this PR. A CI repo’s workflow can skip a PR through a paths or branches filter. If
the PR’s latest commit has no checks for 10 minutes and GitHub reports it mergeable, the banner
says CI apparently doesn’t run for this PR and offers Merge without CI. Pushing a commit that
triggers CI clears it.
Session chat
Section titled “Session chat”Every session has a Chat tab, first in its tab bar. It shows the session’s transcript as a conversation: the agent’s replies as text, your messages on the right, and the tool calls between two messages folded into one row (7 tool calls · edited server.ts). Open a row to see each call and whether it failed. The fold the agent is still adding to shows its latest call, so a long turn doesn’t look stalled. The transcript is written per message, not per token: a reply appears once the agent finishes it.
On desktop a session opens on its Terminal; on a phone or touch screen it opens on the Chat. Either way the other is one tap away. To always open on one of them, pick it under Settings → Device → Session opens on; the choice is saved per device.
- Write in the field at the bottom: Enter sends, Shift+Enter breaks the line, Esc or Stop interrupts a turn (on touch, the Send button sends). Your message is typed into the session’s terminal, or goes through the reply route when no terminal is connected. A Codex session’s message always takes the reply route: Codex exits after each turn, and the route resumes it first. A sent message shows as sent until the transcript logs it.
- Answer a prompt with the buttons the Chat shows when the agent waits on you: a permission menu’s options, a (y/n) question, a plan approval (with Show plan), or an AskUserQuestion form. A button types the option’s key into the terminal.
- Steer with the steer bar under the Chat, the same chips as on the Terminal. A fired steer shows as sent at once and, once logged, carries its chip (You · 🧪 Retest).
- Shepherd’s own steers (a CI fix, a rebase, the critic’s findings, a plan go-ahead) fold into one row, Shepherd · Critic findings; open it to read the full text.
- Everything else stays in the terminal: slash-command pickers, trust and upsell dialogs, rewind and compaction output. When the agent waits on one of those, the Chat says so and Open terminal switches to it.
The Chat reads the transcript only while it is on screen: every 2 s while the agent works, every 8 s at rest. A herd card never reads it.
Operator Assistant
Section titled “Operator Assistant”The Operator Assistant is an interactive Claude or Codex session that Shepherd runs for you and grounds in its own state. Ask it in plain words why the herd stalled, why an epic won’t drain, why an agent died, or whether this node is healthy. It reads, explains and tells you what to do. Ask it to get something done in a repo and it starts a session for it, as New Task would. What else it may change itself — steer a session, approve a plan, merge a PR, flip repo automation — you decide under Settings → Assistant.
Set its preferred provider, model and reasoning effort under Settings → Assistant. One configuration applies to the global and all repo assistants, independently of coding defaults. Each provider keeps its own model and effort selection. Existing installations start with a snapshot of their previous Claude defaults; changing coding defaults later has no effect on HUD assistants.
Saving applies to new conversations. Switch now transfers every open assistant’s recent conversation to the saved runtime, waiting for current answers and pending approvals. The old conversation is archived only after its replacement starts. A failed switch keeps the original conversation and reports the failure. Transfers use a bounded recent history, including recorded tool activity; they do not preserve native conversation identity across providers. After a transfer, actions wait for a fresh operator request so old actions are not replayed.
Automatic fallback selects the other available provider when the preferred one is missing, unsupported or has a confirmed capacity limit. The preferred setting and coding defaults stay unchanged. Existing fallback conversations stay where they are when capacity returns; new conversations check the preference again. Unknown or stale capacity measurements do not cause a switch, and ordinary network errors do not trigger fallback. The panel shows the active provider/model and switch progress. If both providers are unavailable, retry when capacity returns or change the selection.
Turning HUD assistants off prevents starts and resumes and stops their processes after current answers finish. Conversations are retained. Turning them on does not start work. Codex assistants use an isolated configuration and a read-only sandbox with credential paths blocked. Their diagnostic shell runs through Shepherd’s approval controls inside that same sandbox; approval never grants file writes or credential access. Installations whose Codex CLI cannot enforce this profile cannot run a Codex HUD assistant.
Their tokens count toward your usage like any session’s. The first open starts it with no prompt, so nothing is spent until you ask.
Open it
Section titled “Open it”- Assist in the top bar opens it as a panel beside the selected session; ⌘K / Ctrl+K → Open Operator Assistant does the same, and on desktop ⌘J / Ctrl+J opens or closes it from anywhere, even a focused terminal. The session terminal keeps working next to it. On a phone the assistant is its own screen, opened from the floating Assist button above the bottom bar; Back returns to where you were.
- Ask the assistant in a herd row’s right-click / long-press menu opens it with “What’s going on with TASK-31, and is anything stuck?” typed into its prompt.
- Ask the assistant on a warning or error check under Settings → Diagnose does the same for that check.
The panel shows the conversation on its own warm card, so it never reads as one more session terminal: your messages on the right, its replies as text, background notices as quiet one-liners. Write in the field at the bottom — Enter sends, Shift+Enter breaks the line, Esc or Stop interrupts a turn. A permission menu, a (y/n) question or a plan approval it waits on gets answer buttons in the conversation, as in a session’s Chat. Terminal in the panel header switches to its raw terminal, for anything the conversation leaves out (it points you there); the choice is remembered per device. The header also names what it sees right now, such as sees TASK-31 · Diff.
An Ask types the question but never sends it: read it, edit it, press Enter. Whether the panel is open is remembered per device. The Assist button carries a pip: amber while the assistant works, red while a command waits for your approval. The chip in the panel header shows what it may do — Read-only, Asks first or Acts itself — and opens Settings → Assistant.
Repo assistants
Section titled “Repo assistants”The Operator Assistant covers everything. When you work on one repo for a while, give it its own assistant: Assistant for this repo only in a repo chip’s right-click / long-press menu, or + Repo assistant at the top of the panel, which lists every repo. There is at most one per repo; on a repo that has one, the same entries open it.
- It sees one repo. Its herd and repo-automation tools show only that repo’s sessions, held tasks and settings, it starts sessions only there, and its actions refuse a session or repo outside it. Shepherd enforces this, not only the prompt. With every message it gets that repo as its filter, and the session in view only when it belongs there. Questions about the node — health, logs — still work.
- Everything else stays with the global assistant. Ask there about the whole herd or about another repo. Halting the whole herd is the global one’s too: a repo assistant interrupts its repo’s sessions one by one instead.
- All of them stack in the panel. The global one is always on top, repo assistants follow in the order you started them, and that order never changes. One is open; the others fold into bars that show the repo, whether it is working (amber) or waiting for you (red), its newest reply and how many replies came in since you folded it. Click a bar to open it. On a phone the stack becomes tabs under the assistant’s title.
- The Assist button counts them once there are two or more, and its pip shows the most urgent: red as soon as any one waits for your approval.
- End (the square in a repo assistant’s header, clicked twice) archives it. The global assistant is never ended, only started over with New conversation.
Each repo assistant is a Claude session of its own, with the same model, effort and Settings → Assistant permissions, and its tokens count like the global one’s.
What it reads
Section titled “What it reads”Through four read-only tools, which never ask:
- the herd — every live session’s status, plan gate, hold reason and minutes since its last update, plus the held tasks per repo; for one session also its plan questions, recent signals and its PR and CI state;
- repo automation — the drain, auto-merge, critic and usage-ceiling settings, the drain status, epic runs and queues, and the repo’s schedules;
- diagnostics and the host — the Settings → Diagnose checks, herdr’s runtime, load, memory and pressure, the Shepherd, herdr and claude versions, and the systemd state of Shepherd and herdr;
- logs — the tail of Shepherd’s, herdr’s and the updaters’ logs, with secrets masked.
It can also read Shepherd’s own docs and source in the install directory.
With every message it also gets what your Shepherd window shows: the repo filter, the session in view with its open tab and its epic, and which client you use. So “why is this one stuck?” or “what’s wrong in the current repo?” means what you’re looking at. On a phone, the session in view is the one you opened the assistant from. When that doesn’t settle what you mean, it asks. A conversation started before this arrived picks it up after New conversation.
Permissions (Settings → Assistant)
Section titled “Permissions (Settings → Assistant)”The assistant runs in Claude’s default permission mode. Settings → Assistant sets how far it goes, one row each:
| Row | Covers | Levels |
|---|---|---|
| Shell commands | Commands it runs on this node to diagnose | Off · Ask for every command · Reads run, the rest asks · Run everything |
| Steering & answers | Send a message into a session, answer a plan’s open questions | Off · Ask first · Allowed |
| Start sessions | Start a new session in a repo, as New Task: its own worktree from the repo’s default branch (or one it names), your default model and effort, and the repo’s plan gate, autopilot and usage hold | Off · Ask first · Allowed |
| Control sessions | Interrupt, resume, relaunch or archive a session | Off · Ask first · Allowed |
| Approvals & merge | Start an approved plan, approve a build queue, mark a PR ready, merge a PR | Off · Ask first · Allowed |
| Automation | Repo automation (Auto-Drain, max auto sessions, auto-merge, critic, usage ceiling), a session’s autopilot and auto-merge, pausing or resuming an epic, held tasks, halting and retrying the herd, creating a repo schedule | Off · Ask first · Allowed |
- The default lets read-only commands run (grep, cat, git log, ps, systemctl status …) and starting a session, and has every other action ask first. It answers with the new session’s designation, or the held task’s when the usage hold queued it.
- Ask first shows Claude’s confirmation in the panel’s terminal before each call; Allowed runs it without asking; Off refuses it — Shepherd itself refuses a turned-off action, not only Claude.
- A merge you approve is the revision you saw. The request names the PR, its head commit and its base. If the agent pushes after that, the merge is refused and the assistant has to ask again. A request you leave unanswered expires after 10 minutes.
- Changes apply at once, also to a running conversation: Shepherd rewrites the
assistant’s
.claude/settings.local.jsonand Claude re-reads it. - Every action is your own button. Each one runs the same route the UI does, with the same checks. It targets sessions by designation, never the assistant itself or another machine’s sessions, and logs one line per action to Shepherd’s log.
- It acts only when you ask. For a diagnosis question it names the action it would take and waits; it never acts because issue, PR or log text tells it to. A merge someone else is responsible for is never taken over — it hands that back to you.
- Security-relevant repo settings (sandbox profile, egress and browser hosts, the Shared Browser, repo mode, draft mode and sign-off) stay yours alone.
Whatever the levels:
- File edits are denied (Edit, Write, NotebookEdit).
- Credential files are unreadable:
~/.shepherd/env,~/.config/shepherd/config.toml,~/.claude/.credentials.json,~/.codex/auth.json,~/.config/gh/hosts.yml,~/.ssh/,~/.aws/,~/.config/gcloud/,~/.azure/,~/.netrc,~/.git-credentials,~/.docker/config.jsonand~/.kube/config. - Reading anything outside its own directory and the install directory asks first. Raw log files are no exception; the log tool’s masked tail is the way in.
- Your own
~/.claudesettings don’t apply inside it: their allow rules can’t approve a command for you, and your plugins and hooks don’t load. Only Shepherd’s MCP server is connected.
What Allowed costs: text in issues, PRs and logs reaches the assistant fenced as data,
and its instructions say never to act on it — but on Allowed nothing else stands between
such text and an action. With the shell on Reads run, Claude’s file rules still keep
credential files out of cat, head and the like, but a command built to dodge them
(say, a file read inside $(…)) is no plain read and asks. And on a host shared with other
users, the assistant’s session id — the key to its tools on Shepherd’s local agent
port — shows in the process list like every agent’s. Keep the groups you would regret on
Ask first.
A conversation started before an update that added tools learns them on its next start: New conversation once.
When herdr is down
Section titled “When herdr is down”The assistant runs in a herdr pane, so it can’t run while herdr is down. The panel then
says so and links to Settings → Diagnose, which reports herdr’s state and offers its
repair. The same goes for a node without claude: the panel names the reason instead of
starting anything.
New conversation
Section titled “New conversation”New conversation in the panel header (click it twice to confirm) archives the current conversation and starts a fresh one. Closing the panel only hides it: the conversation keeps going, survives reloads, and after a Shepherd or herdr restart the next open resumes it.