Service & Daemon
This page is the operations-side companion to Setup → Service management; that page covers installing and uninstalling the service. This page covers running it: tuning, resource limits, graceful restarts, and multi-workspace setups.
Choosing between user and system scope
| Scope | Good for | Downside |
|---|---|---|
| User | Laptop, single-user dev box, simple deployments | Only runs when the user is logged in (Linux with a desktop, macOS) unless you enable lingering |
| System | Headless servers, SBCs, VPSes, multi-user hosts | Needs root to install; gets its own user account |
On desktop Linux, enable user-service lingering so the user service persists across logouts:
sh
loginctl enable-linger $USER
Without lingering, a user-scope systemd service stops when the last session closes.
Restart behaviour
The installed systemd user unit (~/.config/systemd/user/zeroclaw.service) uses:
Restart=always
RestartSec=3
systemd restarts the daemon on any exit with a 3-second backoff. There is no exit-code allowlist, so a daemon that fails fast on a bad config will flap; fix the config and systemctl --user restart zeroclaw rather than relying on the service to give up.
On macOS, the LaunchAgent (~/Library/LaunchAgents/com.zeroclaw.daemon.plist) sets RunAtLoad and KeepAlive to true, so launchd keeps the daemon running and relaunches it whenever it exits.
On Windows, zeroclaw service install registers a Task Scheduler task triggered ONLOGON at the LIMITED run level. It starts the daemon at logon; it does not add an automatic restart-on-failure policy.
Graceful shutdown
On Unix the daemon traps SIGINT and SIGTERM; on Windows it traps Ctrl+C (ctrl_c). Any of these triggers a clean shutdown: the daemon stops its channel server and the gateway listener and exits.
SIGHUP is ignored (the daemon stays running). A reload requested via the /admin/reload endpoint restarts the daemon loop in place rather than exiting.
An in-place reload starts a replacement generation over the same durable sessions, so it waits for the RPC connections the retiring generation accepted to finish, including any agent turn still unwinding after a forced teardown. If that wait runs out of budget, the reload is refused and the daemon shuts down instead. The refusal is deliberate: exiting is what keeps a replacement generation from overlapping work that the old one never retired.
How the daemon comes back after a refusal depends on what was running it:
- Under the installed systemd unit (
Restart=always) or launchd LaunchAgent (KeepAlive), the service manager starts a fresh daemon process, as described under Restart behaviour. - Under the Windows
ONLOGONtask, which has no restart-on-failure policy, the daemon stays down until you restart it manually or the next logon. - When ZeroCode started the daemon itself, which it does by spawning an ephemeral daemon whenever it finds no local endpoint, ZeroCode owns that child and replaces it itself instead of waiting on a service manager. It makes a single automatic respawn attempt per disconnect, and flags the daemon as needing attention if that attempt does not come back.
Conversation memory and session state are written to SQLite incrementally during operation, not buffered until shutdown, so a clean stop does not depend on a flush step. Tool receipts are in-band HMAC tokens in the conversation, not a separate on-disk log. A hard SIGKILL skips the clean channel teardown but does not corrupt already-committed memory; only an agent turn that was mid-write is lost.
Manual start for debugging
Skip the service and run the daemon directly:
sh
zeroclaw service stop # free the gateway port if the service is running
zeroclaw daemon
zeroclaw daemon runs in the foreground, logs to stderr, and is the same process the service runs, just without the service harness. Useful when:
- Diagnosing startup failures that the service swallows
- Running under
gdb/lldb - Testing a config change before committing to it
Terminate with Ctrl-C, same graceful shutdown semantics as SIGTERM.
Resource limits
Linux: systemd
Add to a drop-in:
sh
systemctl --user edit zeroclaw.service
[Service]
MemoryMax=2G
CPUQuota=200% # two cores
LimitNOFILE=16384 # if opening many channel sockets
Reload and restart:
sh
systemctl --user daemon-reload
systemctl --user restart zeroclaw
macOS: launchd
Edit ~/Library/LaunchAgents/com.zeroclaw.daemon.plist:
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>16384</integer>
</dict>
Unload + load the plist to apply:
sh
launchctl unload ~/Library/LaunchAgents/com.zeroclaw.daemon.plist
launchctl load ~/Library/LaunchAgents/com.zeroclaw.daemon.plist
Docker
Compose:
services:
zeroclaw:
image: ghcr.io/zeroclaw-labs/zeroclaw:latest
mem_limit: 2g
cpus: 2.0
ulimits:
nofile: 16384
Running multiple workspaces
Each ZeroClaw daemon owns one config directory (which contains its data/ dir). To run two side by side, give each its own config directory via --config-dir (or the ZEROCLAW_CONFIG_DIR env var):
sh
zeroclaw --config-dir ~/.zeroclaw-home daemon
zeroclaw --config-dir ~/.zeroclaw-work daemon
Each instance reads its own config, its own data/ (memory, sessions), its own gateway port (set per config), and its own channel bindings. Memory stays separate; a Telegram bot in one config dir doesn’t know about the other.
zeroclaw service install always installs a single unit pointed at the default config directory; it has no flag to name or parameterize instances. To run more than one as a persistent service, hand-author a second unit file (copy ~/.config/systemd/user/zeroclaw.service to a new name) whose ExecStart passes --config-dir <dir>, then enable it separately.
Don’t point two daemons at the same config directory. SQLite is single-writer; the second will fail on startup.
Observing restarts and crashes
sh
# Linux
journalctl --user -u zeroclaw --since "1 day ago" | grep -E 'Started|Stopped|failed'
# macOS
log show --predicate 'process == "zeroclaw"' --last 1d | grep -E 'start|stop|error'
If you’re seeing repeated restarts, enable debug logging (RUST_LOG=debug via the unit file’s Environment=) and let one more crash happen to capture the full trace.