Verbs

agentproto daemon

agentproto daemon install [--dry-run]   register service + start it (macOS launchd, Windows Task Scheduler)
agentproto daemon uninstall             stop + deregister service
agentproto daemon start                 start the service (idempotent; never kills healthy)
agentproto daemon restart               kill + relaunch
agentproto daemon stop                  stop the service
agentproto daemon status                installed? running? /health reachable?
agentproto daemon logs [--lines <N>]    tail daemon.log

Runs agentproto serve as a background service via the host's service manager. Today: macOS launchd and Windows Task Scheduler (per-user scheduled task at logon). Linux (systemctl --user) ships later; until then the verb prints a clear "not supported" message and points you at agentproto serve &; disown.

The daemon picks up its config from ~/.agentproto/config.json via daemon.* keys. Configure once, then daemon install captures the snapshot into the plist. Re-run install after any config change.

Logs (stdout + stderr) go to ~/.agentproto/daemon.log.

Platform notes

  • macOS: plist at ~/Library/LaunchAgents/sh.agentproto.plist. RunAtLoad=true, KeepAlive is crash-only (SuccessfulExit=false), ProcessType=Interactive (so it stays alive at user login and across login/logout). Crash-only means a clean exit-0 stays settled, while a crash respawns automatically. The plist's ProgramArguments is [node, cli.mjs, serve, …flags], computed from process.execPath + process.argv[1] of the install invocation — so the daemon runs on the same Node binary that ran daemon install (works correctly with nvm / fnm / Homebrew).
  • Linux: not yet supported. Fall back to agentproto serve &; disown or your own systemd --user unit pointing at agentproto serve.
  • Windows: agentproto daemon install registers a per-user scheduled task (schtasks /Create /TN agentproto-daemon /SC ONLOGON /F — no admin required) whose payload is a generated ~/.agentproto/agentproto-daemon.cmd launcher that captures the same node cli.mjs serve … argv snapshot the macOS plist does, and merges stdout+stderr into ~/.agentproto/daemon.log. install then starts the task once; start/restart/stop map to schtasks /Run, /End + /Run, and /End. status queries the task and probes /health with the same three-part shape as the macOS branch. The task runs at user logon, so the daemon survives closing the terminal window.

Subverbs

install

# Optional: configure first
agentproto config set daemon.workspace /Users/me/code
agentproto config set daemon.port 18791
agentproto config set daemon.allowedOrigins https://guilde.work
agentproto config set tunnel.host wss://guilde.work/api/v1/agentproto/tunnel
agentproto config set tunnel.autoconnect true
# Optional: end-to-end encrypt the tunnel (needs tunnel.token; the host
# must also support it, else the daemon falls back to plaintext).
agentproto config set tunnel.e2e true

agentproto daemon install

tunnel.e2e wraps the outbound tunnel in an AEAD box negotiated from the shared tunnel.token, so even the host can't read the frames. It is opt-in and negotiated — an older host that doesn't advertise E2E keeps working over the plaintext tunnel. See serve → End-to-end encryption.

Writes the plist, runs launchctl bootout on any prior version, then launchctl bootstrap gui/<uid>. Service starts immediately because of RunAtLoad.

--dry-run prints the would-be plist + the launchctl bootstrap command without touching the filesystem or launchd. Useful for verifying the captured flags before committing.

install also prints the exact node + CLI entry it captured, and whether that entry is the published npm install or a workspace build (a monorepo/dev checkout). When it is a workspace build the service would run that local folder, so it warns loudly with the npm fix:

agentproto daemon: service will run:
  node:    /Users/me/.nvm/versions/node/v22.22.0/bin/node  (nvm, global prefix /Users/me/.nvm/versions/node/v22.22.0)
  entry:   /code/agentproto/packages/cli/dist/cli.mjs
  source:  workspace build (LOCAL FOLDER — not the npm install)
  ! this is a workspace/dev checkout, not the npm-installed CLI — the service
    will run that local folder. To run the published CLI instead:
      npm i -g @agentproto/cli@latest   (then re-run: agentproto daemon install)

This is the field-observed failure mode: run agentproto setup from a monorepo checkout and the daemon is installed from the local dist/ instead of npm. agentproto doctor/agentproto setup flag the same thing in their preflight step.

uninstall

agentproto daemon uninstall

launchctl bootout + delete plist. Idempotent — "already absent" is not an error.

start / restart / stop

agentproto daemon start    # launchctl kickstart (idempotent; leaves a healthy daemon running)
agentproto daemon restart  # launchctl kickstart -k (kill + relaunch)
agentproto daemon stop     # launchctl kill SIGTERM

start is idempotent: it asks launchd to start the service if it isn't running and is a no-op if a healthy daemon already is. It never kills the incumbent. restart is the force-cycle: kickstart -k kills the running daemon (if any) and relaunches it — the clean replacement for manually killing the port. Crash-only KeepAlive means launchd only respawns the daemon when it exits non-zero; daemon stop sends a single SIGTERM and exits.

start/restart also self-heal the plist's EnvironmentVariables.PATH: each kickstart probes a login shell for the current PATH and rewrites the plist if it changed. This removes the need to re-run daemon install after installing new CLI tools (for example via uv tool install).

status

agentproto daemon status

Prints a multi-line summary:

agentproto daemon status
  plist:     installed (/Users/me/Library/LaunchAgents/sh.agentproto.plist)
  launchd:   loaded · pid=12345 · state=running
  /health:   ok · v0.20.0 (workspace abc1234, built 2026-09-11T…) · pid 12345 · up 2h  (http://127.0.0.1:18790)
  bin:       /Users/me/.local/share/fnm/node-versions/v22/bin/node /Users/me/.npm/lib/node_modules/@agentproto/cli/cli.mjs
  config:    /Users/me/.agentproto/config.json
  logs:      /Users/me/.agentproto/daemon.log

  recent logs:
    …last 5 lines of daemon.log…

The /health line now includes build identity when the running binary was built with one: source (workspace or published), short git sha, and a built ISO timestamp. This lets you distinguish a workspace distribution from a published tarball of the same version. bin shows the exact Node executable and script launchd (or the shell) invoked — useful when multiple Node managers are in play.

Exit code is 0 only when plist exists AND launchctl reports the service loaded. /health reachability is informational — it can be red briefly during a restart without changing the exit code.

logs

agentproto daemon logs              # last 10 lines
agentproto daemon logs --lines 100  # last 100 lines (short: -n 100)

Tails ~/.agentproto/daemon.log.

When to use the daemon vs serve directly

  • agentproto serve — run-as-foreground. Good for ad-hoc development, debugging the tunnel, the --interactive TUI, or short-lived test sessions where Ctrl-C should kill everything.
  • agentproto daemon — managed by the OS. Survives logout/login, restarts on crash, logs to a file. Use this when you want the tunnel "always up" for a hosted agent (Guilde, etc.).

Both share the same config.json so flipping between them is just which way you launched.