Skip to content

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.

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:

Terminal window
deploy/install-cli.sh # the checkout's version, into ~/.local/bin
deploy/install-cli.sh 1.48.0 # a specific version

SHEPHERD_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):

Terminal window
cargo install --path cli # installs `shepherd` into ~/.cargo/bin

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:

Terminal window
shepherd login --token shp_… # default profile, local server
shepherd --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.

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.

  • 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 as sessions list print 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.

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)

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.

Terminal window
shepherd --json sessions show TASK-07 | jq '{awaitingInput, pendingQuestion}'
shepherd messages TASK-07 -n 3 --include-user

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).

Terminal window
shepherd --json git TASK-07 | jq '.worktree | {behindBase, unpushedCount, dirty}'
shepherd diff TASK-07 --stat -- src/
shepherd cat TASK-07 .superpowers/plan.md

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 in working.
  • planning → needs_input (questions, requested changes) → planning → plan_ready once the reviewer approves.
  • plan_ready → working on shepherd go, on auto-release, or when you answer the session with shepherd steer: a reply to a session whose plan is approved counts as the go-ahead and moves its planPhase to executing. Task amendments and the Operator Assistant’s steer don’t. A pull request appearing while still planning moves it to executing as well.
  • Each turn ends in needs_input, pr_open or idle, and the next steer goes back to working.
  • Any phase → halted on a halt, and back on retry.
  • pr_open → finished when the PR merges. Archiving finishes any session.
Terminal window
shepherd --json sessions list | jq -r '.[] | select(.phase | IN("plan_ready","needs_input","pr_open","halted","idle")) | .desig'
Terminal window
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.

Terminal window
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.

Terminal window
shepherd wait TASK-07 --until needs-input,pr --timeout 2h
case $? in
10) echo "TASK-07 is asking something" ;;
12) echo "TASK-07 opened its PR" ;;
9) echo "still working after two hours" ;;
esac

How 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 wait finds it only with a full token. Waiting on it returns done at once.
  • GET /api/plan-gates needs a full token. With a read token, wait warns on stderr and learns about approval from /events. That misses a plan that was already approved before wait started, 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.

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 to main.
  • --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 reason after and spawns on a fresh base once the PR lands. A predecessor that already landed spawns it now; an unknown one is 404, one archived without landing is 409. held spawn starts it early, held discard drops 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 machines POST /api/placement lists for --repo (this one first, then every connected machine that can take tasks and has the project checked out) and sends it as target. Naming this machine is allowed and means “here”. An unknown or ambiguous name exits 2 and lists the machines; so does any other name while no other machine is connected. The server refuses a machine that doesn’t fit (exit 6), e.g. missing_capabilities or remote_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 as requirements. 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 is 400 (exit 6). With --on, the server checks that machine has them all. When no machine fits, the refusal (exit 6, no_suitable_machine) names the missing capabilities and any machine that has them but no checkout of the project.
  • --after with --on exits 2 without sending anything: a dependent task always runs on this machine, where its predecessor lives. --after with --require is 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.

Terminal window
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'

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.

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.

Terminal window
shepherd up-next list
shepherd up-next start owner/repo#42 --provider claude
shepherd merge TASK-07 --method squash
shepherd merge-pr 1234 --repo ~/Work/my-repo
shepherd 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.

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-separated mon…sun, weekdays, weekends or daily (the default), e.g. --days mon,wed,fri.
  • --repo <path>: defaults to the current directory’s main git checkout, as for new.
  • --disabled: create it switched off.
  • --provider, --model, --effort, --autopilot, --plan-gate, --base <branch>: launch options for every firing, as for new. 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.

Terminal window
shepherd schedule add --repo ~/Work/my-repo --name triage --days weekdays --at 09:00 /triage-feedback
shepherd schedule run triage
Terminal window
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_URL and SHEPHERD_INGRESS_HEADER_FILE set — 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 (full scope). 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.

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:

Terminal window
shepherd settings set anthropicApiKey - < ~/.secrets/anthropic-key

diagnose 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.

Terminal window
shepherd settings set usageHoldPct 80
shepherd repo-config set maxAuto 3 --repo ~/Work/my-repo
shepherd --json diagnose | jq -r '.checks[] | select(.state != "ok") | .id'