Autostart
sudo runwisp service installThat’s the whole thing on a Linux box. It writes
/etc/systemd/system/runwisp.service, enables it, starts it, and — if
cron is still running and RunWisp is reading its crontabs — asks whether
you’d like cron retired while it’s at it.
One host, one system service, running as root, named runwisp.service.
That’s the shape almost everyone wants, so it’s the default.
If you’d rather have a daemon that belongs to your user account, pass
--local: you get ~/.config/systemd/user/runwisp-<fingerprint>.service
on Linux (with linger enabled so it survives logout) or
~/Library/LaunchAgents/com.runwisp.daemon.<fingerprint>.plist on macOS.
Several of those can coexist on one host — that’s what the fingerprint in
the name is for. macOS has no system-wide install yet, so --local is
required there.
Either way it never touches runwisp.toml — this is purely OS plumbing.
The four commands
Section titled “The four commands”sudo runwisp service install # wire up systemd (or launchd, with --local)runwisp service status # show autostart wiring and any driftrunwisp service uninstall # remove the unit (data dir is preserved)runwisp service install --print > custom.service # Ansible / Nixstatus, uninstall, stop, and restart find whichever unit is
actually installed — you don’t have to tell them how you installed it.
Pass --local only to disambiguate when a system unit and a user unit
are both present; they’ll say so and ask.
Don’t mix up the two status commands: runwisp status answers “is the
daemon alive right now?”, while runwisp service status answers “will it
come back on its own after a reboot?”.
Stop & restart
Section titled “Stop & restart”runwisp stop and runwisp restart are service-aware. When the
daemon runs under a unit installed by service install, they
delegate to the service manager (systemctl --user stop|restart,
or launchctl on macOS) so its view of the unit stays in sync —
a raw SIGTERM would just make systemd restart the daemon behind
your back, and stopping it by hand would leave the manager thinking
it crashed. Without a managed unit, they fall back to a plain
SIGTERM + wait against the PID file.
runwisp restart # the way to apply runwisp.toml editsrunwisp stop # stops the daemon; the unit stays enabled for next bootrestart is also one answer to the “runwisp.toml has changed since
the daemon started” notice that runwisp status, the TUI header,
and the Web UI banner show after you edit the config. For most edits
runwisp reload clears it without bouncing the
process; reach for restart when you’ve changed a restart-only setting
([daemon], scheduler timezone, [storage], [notify], the bind
host/port) or want a fresh boot to re-fire run_on_start and catch-up.
What install does
Section titled “What install does”The system install (the default, Linux and WSL, needs root):
- Writes
/etc/systemd/system/runwisp.servicewith the resolved binary path, config path, data dir, port, and host baked in. The file starts with a# Managed by runwisp service installmarker so a later re-run can tell whether it can safely overwrite. systemctl daemon-reload.- Offers to stop and mask cron, if cron is running and RunWisp is reading its crontabs. See Take over from cron.
systemctl enable --now runwisp.service.
There’s no fingerprint in the name: there’s exactly one system daemon per
host, so the unit name mustn’t depend on which directory you happened to
run the install from — otherwise cd /etc && sudo runwisp service status
would report “not installed” against the unit cd / created.
The unit is ordered after remote-fs.target and nss-user-lookup.target
(same as cron.service, in case the config or an account database lives
on a network mount or an NSS backend) and time-sync.target (so a
boot-time NTP clock step lands before scheduling starts on an RTC-less
box), and it’s WantedBy=multi-user.target.
Running as root, the sudo prefix on every systemctl/journalctl call
is skipped — a minimal container root image may not have sudo installed
at all, and root already has the privilege.
--local on Linux and WSL instead:
- Writes
~/.config/systemd/user/runwisp-<fingerprint>.service. The unit name carries the daemon’s human-readable fingerprint (e.g.bright-falcon) so multiple RunWisp instances on the same host install side-by-side. systemctl --user daemon-reload.sudo loginctl enable-linger <user>— only when linger isn’t already on. This is the single privileged step.systemctl --user enable --now runwisp-<fingerprint>.service.
--local on macOS:
- Writes
~/Library/LaunchAgents/com.runwisp.daemon.<fingerprint>.plistwith the same managed-marker preamble. The LaunchAgent label matches (com.runwisp.daemon.<fingerprint>), so multiple instances coexist without overwriting each other. launchctl bootout(best effort, in case an old copy is loaded).launchctl bootstrap gui/$UID <path>thenlaunchctl enable.
On WSL, the systemd path runs as on Linux and the command prints a
Register-ScheduledTask PowerShell snippet you paste into a Windows
shell so the distro boots on login.
When install refuses
Section titled “When install refuses”Three combinations can’t work, and each stops before anything is written:
runwisp service installwithout root. The system service needs root. Re-run withsudo, or pass--localif you meant a user-scoped daemon. RunWisp won’t silently downgrade you to one — a unit that dies with your login session is not what someone typing the bare command on a VPS is after.runwisp service installon macOS. There’s no root LaunchDaemon implementation yet, so--localis required.sudo runwisp service install --local.systemctl --userhas no bus for root under sudo, so the unit would be written and then never start.
The same rules apply to --print and --dry-run, which resolve the scope
before rendering anything — sudo runwisp service install --print is how
you inspect the system unit without installing it.
A confirmation banner lists every action and the resolved settings
before anything is written. --yes skips the prompt for CI; it does
not skip the literal-word confirmation that --purge requires.
Resolving paths
Section titled “Resolving paths”A unit that boots at login can’t rely on your shell or your current
directory, so it needs absolute, durable paths for both the binary and
the data dir. Here’s how runwisp service install works them out:
- Binary:
os.Executable()followed byEvalSymlinks. The installer rejects transient locations outright (/tmp/,/var/tmp/,/dev/shm/, thego-buildcache used bygo run). It warns on awkward-but-acceptable ones (~/go/bin,~/.cache, paths with spaces) and suggests copying to~/.local/bin/runwisp. Override with--binaryfor Ansible/Nix. - Data dir and config, system install:
/var/lib/runwispand/etc/runwisp/runwisp.toml— the same defaultsrunwisp daemonitself picks when it runs as root, sosudo runwisp reloadlater finds the same paths. An explicit--data/--configoverrides either, and then goes through the same validation as below.--dataand--confignever change the scope — only--localdoes that. - Data dir,
--local: an absolute--datais accepted silently. A relative--datais resolved to its absolute form and baked into the unit — a boot-launched unit hascwd=/, so the absolute path is what makes it work. That meansrunwisp service install --local --data .installs the daemon right here, storing its data in the current directory. The one hard limit is durability: a path under/tmp,/var/tmp, or/dev/shmis still rejected because it won’t survive a reboot. The default./.runwispis upgraded interactively: if a DB already exists there, the installer asks whether to keep using it (default yes); otherwise it offers$XDG_DATA_HOME/runwisp(or~/.local/share/runwispif XDG is unset), and if you decline that it offers the current directory before giving up. - Config,
--local: same resolution as the daemon. When both~/.config/runwisp/runwisp.tomland./runwisp.tomlexist, the XDG location wins.
A system install also checks that the resolved config is root-owned and
not group- or world-writable before baking it into the unit — that unit
runs as root, so sudo runwisp service install from a directory holding
someone else’s runwisp.toml must not hand that file root’s shell.
Idempotent re-install
Section titled “Idempotent re-install”Re-running the install command on an unchanged host is a no-op:
| Existing unit | Outcome |
|---|---|
| absent | Install |
| managed, content matches | Noop — exits 0 with Already installed. ✓ |
| managed, content drifted | Update — shows a unified diff, asks before writing |
| hand-written (no marker) | Conflict — refuses; pass --force to overwrite |
The hashes embedded in the unit’s # runwisp-config-hash: and
# runwisp-binary-sha256: lines are how service status notices that
the operator replaced the binary or changed a flag since installing.
Installing over a service that’s already running is allowed — that’s what
a cron take-over does — but systemd
keeps a running unit on the settings it started with, so an install that
changed the port, config, or data dir says as much and points at
runwisp restart. A daemon you started by hand is the one thing install
still refuses to work around: nothing can start the unit while another
process owns the data dir, so stop it (runwisp stop --data <dir>) first.
service status
Section titled “service status”RunWisp service status
Installed: yes Autostart: enabled Running: yes Unit file: /etc/systemd/system/runwisp.service (matches recorded settings) Binary: /usr/local/bin/runwisp Data dir: /var/lib/runwisp (last write 2026-05-20 10:32:01) Last start: 2026-05-19 09:00:11 UTC Logs: sudo journalctl -u runwisp.serviceA --local install adds a Linger: row (a system unit starts under PID 1
and has no session to linger).
An install that retired cron
adds one more line reporting whether the masked cron unit is still
masked. active — jobs may be firing twice there means something brought
cron back after the take-over; re-run sudo runwisp takeover to retire it
again:
Cron: cron.service masked by this instanceExit codes are part of the contract:
| Code | Meaning |
|---|---|
| 0 | healthy (installed, enabled, running, no drift) |
| 1 | degraded (installed but disabled / stopped / drift) |
| 2 | not installed |
Secrets on systemd
Section titled “Secrets on systemd”runwisp service install never copies
RUNWISP_PASSWORD into the
unit — secrets stay off disk. The recommended pattern is a drop-in
file alongside the managed unit:
sudo mkdir -p /etc/systemd/system/runwisp.service.dsudo tee /etc/systemd/system/runwisp.service.d/password.conf > /dev/null <<'EOF'[Service]Environment=RUNWISP_PASSWORD=…EOFsudo chmod 600 /etc/systemd/system/runwisp.service.d/password.confsudo systemctl daemon-reloadsudo systemctl restart runwisp.serviceFor a --local install the same pattern applies under
~/.config/systemd/user/, with <fingerprint> replaced by the slug
runwisp service status reports for that instance (e.g. bright-falcon):
unit=runwisp-<fingerprint>.servicemkdir -p ~/.config/systemd/user/$unit.dcat > ~/.config/systemd/user/$unit.d/password.conf <<'EOF'[Service]Environment=RUNWISP_PASSWORD=…EOFchmod 600 ~/.config/systemd/user/$unit.d/password.confsystemctl --user daemon-reloadsystemctl --user restart $unitDrop-ins live next to the managed unit but outside the unit file
itself, so service install updates and re-installs do not touch
them.
--print for Ansible / Nix
Section titled “--print for Ansible / Nix”runwisp service install --print writes the rendered unit (or plist)
to stdout and exits 0. It does not touch the filesystem. Combine with
--binary, --config, --data, --port, --host to bake declared
values in:
sudo runwisp service install --print \ --binary /opt/runwisp/bin/runwisp \ --config /etc/runwisp/runwisp.toml \ --data /var/lib/runwisp \ --port 9477 --host 127.0.0.1 > /etc/systemd/system/runwisp.serviceAdd --local to render the per-user unit instead. --print never prompts and
never touches cron.
Retiring cron
Section titled “Retiring cron”sudo runwisp service install wires up the unit and nothing else. If cron is
still running jobs on this box, the install says so and points you at
sudo runwisp takeover — that’s the one command that retires cron, and it
works even on a box with no runwisp.toml yet.
service uninstall hands cron back, and service status grows a Cron: row
while the mask is in place. See
Take over from cron for the full
walkthrough: what happens in what order, the refusals, the first-run
prompt, how to undo it, and why macOS can’t do it at all.
--purge for full removal
Section titled “--purge for full removal”runwisp service uninstall --purge removes the data dir in addition
to the unit. It is the only place where --yes does not skip a
prompt: the operator must type the literal word delete to proceed.
That guard is permanent; nothing on the command line removes it.
Leave --purge off and uninstall is clean and reversible: it stops the
service, disables autostart, removes the managed unit, runs
daemon-reload, and leaves your data dir exactly where it was.
On WSL, the Linux-side service install runs unchanged — it writes
the systemd user unit and turns on linger. The Windows side then
needs a one-shot scheduled task that boots the WSL distro on login so
the systemd user instance comes up:
$Action = New-ScheduledTaskAction -Execute 'wsl.exe' -Argument '~ -d <distro> -- true'$Trigger = New-ScheduledTaskTrigger -AtLogOnRegister-ScheduledTask -TaskName 'RunWispBoot' -Action $Action -Trigger $Triggerrunwisp service install detects WSL (via WSL_DISTRO_NAME or the
microsoft token in /proc/sys/kernel/osrelease) and prints this
snippet at the end of a successful install with <distro> filled in.
macOS notes
Section titled “macOS notes”macOS installs are --local only — there’s no root LaunchDaemon yet, and
the bare runwisp service install says so rather than quietly installing
a LaunchAgent you didn’t ask for.
The LaunchAgent runs as the logged-in user, on every login. macOS has
no equivalent of loginctl enable-linger — the LaunchAgent is per-user
by design. Logs go to <data-dir>/daemon.log; service status
prints a tail -f hint to the same file.
Flag reference
Section titled “Flag reference”runwisp service install -y, --yes skip the install confirmation --print write the rendered unit to stdout and exit --dry-run print the plan, run the same checks, and exit without writing --force overwrite a hand-edited unit --local install a per-user unit instead of the system service --binary override the binary path baked into the unit
runwisp takeover -y, --yes take over without asking (for provisioning scripts) --dry-run print the plan and exit without writing --force overwrite a hand-edited unit --binary override the binary path baked into the unit --allow-skipped-cron-jobs take over even if a cron source failed to load
runwisp service uninstall -y, --yes skip the uninstall confirmation --purge also remove the data dir (typed 'delete' to confirm) --force remove a hand-edited unit --local act on the per-user unit
runwisp service status --local report on the per-user unit
runwisp stop / runwisp restart --local delegate to the per-user unit--local is only needed to disambiguate: without it, everything except
install finds whichever unit is installed, and install targets the
system service.