Surviving a reboot
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.
active (running) means something specific: the generated unit is Type=notify, so systemd doesn't consider it started until the daemon itself says so: it only sends that signal once its muster restore has finished. A green status at step 3 is already evidence the restore path works, before the reboot ever happens.Rolling back
shep unstartup disables and removes whatever unit startup installed. Run unprivileged, it prints the same kind of paste-able sudo command startup does. A machine that never ran startup reports the unit absent and exits successfully. Nothing left to guess at.
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 CFG 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.
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.
Never escalates its own privilege
Installing a unit needs root. 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
Under sudo, the unit is built for $SUDO_USER rather than root, and its $SHEP_HOME is that user's own ~/.shep unless --home or a $SHEP_HOME that survived sudo says otherwise. If it isn't there yet, run any shep verb as that user first, or point --home or $SHEP_HOME at a directory that already exists: a named home is refused when missing, never created.
The PATH capture, and its one trap
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. shep startup captures the PATH of the shell that ran it directly into the unit (Environment="PATH=..." on systemd, the plist's EnvironmentVariables 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: often dropping the exact ~/.bun or ~/.cargo entry the capture above exists to preserve. shep can't tell a sanitized PATH from an untouched one, 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, since startup refuses to overwrite the unit it just wrote).Honestly: openrc and the BSDs are untested
All four unit renderers (systemd, launchd, openrc, FreeBSD/OpenBSD rc.d) are pinned by exact-string tests, the same tier the systemd unit has always had. 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. 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's start_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 through start_postcmd. OpenBSD's rc.subr has no documented post-start hook at all; its script reports started as soon as the process is spawned, naming shep flock as the real check.