shepherd CLI
shepherd is a command-line client for a Shepherd server, for operators and for external
agents. It reads the herd, streams server events, and steers sessions. Everything goes over the
same HTTP API and /events socket as the web UI. The client is generated from
contracts/openapi.rust.yaml, so nothing about the
server is hand-typed. The CLI ships in lockstep with the server and warns on stderr when
/api/health reports a different version.
Claude sessions that Shepherd spawns learn the session commands (new, steer, wait and the
rest) from the shipped shepherd-cli agent skill
(agent-skills/.claude/skills/shepherd-cli/SKILL.md).
Its commands and flags are tested against this page, so a rename here has to update the skill too.
Install
Section titled “Install”You don’t need a Rust toolchain. deploy/install.sh installs a prebuilt shepherd binary into
~/.local/bin, and deploy/update.sh (bun run update) refreshes it on every deploy, so the CLI
stays at the server’s version. When the installed binary is already at that version, nothing is
downloaded. Both scripts only warn, and carry on, when no binary is published for the version, for
example on a dev checkout or while the release is still building.
Prebuilt targets:
| Host | Target |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-gnu |
| Linux aarch64 | aarch64-unknown-linux-gnu |
| macOS (Apple Silicon) | aarch64-apple-darwin |
The Linux builds need glibc 2.35 or newer, for example Ubuntu 22.04 or Debian 12. Other hosts, including musl distros such as Alpine and Intel Macs, build from source (below). If a downloaded binary doesn’t run on the host, the script leaves the existing one in place.
Each release vX.Y.Z has a companion release cli-vX.Y.Z that holds the binaries, each with a
.sha256 file. They live in a separate release because Shepherd’s releases are immutable once
published. To install or refresh by hand, run the same script the installer runs, from a checkout:
deploy/install-cli.sh # the checkout's version, into ~/.local/bindeploy/install-cli.sh 1.48.0 # a specific versionSHEPHERD_CLI_DIR changes the install directory. SHEPHERD_NO_CLI=1 makes install.sh and
update.sh skip the CLI. If ~/.cargo/bin comes before ~/.local/bin on your PATH, a
cargo installed shepherd shadows the managed one.
The crate lives in cli/. It needs a Rust toolchain (stable, 1.88 or newer):
cargo install --path cli # installs `shepherd` into ~/.cargo/binConnect and authenticate
Section titled “Connect and authenticate”By default the CLI talks to http://127.0.0.1:7330. To reach a remote server, for example the
Tailscale ts.net origin, pass --url, set SHEPHERD_URL, or store it in a profile.
Mint an access token in the web UI under Settings → Access, then store it:
shepherd login --token shp_… # default profile, local servershepherd --profile remote --url https://box.tail1234.ts.net login --token shp_…--token - reads the token from stdin, which keeps it out of your shell history and the process
list. login picks its server the same way as every other command (see below) and first checks the
token against it, storing nothing if it is rejected. A URL given by --url or SHEPHERD_URL is
saved in the profile with the token, so the two stay paired. The CLI
can’t mint tokens itself, because the server mints only for an interactive operator session.
The config file is $XDG_CONFIG_HOME/shepherd/config.toml, or ~/.config/shepherd/config.toml
when that variable is unset. This path is the same on every OS. The file is written with mode
0600 and its directory with 0700, because it holds a bearer token.
default_profile = "default"
[profiles.default]token = "shp_…"
[profiles.remote]url = "https://box.tail1234.ts.net"token = "shp_…"Where the URL comes from (first match wins): --url, then SHEPHERD_URL, then the profile’s
url, then http://127.0.0.1:7330.
Where the token comes from: SHEPHERD_TOKEN, then the profile’s token. A profile’s token is
sent only to that profile’s own URL. If --url or SHEPHERD_URL points somewhere else, the CLI
withholds the stored token and says so on stderr. Set SHEPHERD_TOKEN to authenticate there.
On Linux, the CLI also withholds a stored token from a loopback URL when the process listening on
that port belongs to another user, because any local account can bind a free loopback port.
The check runs again before every new connection that would carry the stored token, so a server
restart can’t hand the port to someone else: events tail and wait stop with an error instead of
reconnecting, and login --token refuses to send the token it is validating. A SHEPHERD_TOKEN
token is exempt.
--profile <name> picks a profile. Without it the CLI uses default_profile, and falls back to
default when that isn’t set.
Token scopes
Section titled “Token scopes”A token’s scope, set when it is minted, limits what the CLI can do:
| Scope | Commands |
|---|---|
read |
sessions list, sessions show (active sessions), status, holds, git (all sessions), reviews, events tail, wait (active sessions), login |
submit |
everything read can, plus new, held list|spawn|discard and train launch |
full |
everything else, including steer, interrupt, archive, resume, merge, merge-pr, go, halt, retry, epics, schedule, drain, up-next, settings, repo-config, diagnose, messages, git <session>, diff, cat, sessions list --all, sessions show or wait of an archived session, and the awaitingInput/pendingQuestion fields of sessions show |
The server’s 403 doesn’t say which scope was missing. The CLI names it for you, for example:
error: `shepherd steer` needs a 'full' token; this token's scope does not include it.
Output
Section titled “Output”- Tables when stdout is a terminal. On a terminal, control characters (ANSI/OSC escapes included) and bidi overrides in server, agent and peer text are dropped from stdout and stderr; JSON output is unaltered.
- JSON when stdout is not a terminal, or when you pass
--json. Each command prints one JSON document; list commands such assessions listprint one array. - NDJSON from
events tail, always. - Errors and warnings go to stderr only, so stdout stays machine-readable.
The CLI never prompts. It reads stdin only when you pass - as the text argument of new,
steer or schedule add.
Exit codes
Section titled “Exit codes”These codes are stable. New ones may be added, but existing ones never change meaning.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Unexpected failure |
| 2 | Usage error (bad flag or argument, bad URL, unknown profile) |
| 3 | Unauthenticated (401, or no token configured) |
| 4 | Insufficient token scope (403 insufficient_scope) |
| 5 | Not found (404) |
| 6 | Refused by the server (400, other 403, 409, 415, 422) |
| 7 | Server unreachable (connection, DNS, TLS, timeout) |
| 8 | Server error (5xx, or a response the CLI cannot decode) |
| 9 | wait only: --timeout passed first |
| 10 | wait only: needs-input |
| 11 | wait only: plan-ready |
| 12 | wait only: pr |
| 13 | wait only: done |
| 14 | wait only: halted |
| 75 | slot run only: the slot is taken, the command did NOT run (other codes are the command’s own) |
Commands
Section titled “Commands”A <session> argument takes a session id or the designation the UI shows: TASK-07, task-7,
or a bare 7. The CLI resolves designations against the active session list. sessions show also
finds an archived session by its designation, with archivedAt set.
| Command | What it shows |
|---|---|
shepherd sessions list [--all] |
Active sessions. --all adds archived ones after them (needs a full token) |
shepherd sessions show <session> |
One session. Archived sessions aren’t in the active list and need a full token |
shepherd status |
Server URL and version, CLI version, session counts by status, held sessions |
shepherd holds |
Sessions parked by a hold, and why |
shepherd git |
Each session’s cached pull-request state |
shepherd git <session> |
Its cached PR state plus the live worktree state (needs a full token) |
shepherd diff <session> |
The worktree against its fork point from the base (needs a full token) |
shepherd cat <session> <path> |
One file from the worktree (needs a full token) |
shepherd reviews |
Critic reviews running right now, with the provider, model and effort of each |
shepherd messages <session> |
The agent’s last messages, and the question it waits on (needs a full token) |
Both sessions list and sessions show report each session’s phase (the PHASE column, the
phase row, or the phase field in JSON). See Session phase.
messages reads the agent’s transcript on the server (Claude or Codex), so no SSH is needed.
-n <N> sets how many of the newest messages to show (default 5, at most 100). --include-user
adds the user’s and the operator’s steering messages, which are otherwise left out.
sessions show also reports awaitingInput and pendingQuestion. A session waits for input
when it is at rest (idle, done or blocked) and either an AskUserQuestion call is still
unanswered, or its last message ends its turn with a question (a ? in one of the last three
paragraphs). pendingQuestion is then the question, or the whole last message. The check is
deterministic and can miss a question asked without a ?, so read messages when in doubt. With
a token below full, both fields are null and a warning goes to stderr.
shepherd --json sessions show TASK-07 | jq '{awaitingInput, pendingQuestion}'shepherd messages TASK-07 -n 3 --include-userWorktree state
Section titled “Worktree state”git <session>, diff and cat show what a task has done before it opens a pull request, so no
SSH is needed. All three need a full token and work only for active sessions.
git <session> adds the worktree’s live state to the cached PR state. The server fetches the
base first. The base is the PR’s base when there is a PR, and otherwise the session’s own base.
| Field | Meaning |
|---|---|
mergeBase |
The commit the branch forked from |
baseSha |
The base’s tip (baseRef, usually origin/<base>) |
behindBase, aheadOfBase |
Commits on the base that the branch lacks, and the reverse |
upstream |
origin/<branch>, or null when the branch was never pushed |
unpushedCount, unpushed |
Commits past upstream (past the base when there is none), newest first, ≤50 |
dirty |
Uncommitted or untracked changes in the worktree |
A session without its own branch reports these fields as null. JSON output is
{"session", "pr", "worktree"}, and pr is null when the session has no cached PR.
diff compares the fork point with the working tree, so uncommitted edits to tracked files show
up. Untracked files don’t (see dirty). --stat prints per-file line counts instead of the
patch. -- <path>… keeps only those files and directories. A file without a patch shows as a
one-line note instead: a binary file, a file with more than 2000 changed lines (too large to show), or a change with no changed lines, such as a pure rename (old -> new: renamed), an empty
added or deleted file, or a mode change. JSON output is the server’s diff with the same filter
applied.
cat prints the file’s bytes as they are when stdout is redirected (or with --json). On a
terminal it drops control characters, as for any text an agent wrote. A path that leaves the
worktree or enters .git is not found (exit 5).
shepherd --json git TASK-07 | jq '.worktree | {behindBase, unpushedCount, dirty}'shepherd diff TASK-07 --stat -- src/shepherd cat TASK-07 .superpowers/plan.mdSession phase
Section titled “Session phase”phase says in one field whether a session needs you. status doesn’t: done means the agent
ended its turn, not that the task is finished. The server derives phase on every read of
GET /api/sessions and GET /api/sessions/{id}, from the session, its pull request, its plan
gate, any prompt on screen and the end of the transcript. The MCP sessions_list and
sessions_show tools report it too. It is not on /events frames, and a server older than the
CLI leaves it out (- in a table). When several rules hold, the first one wins:
| Phase | Holds when | You |
|---|---|---|
finished |
Archived, PR merged, halted as completed, or autopilot judged the task complete | nothing |
halted |
Halted for any other reason: usage limit, operator or error | act |
needs_input |
A prompt is on screen, autopilot handed back a question, the plan gate has open questions or requested changes, or the turn ended on a question | act |
plan_ready |
Planning, and the plan gate approved the plan | act |
planning |
Planning: writing the plan or under review | wait |
working |
The agent is running, also while a PR is open | wait |
pr_open |
At rest with an open PR | act |
idle |
At rest with no open PR and no question | act |
“Turn ended on a question” is the awaitingInput check described under Read. The
phase itself is readable with a read token; the question text still needs full.
The usual transitions:
- A plan-gated session starts in
planning, others inworking. planning→needs_input(questions, requested changes) →planning→plan_readyonce the reviewer approves.plan_ready→workingonshepherd go, on auto-release, or when you answer the session withshepherd steer: a reply to a session whose plan is approved counts as the go-ahead and moves itsplanPhasetoexecuting. Task amendments and the Operator Assistant’s steer don’t. A pull request appearing while still planning moves it toexecutingas well.- Each turn ends in
needs_input,pr_openoridle, and the next steer goes back toworking. - Any phase →
haltedon a halt, and back on retry. pr_open→finishedwhen the PR merges. Archiving finishes any session.
shepherd --json sessions list | jq -r '.[] | select(.phase | IN("plan_ready","needs_input","pr_open","halted","idle")) | .desig'Events
Section titled “Events”shepherd events tail [--session <session>] [--event <prefix>]… [--no-snapshot]/events doesn’t replay past events. So the CLI opens the socket first, then prints one synthetic
line, and only then streams:
{"event":"snapshot","data":{"sessions":[…],"holds":{…},"git":{…}}}After that line, every frame is printed verbatim as it arrives, as {"event": …, "data": …}.
Frames that arrived while the snapshot was loading are printed right after it, so nothing falls
between the snapshot and the stream.
If the connection drops, the CLI reconnects with backoff (1 s up to 30 s) and prints a fresh
snapshot. --event keeps only frames whose name starts with the prefix, and you can repeat it.
--session keeps only frames about one session and narrows the snapshot to that session.
Ctrl-C exits 0.
shepherd wait <session> [--until <state>[,<state>…]] [--timeout <duration>]wait blocks until the session is in one of the requested states. It then prints one line and
exits with that state’s code, so an orchestrator can drive a task like a sub-agent without its own
polling loop. A state that already holds returns at once. Without --until it waits for any of the
five:
| State | Exit | Holds when |
|---|---|---|
needs-input |
10 | The agent waits on a menu, yes/no or other answer, autopilot handed back with a question, or the plan gate has open questions or requested changes |
plan-ready |
11 | The session is in the planning phase and the plan gate approved the plan; shepherd go releases it |
pr |
12 | The session has a pull request, open or merged |
done |
13 | The session was archived, its pull request merged, or autopilot judged a non-PR task complete |
halted |
14 | The session halted: usage limit, operator or error |
When several requested states hold at once, the first of done, halted, needs-input,
plan-ready, pr is reported. So a merged PR reports done unless you asked only for pr.
The line is JSON when stdout is not a terminal (or with --json). It always carries state,
session, desig and status, plus pr (number, url, state) whenever the session has one:
{"state":"needs-input","session":"0b6f…","desig":"TASK-07","status":"idle","hold":"autopilot-paused","question":"Open the PR now, or wait for the migration review?"}{"state":"pr","session":"0b6f…","desig":"TASK-07","status":"idle","pr":{"number":412,"url":"https://github.com/o/r/pull/412","state":"open"}}{"state":"done","session":"0b6f…","desig":"TASK-07","status":"archived","reason":"merged","pr":{…}}needs-input adds hold (the server’s hold code, or null) and question (the text, when
there is one). halted adds haltReason. done adds reason (archived, merged or
complete) and, for complete, autopilot’s summary.
--timeout takes a whole number with an optional unit: 90, 90s, 30m, 2h, 1d. When it
passes first, wait prints {"state":"timeout", …} with the last status it saw and exits 9.
Without --timeout it waits indefinitely.
shepherd wait TASK-07 --until needs-input,pr --timeout 2hcase $? in 10) echo "TASK-07 is asking something" ;; 12) echo "TASK-07 opened its PR" ;; 9) echo "still working after two hours" ;;esacHow it works: wait opens /events first and then reads the session, its hold and its PR state
(GET /api/sessions, /api/holds, /api/git). It reads them again whenever a frame reports a
change to that session, and after every reconnect. So it never polls on a timer, and nothing that
changes between the read and the subscription is missed.
A read token is enough for active sessions. Two cases go further:
- An archived session isn’t in the active list, so
waitfinds it only with afulltoken. Waiting on it returnsdoneat once. GET /api/plan-gatesneeds afulltoken. With areadtoken,waitwarns on stderr and learns about approval from/events. That misses a plan that was already approved beforewaitstarted, until its gate changes again.
needs-input covers what the server can tell. When an agent without autopilot ends its turn with
a question in plain chat, the server doesn’t know it asked anything. Its status is just idle or
done, and wait doesn’t report needs-input.
Session control
Section titled “Session control”| Command | Route | Scope |
|---|---|---|
shepherd new [flags] <prompt|-> |
POST /api/sessions (--on: also POST /api/placement) |
submit |
shepherd steer <session> <text|-> |
POST /api/sessions/:id/reply |
full |
shepherd interrupt <session> |
POST /api/sessions/:id/interrupt |
full |
shepherd archive <session> |
DELETE /api/sessions/:id |
full |
shepherd resume <session> [--force] |
POST /api/sessions/:id/resume |
full |
shepherd go <session> |
POST /api/sessions/:id/go |
full |
shepherd halt --yes |
POST /api/halt |
full |
shepherd retry [<session>…] [--text] |
POST /api/retry |
full |
go releases an approved plan gate and starts execution. The server refuses (exit 6) when the
session isn’t in the planning phase or its plan isn’t approved.
halt interrupts every live working agent at once, the fleet-wide emergency stop. It needs
--yes; without it the CLI exits 2 and sends nothing.
retry resumes halted sessions and steers each to continue. Without sessions it takes every
session the usage limit halted, and prints nothing to retry when there is none. --text replaces
the default steer, the one the UI’s Retry dialog sends.
Flags for new:
--repo <path>: the repository path on the server. Defaults to the current directory’s main git checkout (the repository root, also when run from a linked worktree), which is only right when the CLI runs on the server’s host.--base <branch>: the base branch. Defaults tomain.--model,--effort <low|medium|high|xhigh|max|ultra>,--provider <claude|codex>: override the server’s defaults.--plan-gate,--autopilot: turn on the plan gate or autopilot for this session.--force: spawn now even if the usage hold would queue the task.--after <task>: hold the task until that task’s PR has landed (id or designation, e.g.TASK-1490; merged, or squash-merged into its epic branch). It is queued as a held task with reasonafterand spawns on a fresh base once the PR lands. A predecessor that already landed spawns it now; an unknown one is404, one archived without landing is409.held spawnstarts it early,held discarddrops it.--on <machine>: run the whole session on that machine, by name (case doesn’t matter) or instance id. The CLI looks it up among the machinesPOST /api/placementlists for--repo(this one first, then every connected machine that can take tasks and has the project checked out) and sends it astarget. Naming this machine is allowed and means “here”. An unknown or ambiguous name exits2and lists the machines; so does any other name while no other machine is connected. The server refuses a machine that doesn’t fit (exit6), e.g.missing_capabilitiesorremote_target_unavailable. Without--on, the server chooses (Linux before macOS by default).--require <capability>: only a machine with this capability may run the task; repeat it or separate values with commas. Sent asrequirements. Known values:platform:linux,platform:macos,platform:windows,xcode,sdk:macos,sdk:ios,ios-simulator,android-sdk,agent:claude,agent:codex,sandbox:trusted,sandbox:standard,sandbox:autonomous. Another value draws a warning on stderr and is sent anyway, so a newer server can accept it; one the server doesn’t know is400(exit6). With--on, the server checks that machine has them all. When no machine fits, the refusal (exit6,no_suitable_machine) names the missing capabilities and any machine that has them but no checkout of the project.--afterwith--onexits2without sending anything: a dependent task always runs on this machine, where its predecessor lives.--afterwith--requireis allowed; the requirements are then checked against this machine.
When the usage hold trips, new still exits 0. It prints the held task ({"held": true, …})
instead of a session.
In text mode, a session placed on a machine names it: created TASK-12 (…) — name on macbook.
Once running, an idle agent is steered when its base branch gains commits it does not have (checked every 2 minutes, at most one notice per 30 minutes), with the list of new commits, so it can rebase if they affect its work.
shepherd new --repo ~/Work/my-repo "Add OAuth login to the settings page"shepherd new --repo ~/Work/my-repo --after TASK-07 "Use the new OAuth hook on the profile page"shepherd new --repo ~/Work/my-app --on macbook "Fix the iOS share sheet crash"shepherd new --repo ~/Work/my-app --require xcode --require sdk:ios "Add a widget extension"echo "please rebase onto main" | shepherd steer TASK-07 -shepherd --json sessions list | jq -r '.[] | select(.status == "done") | .desig'Work intake
Section titled “Work intake”A --repo <path> flag defaults to the current directory’s main git checkout, as it does for new.
| Command | Route | Scope |
|---|---|---|
shepherd backlog |
GET /api/backlog |
full |
shepherd issues [--repo] |
GET /api/issues?repo= |
full |
shepherd drain status |
GET /api/drain |
full |
shepherd drain queue [--repo] |
GET /api/drain/queue?repo= |
full |
shepherd drain start|stop [--repo] |
PUT /api/repo-config?repo= (autoDrainEnabled) |
full |
shepherd up-next list |
POST /api/up-next/refresh, then GET /api/up-next?peek=1 |
full |
shepherd up-next start <item>… [flags] |
same, then POST /api/up-next/start |
full |
shepherd held list |
GET /api/held |
submit |
shepherd held spawn <id> [--provider] |
POST /api/held/:id/spawn |
submit |
shepherd held discard <id> |
DELETE /api/held/:id |
submit |
backlog hides repos hidden in the UI from its table. Its JSON carries every repo with a hidden
flag. drain start and drain stop flip the repo’s auto-drain setting, the same one the UI
toggles.
up-next opens /events before asking the server to recompute the queue. After an
upnext:changed signal, it reads GET /api/up-next?peek=1 and prints that snapshot. This
cache-only read does not trigger another recomputation. Older servers can still send a legacy
upnext:snapshot frame directly. The recompute lists every repo on its forge, so it can take a
while; the CLI waits up to 90 s and then exits 1.
up-next start takes items as <repo>#<number>, where <repo> is the slug (owner/repo), the
repo name or its label. A bare <number> works when only one queued item has it. An unknown or
ambiguous item exits 2 and names the candidates. --provider <claude|codex>, --model and
--effort override the defaults; --model and --effort need --provider. A start that fails
for every item exits 8 and names each failure.
Reviews and merge
Section titled “Reviews and merge”| Command | Route | Scope |
|---|---|---|
shepherd review-pr <session> |
POST /api/sessions/:id/review-pr |
full |
shepherd review-plan <session> |
POST /api/sessions/:id/review-plan |
full |
shepherd merge <session> [--method] [--keep-branch] [--takeover] |
POST /api/sessions/:id/git/merge |
full |
shepherd train status |
GET /api/automerge |
full |
shepherd train start|stop [--repo] |
PUT /api/repo-config?repo= (autoMergeEnabled) |
full |
shepherd train set <session> on|off|default |
PUT /api/sessions/:id/automerge |
full |
shepherd merge-pr <number> [--repo] [--method] [--keep-branch] [--takeover] |
POST /api/prs/merge |
full |
shepherd train launch [<number>…] [--repo] [--base] |
POST /api/sessions |
submit |
review-pr and review-plan start the critic or the plan review now and print what the server
did, for example started or running.
merge --method <merge|squash|rebase> picks the merge method; the forge default applies without
it. --keep-branch keeps the head branch. When someone else is responsible for the pull request,
the server refuses the merge and the CLI exits 6 and suggests --takeover. --takeover confirms
the takeover with the PR state the server has cached (head commit, target branch and who is
responsible), and the server refuses again if any of it changed.
merge-pr merges a repo’s pull request by number, with or without a session, like Merge in
the backlog’s PR panel. --method and --keep-branch work as for merge. --takeover has no
cached state to echo, so it sends the merge unconfirmed first. When the server refuses because
someone else is responsible, the CLI echoes the PR state from that refusal and retries once; the
server rechecks every field, and a second refusal is final.
train is the full-auto merge train. train start and train stop flip the repo’s setting.
Like the UI toggle, train start also turns off the repo’s draft mode, because the two can’t both
be on. train set overrides the train for one session, and default goes back to the repo setting.
train launch spawns a merge-train agent session, like Merge train in the Herd or the PR
panel: it reviews the PRs, proposes a merge order and waits for your approval before merging.
Without numbers it takes every PR flagged ready to merge whose review isn’t running. --repo
narrows them to one repo; without it the CLI picks the repo with the most ready PRs and warns on
stderr about the ones it left out. No ready PR exits 6. With numbers it runs over exactly those
PRs in --repo (default: the current directory’s main git checkout). The session skips the plan gate
and autopilot and bypasses the usage hold. --base defaults to main.
shepherd up-next listshepherd up-next start owner/repo#42 --provider claudeshepherd merge TASK-07 --method squashshepherd merge-pr 1234 --repo ~/Work/my-reposhepherd train launch --repo ~/Work/my-repo<parent> is the epic’s parent issue number. --repo defaults to the current directory’s main
git checkout.
| Command | Route | Scope |
|---|---|---|
shepherd epics list [--repo] |
GET /api/epics?repo= |
full |
shepherd epics show <parent> [--repo] |
GET /api/epic?repo=&parent= |
full |
shepherd epics start <parent> [--mode] [--provider] [--model] [--effort] |
PUT /api/epic?repo=&parent= |
full |
shepherd epics pause|stop <parent> [--repo] |
PUT /api/epic?repo=&parent= |
full |
shepherd epics approve-next <parent> [--repo] |
POST /api/epic/approve-next?repo=&parent= |
full |
epics show prints the epic’s run settings, its warnings and one row per child with its state,
blockers, PR and session. start sets the run to running and, when given, its mode
(auto|attended), coding agent, model and effort; pause and stop set it to paused or idle,
like the epic panel’s buttons. approve-next approves the next child spawn of an attended epic.
The server keeps one epic run per repo, so pause, stop and approve-next first read the
epic and exit 6 unless its own run is in the right state: running for pause, running or paused
for stop, running and attended for approve-next.
A server without the drain answers 503, so these exit 8.
Schedules
Section titled “Schedules”A schedule runs a prompt or /skill as a fresh session in one repo, on chosen weekdays at
server-local times.
| Command | Route | Scope |
|---|---|---|
shepherd schedule ls [--repo] |
GET /api/schedules?repo= |
full |
shepherd schedule add [flags] <prompt|-> |
POST /api/schedules |
full |
shepherd schedule run <schedule> [--repo] |
POST /api/schedules/:id/run |
full |
schedule ls (also schedule list) lists every repo’s schedules, or one repo’s with --repo,
with the cadence, whether each is on, its next firing and its last run.
Flags for schedule add:
--name <name>(required): 1–80 characters.--at <HH:MM>(required, repeatable, up to 12): a server-local time to fire at.--days <days>: comma-separatedmon…sun,weekdays,weekendsordaily(the default), e.g.--days mon,wed,fri.--repo <path>: defaults to the current directory’s main git checkout, as fornew.--disabled: create it switched off.--provider,--model,--effort,--autopilot,--plan-gate,--base <branch>: launch options for every firing, as fornew. Omitted, they resolve when the schedule fires: the server’s defaults, attended (no autopilot), the repo’s plan-gate setting and its default branch.
A malformed --at or --days exits 2 before anything is sent. Slots already past when you add
a schedule never fire.
schedule run fires a schedule now, even a disabled one. <schedule> is its id, or its exact
name when that names one schedule (--repo narrows the lookup); an unknown name exits 5 and a
name several schedules share exits 2. It prints the run, then exits 0 when the run spawned a
session or was held by the usage hold, 6 when it was skipped (its previous run is still open)
and 1 when the launch failed; the reason is on stderr.
shepherd schedule add --repo ~/Work/my-repo --name triage --days weekdays --at 09:00 /triage-feedbackshepherd schedule run triageHeavy-check slot
Section titled “Heavy-check slot”shepherd slot run -- cargo clippy # one shell string, or the words after `--`Runs the command through the host-wide heavy-check slot (#3181), the same way the tool guard does for Claude sessions: Shepherd grants the slot before the command starts, the command runs with the slot bound to its processes, and the slot frees once they are gone. If the slot is taken, nothing runs: the place and estimate are printed to stderr and the exit code is 75. Otherwise the exit code is the command’s own (128 + signal when it was killed). If the server cannot be reached or answers with an error, the command runs unmanaged with a one-line note.
- In a session (
SHEPHERD_SESSION_ID,SHEPHERD_AGENT_API_URLandSHEPHERD_INGRESS_HEADER_FILEset — also inside a sandbox membrane) it uses the session’s own ingress key; a session that has to wait is steered when it is its turn. - In an operator terminal it uses your token (
fullscope). A terminal cannot be steered, so a command that would have to wait is not queued: run it again later.
Codex sessions get the same behaviour without typing slot run: Shepherd puts PATH shims for
bun, npm, cargo, vitest, tsc and the like in front of the tools, and they route only
what the tool guard calls heavy. Direct paths (./node_modules/.bin/vitest) and shells that reset
PATH bypass them.
Settings and diagnostics
Section titled “Settings and diagnostics”| Command | Route | Scope |
|---|---|---|
shepherd settings |
GET /api/settings |
full |
shepherd settings set <key> <value|-> |
PATCH /api/settings |
full |
shepherd repo-config [--repo] |
GET /api/repo-config?repo= |
full |
shepherd repo-config set <key> <value> [--repo] |
PUT /api/repo-config?repo= |
full |
shepherd diagnose [--refresh] |
GET /api/diagnostics |
full |
shepherd diagnose fix <check> |
POST /api/diagnostics/fix |
full |
settings and repo-config print one row per key. Use the key names they print with set.
set changes one key per call. The value is read as a JSON literal first (true, 80,
["a.com"]) and as plain text when it isn’t one, so shepherd settings set defaultModel opus
needs no quotes. The CLI checks the key and the value’s type before sending anything; an unknown
key or a value of the wrong type exits 2. The validated value is sent as written, so null and
[] reach the server, which reads them as “clear”: repo-config set egressExtraHosts '[]'
removes every extra egress host. repo-config set browserAllowedHosts '["accounts.example.com"]'
sets the Browser Origin Allowlist an autonomous session’s Shared Browser attach may reach.
anthropicApiKey is only read from stdin, so the key stays out of your shell history and the
process list. A value on the command line exits 2, and so does empty stdin, because the server
would read a blank key as “clear”. shepherd settings set anthropicApiKey null clears it. The line
the CLI prints comes from the server’s hasApiKey, not from what was sent:
shepherd settings set anthropicApiKey - < ~/.secrets/anthropic-keydiagnose prints each environment check with its state, its hint key and, when the check has
one, the fix the server can run. It exits 0 whatever the checks say; read overall in the JSON
to branch on it. --refresh probes again instead of answering from the cached snapshot.
diagnose fix <check> runs that check’s fix on the server host, then prints the check as it
stands after a fresh probe. An unknown check, or one without a fix, exits 6; a fix that fails on
the server exits 8.
shepherd settings set usageHoldPct 80shepherd repo-config set maxAuto 3 --repo ~/Work/my-reposhepherd --json diagnose | jq -r '.checks[] | select(.state != "ok") | .id'