./xirp-remote # foreground, open, no auth
XIRP_REMOTE_KEY=<32+ hex chars> ./xirp-remote # foreground, key required
./xirp-remote install --key <32+ hex chars> # as a service, key requiredinstall bakes both into the LaunchAgent's EnvironmentVariables, so the plist is
the record of how the service is configured.
Turn them on per device in settings. Three parties have to agree — the browser grants permission, its push service issues a subscription, and this server keeps it and signs each message — so the setting reports which step failed rather than a generic "off".
The VAPID keypair is generated on first use and kept in
~/Library/Application Support/xirp-remote/push.json, mode 0600. It must persist:
a new keypair silently invalidates every existing subscription. Subscriptions that keep
failing are dropped after five attempts, and a 404 or 410 removes one immediately —
that is the push service saying the browser is gone.
The watcher asks the daemon nothing while no device is subscribed, so this costs nothing until you switch it on. On iOS the app has to be installed to the home screen before the browser will issue a subscription at all.
When a key is configured, endpoints take it as an X-Xirp-Key header or the
cookie from POST /api/auth. In open mode no credential is needed.
All four cost real debugging time and none is visible from the message catalogue:
agents:listreplies under the keyharnesses, notagents. Reading the obvious key returns an empty list with no error, so the create form silently offered no agents.- A model is not a top-level create field. The daemon derives a session's
requestedModelfromagent.options.model; passingmodelat the top level is silently ignored. Verified by creating a session onclaude-haiku-4-5and readingrequestedModelback. useTerminaldoes not mean "terminal instead of agent". Onsession:createit selects the tmux transport, which is the default; setting it tofalseselects Claude-only SDK mode. Passingtruehoping for a shell yields a full agent session — the pane came up running Claude Code. A standalone shell is a different message:project:create-terminal.- Session search answers from three sources, each marking its own completion.
metadataandmessagesreturn in ~4ms already flaggeddone: true, whilejsonlstreams a frame per matching transcript for seconds afterwards. Stopping at the firstdoneends the search before the source that finds anything has started — it returned 0 results in 4ms instead of 36 in 3s.CallStreaminstead waits until every source seen so far is done and the wire has been idle briefly, so a new upstream source cannot silently truncate results.
