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:7517only. 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 yourweb.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 restrictivePermissions-Policy, and HSTS over HTTPS.
Pairing and device keys
pilothouse paircreates a one-time code, valid for 10 minutes, shown as a QR code.- 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.
- The daemon registers the device and sets an
HttpOnly,SameSite=Strictcookie (Secureover 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:
- Local decisions only on the Unix socket (mode
0600). The TCP listener never accepts an unsigned decision. - Process-ancestry check. On the socket, deciding requests, pairing, creating login links, revoking devices, changing presence and reloading policy are refused with
agent_callerif any ancestor of the calling process is a known agent process (claude,codex, …). - Built-in rule
pilothouse-self-protectionforcesaskwhen 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
~/.pilothouseis0700; the database, keys and config are0600.- 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: falseorPILOTHOUSE_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.