Surviving reboots
shep startup writes an init unit for the target user: systemd, launchd, openrc, or a BSD rc.d script, picked automatically for the running host or named explicitly with--init. shep unstartup takes it back out. Neither talks to a running shepherd; both write and remove a single unit file.
Never escalates its own privilege
Installing a unit needs root. shep never asks for it. Without root it prints the exact command to run and exits non-zero, so a script notices instead of silently doing nothing:
$ shep startup error[failure]: installing the unit needs root; run: sudo /opt/shep/target/release/shep startup --user deploy --home /tmp/shep_startup_demo $ shep unstartup ACTION TARGET RESULT removed /Library/LaunchDaemons/io.github.turtiesocks.shep.deploy.plist absent
A unit that was never installed reports absent rather than failing. There's nothing left to guess at, on either verb. Undersudo, the unit is built for $SUDO_USER rather than root, so it supervises the flock the operator actually has.
The PATH capture, and its one trap
A daemon started by systemd or launchd has no login shell and no.bashrc/.profile behind it. Without help,PATH at boot would be whatever minimal default the init system starts with, and any app whose interpreter lives outside/usr/bin//bin would fail to spawn on the very first boot after migration, silently, until someone reboots the box again to find out why. shep startup captures thePATH of the shell that ran it directly into the unit (Environment="PATH=..." on systemd, the plist'sEnvironmentVariables on launchd): the mechanism that makes an interpreter installed under ~/.bun/bin or~/.cargo/bin findable after a reboot.
sudo on most distributions replaces PATH with its own secure_path before your command ever runs: often dropping the exact ~/.bun or ~/.cargo entry the capture above exists to preserve. shep can't tell a sanitizedPATH from an untouched one after the fact, but it knows when it's running under sudo and warns at install time, naming the PATH about to go into the unit. If it's missing something, rerun as sudo --preserve-env=PATH shep startup ... instead (after shep unstartup, sincestartup refuses to overwrite the unit it just wrote).
The muster roll
The unit brings the shepherd back. What brings the flock back is a file: $SHEP_HOME/flock.json, the muster roll, holding every registered app's config and how many of its instances were running when it was written.
shep saveWrites the roll now and reports where it landed and how many apps it recorded. Not the only thing that writes it: the shepherd keeps the roll current on its own, folding a burst of lifecycle events into one write so a restart storm costs one rewrite rather than one per event. This verb exists so you can see the roll is on disk before a reboot instead of trusting that it is, which is why a failed save is loud rather than a quiet no-op. It refuses when no shepherd is running rather than starting one, since a shepherd started to answer a save has an empty flock, and writing that would replace a good roll with a blank one.
shep musterAssembles the flock from that roll, starting a shepherd first if none is running. Only two verbs in the CLI will do that, this one and shep start, and this is the one the init unit calls at boot: the restore that runs unattended after a reboot is the same code path you can drive by hand.
$ shep save FILE APPS /tmp/shepdocs/home/flock.json 2 $ shep muster ID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIME FOLD SMIT 0 web:0 online 61835 0 - - - 0s - - 1 web:1 online 61836 0 - - - 0s - - 2 worker online 61837 0 - - - 0s - -
Those pids are new because that muster ran with no shepherd at all: it started one, the boot restore brought the flock up, and the request that followed reported what came back.
An app restores if it was running when the roll was saved and its autostart is still true. Being up when the roll was written is the contract, and autostart = false is the explicit opt-out of being brought back. An app the flock already has is left exactly where it stands and still reported as restored, which is what makes the verb safe to run twice: an init system that calls it a second time gets the same honest answer rather than a duplicate flock. A roll that restored nothing says so on stderr rather than exiting 0 in silence.
The roll is the one place shep writes secrets to disk. It stores each app's env verbatim, because a restore has to reproduce it, while every other surface redacts them. On unix the file is written owner-only, through a temp file and a rename so a reader never sees a half-written roll. It is plain JSON and editable by hand, and every entry is re-validated on the way back in, with failures collected and reported rather than aborting the whole muster.
Is it up, and stopping it on purpose
shep pingAsks whether a shepherd is answering, and says which one: its version, its pid, the home it is serving and the socket it is listening on. Offline it still names the home and the socket, since the usual reason for a surprise is that --home or $SHEP_HOME is pointing somewhere other than where you thought, and exits 5 so a script can branch on it. It never starts a shepherd.
shep killShuts the shepherd down, and the flock with it. Every online sheep goes through the full stop ladder first, so this is an orderly shutdown rather than a rug pull, and the socket is unlinked last.
$ shep ping shepherd online version 0.1.15 pid 61682 home /tmp/shepdocs/home socket /tmp/shepdocs/home/run/shep.sock $ shep kill PID SOCKET_REMOVED 61682 true $ shep ping shepherd offline home /tmp/shepdocs/home socket /tmp/shepdocs/home/run/shep.sock $ echo $? 5
A reply is not treated as success. shep kill waits for the control address to stop answering before reporting one, which is what makes shep kill && shep start safe: without the wait, the new shepherd could claim the address before the old one had released it. On unix that means watching the socket file disappear, since the daemon unlinks it last. On Windows the address is a named pipe with no directory entry to watch, so shep kill probes it instead, and a connect that fails because the name is gone is the same proof.
Honestly: openrc and the BSDs are untested
All four unit renderers (systemd, launchd, openrc, FreeBSD/OpenBSDrc.d) are pinned by exact-string tests, the same tier the systemd unit has always had, since it too has only ever beenrendered, on a Mac. That's a real and adequate tier for text. It is not a claim that the openrc or BSD scripts work: no FreeBSD, OpenBSD, or openrc host exists on this project's own machines, and nobody has run them on their own init system. Nothing in these docs claims otherwise until someone reports back from a host that actually runs one.
One functional difference is already known, not just untested: openrc has no sd_notify analogue, so its script'sstart_post() polls the shepherd's own control socket instead and blocks the "started" verdict until the first request is answered: the same milestone READY=1 proves on systemd, one step later. FreeBSD gets the same poll throughstart_postcmd. OpenBSD's rc.subr has no documented post-start hook at all; its script reports started as soon as the process is spawned and says so in its own header comment, naming shep flock as the real check.