Reference

Security model

How Pilothouse protects remote approvals — loopback-only listener, Tailscale Serve, per-device P-256 signatures, single-use nonces, agent-caller checks — and the limits of each.

A remote “Allow” lets an agent do something on your machine. This page describes what stands between a request and that decision, and — just as important — what Pilothouse does not protect against.

Network exposure

  • The daemon’s web listener binds to 127.0.0.1:7517 only. Pilothouse never opens a port on another interface.
  • Remote access goes through Tailscale Serve, which terminates HTTPS on your tailnet address and proxies to loopback. Only devices on your tailnet can reach it.
  • Requests must carry an allowed Host (127.0.0.1, localhost, or your web.public_url), which defeats DNS-rebinding attacks from web pages in your browser.
  • Responses use a strict Content-Security-Policy (default-src 'self'), X-Frame-Options: DENY, Referrer-Policy: no-referrer, a restrictive Permissions-Policy, and HSTS over HTTPS.

Pairing and device keys

  1. pilothouse pair creates a one-time code, valid for 10 minutes, shown as a QR code.
  2. The phone’s browser generates an ECDSA P-256 key pair with a non-extractable private key (WebCrypto, stored in IndexedDB) and sends the public key with the code.
  3. The daemon registers the device and sets an HttpOnly, SameSite=Strict cookie (Secure over HTTPS).

Every state-changing request also needs a custom header and a same-origin Origin, so other sites can’t make your browser act on Pilothouse.

Signed decisions

A decision from a device is accepted only if it carries a valid signature from that device’s key over:

pilothouse-decide:v1
<request id>
<behavior>
<scope>
<timestamp>
<nonce>

The timestamp must be within 120 seconds of the daemon’s clock, and each nonce can be used once. A stolen cookie alone can’t approve anything, and a captured decision can’t be replayed or re-targeted to another request.

Revoke a device any time with pilothouse devices.

ntfy action buttons

Allow/Deny buttons in ntfy carry a random 32-byte token tied to one request, one behaviour and that request’s expiry. Tokens are single-use and stored only as SHA-256 hashes. Anyone who can read your ntfy topic can see the buttons, so use an unguessable topic or a private server.

Agents can’t approve themselves

An agent runs as you, with your shell. Pilothouse makes self-approval hard in three layers:

  1. Local decisions only on the Unix socket (mode 0600). The TCP listener never accepts an unsigned decision.
  2. Process-ancestry check. On the socket, deciding requests, pairing, creating login links, revoking devices, changing presence and reloading policy are refused with agent_caller if any ancestor of the calling process is a known agent process (claude, codex, …).
  3. Built-in rule pilothouse-self-protection forces ask when an agent runs Pilothouse admin commands or edits Pilothouse’s data directory or the agents’ own settings files.

The honest limits

This is defense in depth, not a sandbox. A process running as your user can, in principle, edit files, kill processes or talk to your browser. For example, a process that double-forks so it is re-parented away from the agent is no longer its descendant, so the ancestry check can’t see it; the self-protection rule still asks a person about any command that names pilothouse approve, but an obfuscated one could slip past both. Pilothouse stops an agent from casually approving itself or turning itself off; it can’t stop a determined program that already has your user’s privileges. If you need hard isolation, run agents in a container or VM, or as a different user — Pilothouse works there too.

Fail-open vs fail-closed

By default, if the daemon is unreachable the hook shim answers “no opinion” within two seconds and the agent behaves as if Pilothouse weren’t installed. That keeps a crashed daemon from breaking your work. Set approvals.fail_closed: true to deny PreToolUse calls instead while the daemon is down.

Data at rest

  • ~/.pilothouse is 0700; the database, keys and config are 0600.
  • Device tokens and action tokens are stored hashed (SHA-256) and compared in constant time.
  • Hook events are stored normalised; raw payloads are not kept. Closed requests and events are pruned after retention (default 30 days).
  • Logs record a one-line summary per tool call, not full tool input.

What leaves your machine

  • Notifications to the channels you configured.
  • The anonymous daily update check (version, OS, architecture, a random ID that rotates monthly) — off with update_check: false or PILOTHOUSE_NO_UPDATE_CHECK=1.

Nothing else. There is no telemetry and no Pilothouse account.

Reporting a vulnerability

Please report security issues privately — see pilothouse.dev/security.

Found a mistake? Open an issue.