Skip to content

Autostart

Terminal window
sudo runwisp service install

That’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.

Terminal window
sudo runwisp service install # wire up systemd (or launchd, with --local)
runwisp service status # show autostart wiring and any drift
runwisp service uninstall # remove the unit (data dir is preserved)
runwisp service install --print > custom.service # Ansible / Nix

status, 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?”.

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.

Terminal window
runwisp restart # the way to apply runwisp.toml edits
runwisp stop # stops the daemon; the unit stays enabled for next boot

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

The system install (the default, Linux and WSL, needs root):

  1. Writes /etc/systemd/system/runwisp.service with the resolved binary path, config path, data dir, port, and host baked in. The file starts with a # Managed by runwisp service install marker so a later re-run can tell whether it can safely overwrite.
  2. systemctl daemon-reload.
  3. Offers to stop and mask cron, if cron is running and RunWisp is reading its crontabs. See Take over from cron.
  4. 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:

  1. 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.
  2. systemctl --user daemon-reload.
  3. sudo loginctl enable-linger <user> — only when linger isn’t already on. This is the single privileged step.
  4. systemctl --user enable --now runwisp-<fingerprint>.service.

--local on macOS:

  1. Writes ~/Library/LaunchAgents/com.runwisp.daemon.<fingerprint>.plist with the same managed-marker preamble. The LaunchAgent label matches (com.runwisp.daemon.<fingerprint>), so multiple instances coexist without overwriting each other.
  2. launchctl bootout (best effort, in case an old copy is loaded).
  3. launchctl bootstrap gui/$UID <path> then launchctl 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.

Three combinations can’t work, and each stops before anything is written:

  • runwisp service install without root. The system service needs root. Re-run with sudo, or pass --local if 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 install on macOS. There’s no root LaunchDaemon implementation yet, so --local is required.
  • sudo runwisp service install --local. systemctl --user has 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.

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 by EvalSymlinks. The installer rejects transient locations outright (/tmp/, /var/tmp/, /dev/shm/, the go-build cache used by go run). It warns on awkward-but-acceptable ones (~/go/bin, ~/.cache, paths with spaces) and suggests copying to ~/.local/bin/runwisp. Override with --binary for Ansible/Nix.
  • Data dir and config, system install: /var/lib/runwisp and /etc/runwisp/runwisp.toml — the same defaults runwisp daemon itself picks when it runs as root, so sudo runwisp reload later finds the same paths. An explicit --data / --config overrides either, and then goes through the same validation as below. --data and --config never change the scope — only --local does that.
  • Data dir, --local: an absolute --data is accepted silently. A relative --data is resolved to its absolute form and baked into the unit — a boot-launched unit has cwd=/, so the absolute path is what makes it work. That means runwisp 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/shm is still rejected because it won’t survive a reboot. The default ./.runwisp is 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/runwisp if 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.toml and ./runwisp.toml exist, 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.

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.

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

A --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 instance

Exit 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

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:

Terminal window
sudo mkdir -p /etc/systemd/system/runwisp.service.d
sudo tee /etc/systemd/system/runwisp.service.d/password.conf > /dev/null <<'EOF'
[Service]
Environment=RUNWISP_PASSWORD=…
EOF
sudo chmod 600 /etc/systemd/system/runwisp.service.d/password.conf
sudo systemctl daemon-reload
sudo systemctl restart runwisp.service

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

Terminal window
unit=runwisp-<fingerprint>.service
mkdir -p ~/.config/systemd/user/$unit.d
cat > ~/.config/systemd/user/$unit.d/password.conf <<'EOF'
[Service]
Environment=RUNWISP_PASSWORD=…
EOF
chmod 600 ~/.config/systemd/user/$unit.d/password.conf
systemctl --user daemon-reload
systemctl --user restart $unit

Drop-ins live next to the managed unit but outside the unit file itself, so service install updates and re-installs do not touch them.

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:

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

Add --local to render the per-user unit instead. --print never prompts and never touches 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.

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:

Terminal window
$Action = New-ScheduledTaskAction -Execute 'wsl.exe' -Argument '~ -d <distro> -- true'
$Trigger = New-ScheduledTaskTrigger -AtLogOn
Register-ScheduledTask -TaskName 'RunWispBoot' -Action $Action -Trigger $Trigger

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

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.