Runner Daemon
The runner daemon lets you spawn agent sessions on demand — from the web UI or programmatically via the spawn_session tool — without needing a terminal open.
How It Works
Section titled “How It Works”┌──────────────────────────────────────────────┐│ PizzaPi Web UI / spawn_session tool │└─────────────────────┬────────────────────────┘ │ HTTP request to relay ▼┌──────────────────────────────────────────────┐│ Relay Server ││ → routes spawn request to registered runner │└─────────────────────┬────────────────────────┘ │ WebSocket command ▼┌──────────────────────────────────────────────┐│ Runner Supervisor (pizzapi runner) ││ → spawns daemon child process │└─────────────────────┬────────────────────────┘ │ fork ▼┌──────────────────────────────────────────────┐│ Runner Daemon (_daemon) ││ → spawns Worker for each session request │└─────────────────────┬────────────────────────┘ │ fork ▼┌──────────────────────────────────────────────┐│ Session Worker (_worker) ││ → runs pizzapi session, streams events │└──────────────────────────────────────────────┘The supervisor manages crash recovery — if the daemon dies, the supervisor restarts it. Each session runs in an isolated worker process so a crash never affects other sessions.
Starting the Runner
Section titled “Starting the Runner”# Start in the foreground (Ctrl+C to stop)pizzapi runner
# Start in the backgroundpizzapi runner &
# Stop the runnerpizzapi runner stopRunner Identity
Section titled “Runner Identity”On first start, the runner acquires a persistent identity and process lock and saves them to ~/.pizzapi/runner.json. Treat this file as a credential — it contains the long-lived runnerSecret used to re-authenticate the runner with the relay.
// ~/.pizzapi/runner.json (auto-generated, do not edit){ "pid": 12345, "supervisorPid": 12344, "startedAt": "2025-01-15T10:30:00.000Z", "runnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "runnerSecret": "64-char-hex-secret"}runnerId— stable UUID that identifies this runner to the relay.runnerSecret— 32-byte hex secret used to re-authenticate. Do not commit or share this file.pid/supervisorPid/startedAt— process lock fields that detect and prevent duplicate runners.
The relay durably records which account first registered each runnerId. That
account keeps the identity even while the runner is offline: registering the same
runnerId with an API key from a different account — over the runner socket or
the tunnel — is rejected, and registration also fails closed if the relay cannot
read its ownership record. Only the runner socket registration (which proves the
runnerSecret) can record a new identity: a tunnel connection for a runnerId the
relay has never seen registered is refused, and the runner opens its tunnel only after
its runner registration succeeds. To move a machine to another account, stop the runner
and delete runnerId and runnerSecret from ~/.pizzapi/runner.json so it
registers a fresh identity.
The runner appears in the web UI under its display name (from PIZZAPI_RUNNER_NAME or the hostname) and is targeted by spawn_session calls using runnerId.
Spawning Sessions Programmatically
Section titled “Spawning Sessions Programmatically”Agents can spawn sub-sessions using the built-in spawn_session tool:
// Inside a pi agent session:const { sessionId, shareUrl } = await spawn_session({ prompt: "Fix all TypeScript errors in packages/server/src/", cwd: "/path/to/project", runnerId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // optional — defaults to current runner model: { provider: "anthropic", id: "claude-sonnet-4-20250514" }, // optional});The sub-session streams to the same relay and appears as a child in the web UI session tree.
Process Hierarchy
Section titled “Process Hierarchy”pizzapi runner ← supervisor (PID tracked in ~/.pizzapi/runner.json) └── pizzapi _daemon ← daemon (restarted on crash by supervisor) ├── pizzapi _worker ← session A ├── pizzapi _worker ← session B └── pizzapi _worker ← session CRunning as a System Service
Section titled “Running as a System Service”systemd (Linux)
Section titled “systemd (Linux)”The installer generates the user unit and can activate it for you:
pizzapi runner install --activateTo preview without writing:
pizzapi runner install --dry-runThe generated unit is equivalent to:
[Unit]Description=PizzaPi RunnerAfter=network-online.target
[Service]Type=simpleExecStart=/usr/local/bin/pizzapi runnerRestart=on-failureRestartSec=5WorkingDirectory=/path/to/project
[Install]WantedBy=default.targetsystemctl --user daemon-reloadsystemctl --user enable --now pizzapi-runner.servicesystemctl --user status pizzapi-runner.servicemacOS (launchd)
Section titled “macOS (launchd)”The generated LaunchAgent includes the install-time PATH and working directory:
<key>EnvironmentVariables</key><dict> <key>PATH</key><string>/your/install-time-shell-path</string></dict><key>WorkingDirectory</key><string>/path/to/project</string><key>RunAtLoad</key><true/><key>KeepAlive</key><dict> <key>SuccessfulExit</key><false/> <key>Crashed</key><true/></dict>Use a LaunchAgent (~/Library/LaunchAgents/), not a LaunchDaemon: it runs in your login session and can access your keychain and SSH credentials. Manage it with launchctl for your user; see Apple’s launchd documentation.
pm2 start "pizzapi runner" --name pizzapi-runnerpm2 savepm2 startup # generate startup scriptDocker
Section titled “Docker”To run the runner as a container instead of a host process, see the dedicated Runner Container guide — it covers the Compose service, the persistent data volume, workspace mounts, UID/GID remapping, and sandbox posture.
Stopping the Runner
Section titled “Stopping the Runner”pizzapi runner stopThis requests a graceful supervisor shutdown, then force-kills if it does not exit within its timeout. The daemon disconnects and releases its lock; session workers receive shutdown handling from the daemon. To restart the runner, stop and start it with pizzapi runner stop followed by pizzapi runner (or restart its service/container). A daemon restart can re-adopt eligible live sessions; it is not equivalent to ending every session.
Checking Status
Section titled “Checking Status”pizzapi runner status # human-readablepizzapi runner status --json # machine-readableExits 0 if the process is alive and currently registered with the relay, 1 otherwise (dead process, alive but disconnected, waiting on pairing approval, or rejected by the relay — reason distinguishes these). This is what the runner container image’s Docker HEALTHCHECK runs. See the CLI Reference for the JSON field reference, and Runner Container → Credentials for what auto-pairing does when no PIZZAPI_API_KEY is set.
Re-pairing On Demand
Section titled “Re-pairing On Demand”pizzapi runner pair # refuses if a credential already resolvespizzapi runner pair --force # pairs and overwrites any existing credentialRuns the same headless device-claim flow the daemon runs automatically on a credential-less boot, but on demand — useful after revoking a runner’s API key in the web UI. Refuses without --force if a credential already resolves (env var or config.json), and refuses (or, with --force, warns loudly) if PIZZAPI_RUNNER_API_KEY / PIZZAPI_API_KEY / PIZZAPI_API_TOKEN is set, since that would keep shadowing the freshly paired key at runtime. If a daemon is already running, pairing still writes the new credential but the daemon keeps using the old one in memory until you restart it — the command tells you so. See the CLI Reference for the full guard-rail and exit-code reference.
Session Linking & Triggers
Section titled “Session Linking & Triggers”When sessions are spawned via spawn_session, the runner daemon automatically links them as parent-child. This enables the conversation trigger system. See the Subagents and linked sessions guide for the full list of trigger types, response actions, and delivery semantics.