-
Notifications
You must be signed in to change notification settings - Fork 0
daemon spec
Spec version: P1-E2-W4-S04-T02
Status: Planned
References: ADR-001, ADR-004, ADR-008
Scope: clawd host-local workstation agent (Rust/SQLite) lifecycle only. The clawde-intelligence Go/Postgres sidecar lifecycle is defined in E5.
| State | Description |
|---|---|
stopped |
Process is not running. PID file absent. All sockets closed. |
starting |
Process has launched. Generating workspace token, binding MCP socket, writing PID file. |
running |
All startup steps confirmed. Serving MCP tool calls. Health probe active. |
stopping |
SIGTERM received. Draining in-flight tool calls. Cleaning up. |
crashed |
Process exited with non-zero code OR health probe failed 2 consecutive times. Restart policy takes over. |
upgrading |
In-place binary upgrade. stopped sub-state with a version checkpoint written. |
stateDiagram-v2
[*] --> stopped
stopped --> starting : launch (launchd / systemd / NSSM / manual)
starting --> running : startup sequence complete (see §1.3)
starting --> crashed : startup failure (token write fail, socket bind fail, PID write fail)
running --> stopping : SIGTERM received
running --> crashed : exit code != 0 OR 2 consecutive health probe failures
stopping --> stopped : drain complete + PID removed (exit 0)
stopping --> crashed : drain timeout exceeded → force SIGKILL → exit non-zero
crashed --> starting : restart backoff elapsed (see §3)
crashed --> stopped : max restarts/hour ceiling reached (see §3)
stopped --> upgrading : `clawde upgrade` invoked
upgrading --> starting : binary replaced, version checkpoint written
upgrading --> stopped : upgrade failed, rollback applied
The ordering is strict. Each step must succeed before the next begins.
-
Generate workspace token — compute
HMAC-SHA256(workspace_id || pid || boot_time, $CLAWDE_SESSION_SECRET). Write to~/.claude/hooks/.clawde-token-<workspace>with mode 0600. Fail startup if write fails. Token TTL: 24 hours or until the daemon exits, whichever is sooner. Useclawde token rotateto revoke the active token and generate a new one (ADR-004). -
Bind MCP socket — open Unix domain socket at
/tmp/clawd.<workspace>.sock(Linux/macOS) or TCP127.0.0.1:7430(Windows fallback). Fail startup if bind fails (address in use = prior unclean exit; run stale-lock detection first). -
Write PID file — atomically write PID to
~/.local/share/clawde/daemon.pidusingO_CREAT|O_EXCL(see §2). Fail startup if write fails (another daemon instance is live). -
Report running state — emit structured log line
{ts, event="daemon_started", pid, workspace_id, mcp_socket, token_path}. Transition torunning.
| From | To | Trigger | Actions |
|---|---|---|---|
stopped |
starting |
OS launch agent / manual start | Fork process, begin startup sequence |
starting |
running |
All 4 startup steps pass | Enable health probe, accept MCP connections |
starting |
crashed |
Any startup step fails | Log error, exit non-zero |
running |
stopping |
SIGTERM | Stop accepting new MCP connections, begin drain |
running |
crashed |
Exit code != 0 OR 2 consecutive health failures | Log crash, hand off to restart policy |
stopping |
stopped |
Drain complete, PID removed, exit 0 | — |
stopping |
crashed |
Drain timeout (10s) exceeded | SIGKILL, exit non-zero, restart policy applies |
crashed |
starting |
Backoff timer elapsed, ceiling not hit | OS launch agent retries, or manual restart |
crashed |
stopped |
Restart ceiling hit (5/hour) | OS notification sent, no further auto-restart |
stopped |
upgrading |
clawde upgrade |
Write version checkpoint, replace binary |
upgrading |
starting |
Upgrade success | Normal startup sequence |
upgrading |
stopped |
Upgrade failure | Rollback applied, emit upgrade_failed log |
~/.local/share/clawde/daemon.pid
On macOS and Linux, ~ is $HOME. On Windows, use %LOCALAPPDATA%\clawde\daemon.pid.
On startup, before O_CREAT|O_EXCL write:
- Check if
daemon.pidexists. - If it exists: read the PID value.
- Send
kill -0 <pid>(zero signal — existence check only, no signal delivered).- If
kill -0returns 0 (process exists): anotherclawdis live. Abort startup with errorERR_DAEMON_ALREADY_RUNNING. - If
kill -0returnsESRCH(no such process): PID file is stale from unclean exit. Remove it and proceed.
- If
- On Windows: use
OpenProcess(PROCESS_QUERY_INFORMATION)instead ofkill -0. IfGetLastError()returnsERROR_INVALID_PARAMETER, the process does not exist; remove stale PID file.
The PID file write uses O_CREAT|O_EXCL (Rust: OpenOptions::new().create_new(true)):
- This is an atomic syscall. If two processes both pass the stale-lock check simultaneously (TOCTOU window), only one succeeds. The loser gets
EEXISTand must abort. -
Never use
File::create()for the PID file — it truncates and writes without atomicity guarantees.
The PID file is removed in the stopping → stopped transition (step 5 in §4, after log flush, before exit 0). If the process is SIGKILL'd, the PID file remains; stale-lock detection cleans it on next startup.
A crash is declared when either of the following occurs:
- The
clawdprocess exits with a non-zero exit code. - The health probe (§5) reports 2 consecutive failures.
| Restart attempt | Delay before retry |
|---|---|
| 1st restart | 5 seconds |
| 2nd restart | 10 seconds |
| 3rd restart and beyond | 30 seconds |
- Maximum restarts per hour: 5.
- Counter reset: After 1 hour of continuous clean uptime (no crash, no restart), the restart counter resets to 0.
-
Ceiling action: When the 5th restart within 1 hour fails (or the process crashes after the 5th restart within 1 hour), the auto-restart policy halts:
- Send OS notification: "ClawDE daemon has crashed repeatedly. Manual restart required." (use
osascripton macOS,notify-sendon Linux, Windows Toast on Windows). - Write audit log entry:
{ts, event="RESTART_LIMIT_REACHED", restart_count=5, window_seconds=3600}. - Transition to
stopped. No further automatic restart.
- Send OS notification: "ClawDE daemon has crashed repeatedly. Manual restart required." (use
Platform launch agents (§6) provide the primary restart loop. The ClawDE daemon itself does not self-fork. The backoff is implemented via ThrottleInterval (launchd) and RestartSec + StartLimitIntervalSec (systemd). NSSM on Windows has a native restart throttle.
Triggered by SIGTERM (Unix) or SERVICE_CONTROL_STOP (Windows SCM). Ordered steps:
- Stop accepting new MCP connections — close the accept loop on the MCP socket immediately. In-flight connections continue.
- Drain in-flight tool calls — wait up to 10 seconds for all active MCP tool calls to complete. Each call has its own deadline; the drain window is the outer deadline.
-
Force-kill on SIGKILL — if the 10-second drain window elapses without completion, the OS sends SIGKILL (launchd:
ExitTimeoutEnabled+ExitTimeoutSeconds=10; systemd:TimeoutStopSec=10; NSSM:AppStopMethodSkip+AppKillProcessTree). On SIGKILL, no cleanup is guaranteed; OS handles process table and file descriptor reclamation. - Close MCP socket — after drain, close and unlink the Unix socket file (or close the TCP listener).
-
Flush structured logs — write final shutdown audit row:
{ts, event="shutdown", pid, uptime_s, in_flight_drained}. Flush log buffer to disk. -
Remove PID file — unlink
~/.local/share/clawde/daemon.pid. - Exit 0 — clean exit. launchd/systemd/NSSM detect exit 0 and do not trigger restart policy.
ADR-008 cross-reference: The 2-second hook timeout (ADR-008 §HookContract rule 1) is subordinate to the graceful shutdown drain window. Hooks have their own 2-second deadline per invocation; the 10-second shutdown window is the aggregate drain for all in-flight MCP tool calls.
Default: 30 seconds.
Configurable via CLAWDE_HEALTH_INTERVAL_SECS env var (min: 10, max: 300).
| Probe | Pass condition |
|---|---|
| MCP socket bind | Unix socket or TCP port is still bound and accepting connections |
| SQLite WAL |
PRAGMA wal_checkpoint completes without error; no stuck WAL lock |
| Last tool call age | Last successful MCP tool call was within CLAWDE_TOOL_TIMEOUT_SECS (default 300) seconds. Passes if no tool calls have been received yet (fresh start). |
-
2 consecutive probe failures → health check declares process unhealthy → transition to
crashedstate. - A single failure followed by a pass resets the consecutive counter to 0.
On 2nd consecutive failure:
- Emit audit log:
{ts, event="health_check_failed", consecutive_failures=2, probe_results=[...]}. - Initiate graceful shutdown sequence (§4). If shutdown completes cleanly, transition
running → stopping → stopped. - Restart policy applies: if PID file is absent (clean stop), launch agent restarts with backoff.
- State machine transitions:
running → crashed → starting(per §1.4).
Agent plist path: ~/Library/LaunchAgents/io.nself.clawde.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>io.nself.clawde</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/clawde</string>
<string>daemon</string>
<string>start</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>5</integer>
<key>StandardOutPath</key>
<string>~/.local/share/clawde/logs/daemon.stdout.log</string>
<key>StandardErrorPath</key>
<string>~/.local/share/clawde/logs/daemon.stderr.log</string>
<key>ExitTimeoutEnabled</key>
<true/>
<key>ExitTimeoutSeconds</key>
<integer>15</integer>
<key>EnvironmentVariables</key>
<dict>
<key>CLAWDE_WORKSPACE</key>
<string>default</string>
</dict>
</dict>
</plist>Management commands:
# Install and start
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.nself.clawde.plist
# Stop
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/io.nself.clawde.plist
# Restart
launchctl kickstart -k gui/$(id -u)/io.nself.clawde
# Check status
launchctl print gui/$(id -u)/io.nself.clawdeNotes:
-
KeepAlive=truecauses launchd to restart on any exit (including exit 0). Usebootoutto fully stop. -
ThrottleInterval=5provides the 5-second minimum restart delay (maps to restart attempt 1 backoff). -
ExitTimeoutSeconds=15gives the daemon 15 seconds to drain before launchd sends SIGKILL (covers the 10-second drain window plus 5 seconds margin).
Unit file path: ~/.config/systemd/user/clawde.service
[Unit]
Description=ClawDE background daemon (clawd)
Documentation=https://github.com/nself-org/clawde/wiki/daemon-spec
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/clawde daemon start
ExecStop=/usr/local/bin/clawde daemon stop
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=3600
StartLimitBurst=5
TimeoutStopSec=15
StandardOutput=append:%h/.local/share/clawde/logs/daemon.stdout.log
StandardError=append:%h/.local/share/clawde/logs/daemon.stderr.log
Environment=CLAWDE_WORKSPACE=default
[Install]
WantedBy=default.targetManagement commands:
# Enable and start
systemctl --user enable clawde.service
systemctl --user start clawde.service
# Stop
systemctl --user stop clawde.service
# Disable (do not start at login)
systemctl --user disable clawde.service
# Check status
systemctl --user status clawde.service
# View logs
journalctl --user -u clawde.service -fNotes:
-
Restart=on-failurerestarts only on non-zero exit;Restart=alwayswould restart on exit 0 as well. -
StartLimitIntervalSec=3600+StartLimitBurst=5enforces the 5 restarts per hour ceiling at the OS level. -
TimeoutStopSec=15matches the macOS exit timeout.
NSSM (Non-Sucking Service Manager) configuration:
nssm install ClawDE "C:\Program Files\ClawDE\clawde.exe"
nssm set ClawDE AppParameters "daemon start"
nssm set ClawDE AppDirectory "C:\Program Files\ClawDE"
nssm set ClawDE AppEnvironmentExtra CLAWDE_WORKSPACE=default
nssm set ClawDE DisplayName "ClawDE Daemon"
nssm set ClawDE Description "ClawDE background daemon (clawd)"
nssm set ClawDE Start SERVICE_AUTO_START
nssm set ClawDE AppThrottle 5000
nssm set ClawDE AppExit Default Restart
nssm set ClawDE AppRestartDelay 5000
nssm set ClawDE AppStdout "%LOCALAPPDATA%\clawde\logs\daemon.stdout.log"
nssm set ClawDE AppStderr "%LOCALAPPDATA%\clawde\logs\daemon.stderr.log"
nssm set ClawDE AppRotateFiles 1
nssm set ClawDE AppRotateOnline 1
nssm set ClawDE AppRotateBytes 10485760Alternative — sc.exe (Windows Service Control):
# Create service
sc.exe create ClawDE binPath= "\"C:\Program Files\ClawDE\clawde.exe\" daemon start" start= auto
# Start
sc.exe start ClawDE
# Stop
sc.exe stop ClawDE
# Delete
sc.exe delete ClawDENotes:
- NSSM is preferred because it handles stdout/stderr log routing and restart throttling natively.
-
AppThrottle 5000sets a 5-second restart delay. -
sc.exealternative does not provide restart throttling; use Windows Task Scheduler or NSSM for production deployments. - On Windows,
~/.local/share/clawde/daemon.pidmaps to%LOCALAPPDATA%\clawde\daemon.pid.
~/.local/share/clawde/logs/daemon.log
On Windows: %LOCALAPPDATA%\clawde\logs\daemon.log
JSON one-object-per-line (JSONL), one structured object per log event. Fields:
| Field | Type | Required | Description |
|---|---|---|---|
ts |
ISO 8601 string | Yes | Event timestamp (2026-06-01T12:00:00.000Z) |
level |
string | Yes | Severity level (see §7.3) |
event |
string | Yes | Event type slug (see §7.4) |
pid |
integer | Yes | PID of the clawd process emitting the log |
workspace_id |
string | Yes | Active workspace identifier |
msg |
string | No | Human-readable description (supplemental to event) |
data |
object | No | Event-specific structured data |
error |
string | No | Error message if applicable |
Example log line:
{"ts":"2026-06-01T12:00:05.123Z","level":"info","event":"daemon_started","pid":12345,"workspace_id":"default","data":{"mcp_socket":"/tmp/clawd.default.sock","token_path":"~/.claude/hooks/.clawde-token-default"}}| Level | Description | Examples |
|---|---|---|
debug |
Verbose diagnostic information for developer debugging | Probe cycle results when healthy, socket accept/close details |
info |
Normal operational events | Daemon start/stop, tool call received/completed, health probe pass |
warn |
Abnormal but recoverable conditions | Single health probe failure, tool call exceeding 80% of drain window |
error |
Non-fatal errors requiring attention | Failed socket accept, SQLite checkpoint error |
critical |
Fatal conditions requiring immediate action | Startup failure, stale lock detected, restart ceiling reached |
The following events MUST be logged at info level or above and are considered audit-critical:
| Event slug | Level | When emitted |
|---|---|---|
daemon_started |
info |
Startup sequence completes (step 4 of §1.3) |
daemon_stopped |
info |
Graceful shutdown completes (step 7 of §4) |
daemon_crashed |
critical |
Process exits non-zero or health check declares crash |
tool_call_received |
info |
MCP tool call arrives at the daemon |
tool_call_completed |
info |
MCP tool call returns result to client |
tool_call_denied |
warn |
Tool call rejected by policy engine |
auth_token_generated |
info |
New workspace token written (PID, workspace, token_path, no token value) |
auth_token_rejected |
warn |
Incoming request presented invalid or expired token |
restart_initiated |
warn |
Restart policy triggers a restart attempt |
RESTART_LIMIT_REACHED |
critical |
5th restart within 1 hour ceiling hit, auto-restart halted |
health_check_failed |
warn |
Single health probe failure (consecutive_failures count included) |
upgrade_started |
info |
In-place binary upgrade begins |
upgrade_completed |
info |
Binary replacement and version checkpoint written |
upgrade_failed |
critical |
Upgrade failure, rollback applied |
| Parameter | Value |
|---|---|
| Max file size | 10 MB per file |
| Max rotated files | 5 (excluding current) |
| Compression | gzip on rotation (daemon.log.1.gz ... daemon.log.5.gz) |
| Retention | 30 days default (configurable via CLAWDE_LOG_RETENTION_DAYS) |
| Rotation trigger | Size-based only (no time-based rotation) |
On rotation:
- Current
daemon.logis renamed todaemon.log.1. -
daemon.log.1throughdaemon.log.4are each incremented by 1 (oldestdaemon.log.5.gzis deleted). - A new empty
daemon.logis created. - Prior rotation files are gzip-compressed asynchronously.
The clawde daemon status command queries the running daemon via the MCP socket and outputs the following fields.
| Field | Type | Description |
|---|---|---|
pid |
integer | PID of the running clawd process (0 if not running) |
uptime_s |
integer | Seconds the daemon has been in running state (-1 if not running) |
mcp_server_status |
string | Socket status: "bound", "unbound", "error:<message>"
|
active_tasks |
integer | Count of in-flight MCP tool calls currently being processed |
last_error |
string | null | Last error message emitted at error or critical level; null if no errors since start |
ClawDE daemon status:
pid: 12345
uptime_s: 3720
mcp_server_status: bound (/tmp/clawd.default.sock)
active_tasks: 2
last_error: null
JSON format (clawde daemon status --json):
{
"pid": 12345,
"uptime_s": 3720,
"mcp_server_status": "bound",
"active_tasks": 2,
"last_error": null
}When daemon is not running:
{
"pid": 0,
"uptime_s": -1,
"mcp_server_status": "unbound",
"active_tasks": 0,
"last_error": "Daemon not running — PID file absent"
}-
ADR-001 — ClawDE pivot: service boundary (
clawd= host agent, Rust/SQLite;clawde-intelligence= Go sidecar) -
ADR-004 — Local auth model: workspace token generated on
clawd start, written to~/.claude/hooks/.clawde-token-<workspace>mode 0600 before any hook runs - ADR-008 — Host adapter and hook contract: 2-second hook timeout (per-call), fail-open, audit log per invocation
- T01 daemon-spec: PID file location and socket path defined here are consistent with MCP server spec (T01) health-check endpoint
Spec source: P1-E2-W4-S04-T02. No source code implementation in this ticket — this is the design input for W14-T01 (OpenCode adapter) and W14-T02 (CC companion).
ClawDE · GitHub · MIT License
ClawDE
Getting started
Reference
- Architecture
- Daemon-Reference
- Folder-Structure
- Providers
- Multi-Account
- Security
- Features
- Features/Daemon
- Features/Session-Manager
- Features/Mode-System
- Features/Repo-Intelligence
- Features/Projects
- Features/Remote-Access
- Features/Desktop-App
- Features/Provider-Knowledge
- Features/Coding-Standards
- Roadmap
Branding
Contributing