- See
docs/specs/glossary.mdfor Session, baseboard, and remote-role vocabulary.- This is an assistant-run setup playbook. Start a fresh Claude instance in this repository and say:
read @SELF_HOST.md and walk me through it.- It is also the spec for
deploy/local/— the Installer contract at the end is the maintainer half, andscripts/spec-lint.mjschecks this file with the specs.
Installs the Dormouse coordinating Relay on the user's own laptop — or, to outlive its sleep, on an always-on tailnet box ("Keeping the relay up while the laptop sleeps") — reachable only from their tailnet at https://<laptop>.<tailnet>.ts.net. One idempotent installer per platform:
| OS | Installer | Service | Install root |
|---|---|---|---|
| macOS | deploy/local/install-macos.sh |
LaunchAgent sh.dormouse.relay |
~/Library/Application Support/Dormouse Relay |
| Windows | deploy/local/install-windows.ps1 |
Scheduled Task \Dormouse Relay |
%LOCALAPPDATA%\Dormouse Relay |
| Linux | deploy/local/install-linux.sh |
systemd user unit dormouse-relay.service |
~/.local/share/dormouse-relay |
Pick the column that applies before the first command and stay on it — mixing them is the main way this runbook goes wrong. Each checkpoint that differs gives all three forms; the Mechanism map has the rest.
This runbook covers running the installer and finishing what it cannot — the passkey, the Burrow build (Standalone or VS Code), the backup — with no code for anyone to write or edit.
Guide the user one checkpoint at a time: do the command-line work you safely can, pause for browser consent flows, secrets, and approval of external or destructive changes, and never dump the whole runbook back at the user.
Run the installer; never reimplement it or paper over it with hand-run launchctl, schtasks, systemctl --user or tailscale serve. Wrong behavior is a bug in that platform's installer: say so plainly and offer to fix it as an ordinary reviewed code change, a separate task. Its contract is the Installer contract here; a change to one is a change to both.
Before acting:
- Read
docs/specs/relay.md("Configuration", "Relay origin"),docs/specs/remote-security-model.mdfor the trust model, and the Installer contract. - Establish the OS and pick the installer column; run its
--help/-Help, skim the script, and quote its errors rather than paraphrasing — they are written for whoever is standing here. - An install root that already exists means an update or a repair: read
manage statusbefore changing anything. - Recheck the linked documentation — dashboards and CLI syntax change.
- Explain the checkpoint, carry it out, verify it, then move on.
- Never ask the user to paste the setup password or any other bearer credential into chat.
manage show-passwordprints it in their terminal. - Never commit, push, merge, or delete installed state without first showing the exact change and obtaining approval.
- For a relay that outlives this laptop's sleep, read "Keeping the relay up while the laptop sleeps" with the user rather than improvising cloud infrastructure.
Keep a worksheet, filled in as values become known: laptop OS; its Tailscale DNS name (from tailscale status --json); external origin (https://<laptop-name>.<tailnet-dns-suffix>); install root (the installer prints the exact path, honoring $XDG_DATA_HOME on Linux) and its state/; service; loopback port 3100; lingering, on Linux only; and the installed release, which the installer and manage status both print.
-
A tailnet with MagicDNS and HTTPS certificates enabled, Tailscale running on this laptop and on the phone that will run Pocket. Whether the HTTPS origin stays private is a deployment choice, not a security premise (
docs/specs/security-remote.md→ "Network posture (self-hosted)"). -
macOS, Windows or Linux. Each installer refuses the others. On a fourth OS, or Linux without systemd, design the native service manager with the user rather than translating LaunchAgent, Scheduled Task or unit-file commands blindly.
-
An ordinary terminal. Every installer refuses to run privileged — root on macOS and Linux, elevated on Windows — because the one account owning
config/andstate/is the whole credential posture (docs/specs/security-remote.md→ "Network posture (self-hosted)"). -
On Linux, this account must be allowed to operate
tailscaled. Preflight checks before the build and prints the fix, but never runssudo; this is the only step of a Linux install needing root:sudo tailscale set --operator=$USER
-
On Linux, decide the availability shape before installing. The default is per-login like macOS and Windows: up from login to logout. A machine reached over SSH, or serving with nobody logged in, needs
--linger. Switching later isloginctl enable-linger $USER/disable-linger, not a reinstall. -
On Windows, one signed-in user at a time owns Tailscale. A second signed-in profile fails every
tailscalecall with401 Unauthorized: Tailscale already in use by <user>, and elevating does not bypass it; that user must sign out or quit the tray app (quserlists the sessions). Preflight detects it and names the account. -
A Burrow built for this Relay's origin. The shipped standalone and VS Code Burrows reach only Dormouse Hosted, so a self-host Relay needs a local build of whichever Burrow the user runs, its
DORMOUSE_RELAY_ORIGINbyte for byte theDORMOUSE_ORIGINthe installer writes toconfig/relay.env:DORMOUSE_RELAY_ORIGIN=https://<laptop>.<tailnet>.ts.net pnpm dogfood:standalone DORMOUSE_RELAY_ORIGIN=https://<laptop>.<tailnet>.ts.net pnpm dogfood:vscode
That self-host build has no one-time connection, no managed voice, and no auto-update — update it by rebuilding (
docs/specs/relay.md→ "Relay origin").
It builds the exact current checkout into a self-contained release, registers a per-login user agent restarted on exit (Mechanism map) running the Relay on 127.0.0.1:3100, and points tailscale serve --bg at it to terminate private HTTPS — all under the current user's profile:
<install root>/
bin/
run-relay (run-relay.ps1 on Windows)
manage (manage.ps1 + manage.cmd on Windows)
config/
relay.env
current -> releases/<release-id> (current.txt naming it, on Windows)
previous -> releases/<release-id> (previous.txt, on Windows)
releases/
<release-id>/
runtime/node (runtime\node.exe on Windows)
relay/
lib/dist-pocket/
RELEASE
run/
enroll-offer.json
relay.json
state/
account.json
burrows.json
push-subscriptions.json
setup-password.json
vapid.json
Logs: ~/Library/Logs/Dormouse Relay/ on macOS, <install root>\logs on Windows, ~/.local/state/dormouse-relay/logs on Linux. Service definition: ~/Library/LaunchAgents/sh.dormouse.relay.plist, the Scheduled Task \Dormouse Relay, or ~/.config/systemd/user/dormouse-relay.service.
Until the first Burrow enrolls, run/enroll-offer.json lets a Dormouse Burrow on this machine enroll in one click without the setup password (checkpoint 4, step 2); its lifetime is docs/specs/relay.md → "Configuration".
No installer will ever: run git pull, fetch, or switch branches; install a scheduled updater; install or re-authenticate Tailscale; rewrite an origin that no longer matches the node's DNS name; or touch config/ and state/, which survive every update, prune, and uninstall.
An update is a short restart: Burrow and Pocket WebSockets disconnect and reconnect (Invariants).
manage verify checks all of these locally and exits nonzero on any failure:
- The service is registered and running, declares the run-at-load and restart-on-exit of the Mechanism map, and carries no credential — a definition it cannot read at all fails rather than passes, and
verifysearches it and therun-relaywrapper for every credential name the installer knows. Plus what only the live system shows: macOS, loaded ingui/$UIDwith a plist that lints; Windows, taskRunning, no execution time limit, restarts on failure, unelevated, unstopped by battery or idle,bin\run-relay.ps1still carrying the supervision loop; Linux, unit known to the user manager,enabled, passingsystemd-analyze --user verify. - Loopback
/api/helloresponds, the Pocket app is served, and the process holding the port belongs to the current release (Invariants → "A 200 does not say who answered"); Linux additionally requiressystemctl --user is-active. - Port 3100 is bound only to
127.0.0.1, and the plaintext port is unreachable on the laptop's Tailscale IP. tailscale serveproxies/to127.0.0.1:3100on the:443listener of the origin recorded inconfig/relay.env. A failure prints themanage servecommand that re-applies it.config/,state/,run/,config/relay.envand an unspent offer are owner-only (docs/specs/security-remote.md→ "Credentials at rest"); a spent offer is gone, andverifysays so rather than failing.- The current release pointer resolves to a release with
RELEASEmetadata, and neither the service definition nor therun-relaywrapper refers to the source checkout. An absent previous-release pointer warns (a first install); one naming the same release ascurrent, or a release no longer on disk, fails.
What the laptop cannot prove alone — reachability from another device, a real restart, an update and rollback, a phone session, an off-laptop backup — is checkpoints 3–6.
The installer preflights and stops with a specific error, so do not re-run its checks by hand: OS and unprivileged session; the Tailscale CLI, backend state, MagicDNS name, HTTPS certificates, and (Windows) local-API owner or (Linux) operator role; an origin disagreeing with an existing installation; the Git SHA and dirty status; the Node and pnpm versions pinned in root package.json; and on Linux a reachable systemd user manager, version 240 or newer.
Establish with the user what the script cannot:
-
This checkout is the one they want installed. Show
git status --short, the branch, and the SHA. Never pull or switch branches on their behalf; the installer installs exactly what is checked out. -
Their phone runs Tailscale and is signed in to the same tailnet.
-
Port 3100 is free. Unchecked before installation; a stale listener blocks the new Relay from binding and fails the post-install identity check. A dev Relay is not normally the culprit:
pnpm dev:relaytakes any free port unlessPORTnames one.# macOS lsof -nP -iTCP:3100 -sTCP:LISTEN# Windows Get-NetTCPConnection -State Listen -LocalPort 3100 -ErrorAction SilentlyContinue
# Linux ss -lntp 'sport = :3100'
With the user's approval:
# macOS
./deploy/local/install-macos.sh# Windows, from an ordinary (not elevated) PowerShell
.\deploy\local\install-windows.ps1# Linux, as the ordinary user who will own the install (no sudo).
# Add --linger only if the service must outlive logout.
./deploy/local/install-linux.shOn a machine with a pre-rename install, the installer removes the retired sh.dormouse.server LaunchAgent / dormouse-server.service unit, since both bind the same port, but leaves the old install root and logs: say so, and let the user delete them.
Read its printed steps with the user rather than summarizing. Its confirmations — a dirty worktree, a mismatched pnpm, repointing an already-claimed Serve root path — are the user's decisions, and it refuses to assume an answer with no terminal. Tailscale may open a browser consent flow the first time Serve requests a certificate; that one is the user's to click. A first install ends by pointing at manage show-password; do not run that yet.
# macOS
"$HOME/Library/Application Support/Dormouse Relay/bin/manage" verify# Windows
& "$env:LOCALAPPDATA\Dormouse Relay\bin\manage.cmd" verify# Linux — the installer prints the exact path; this is the default when
# XDG_DATA_HOME is unset.
"$HOME/.local/share/dormouse-relay/bin/manage" verifyExpect every check to pass and the command to exit 0. manage status gives the same picture without the pass/fail framing.
Then, from another tailnet-connected device: request https://<laptop>.<tailnet>.ts.net/api/hello, open the Pocket application at the same origin. If private HTTPS is intended, temporarily leave Tailscale on that device and confirm the origin becomes unreachable.
Kill the Relay process once and confirm the service manager restarts it:
# macOS — launchd restarts within a second or two
pkill -f 'Dormouse Relay/current/relay/dist/index.js'
"$HOME/Library/Application Support/Dormouse Relay/bin/manage" status# Windows — select by install-root path and command line, never by image name:
# other node.exe processes on this machine are not the Relay. The supervision
# loop restarts after a 10s throttle, so wait ~15s before reading status.
$root = "$env:LOCALAPPDATA\Dormouse Relay"
Get-CimInstance Win32_Process |
Where-Object { $_.ExecutablePath -like "$root\*" -or $_.CommandLine -like "*$root*" } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
& "$root\bin\manage.cmd" status# Linux — Restart=always with RestartSec=10, so wait ~15s before reading status.
systemctl --user kill --signal=SIGKILL dormouse-relay.service
"$HOME/.local/share/dormouse-relay/bin/manage" statusOn Linux, also prove the availability shape you chose. Without --linger: log out fully, confirm the service is gone (loginctl shows no session and the origin stops answering), then log back in and confirm it returns on its own. With --linger: it keeps answering across a logout, and loginctl show-user $USER -p Linger reports yes.
Restart the laptop only with the user's approval; otherwise say plainly that the run-at-load trigger and registered service were verified but the reboot test skipped. After a real login or reboot, confirm the process and the background Serve mapping both return without rerunning the installer.
The Relay has no account, no passkey, and no enrolled Burrow. Same sequence as docs/specs/relay.md → "Running it", run against the tailnet origin, with the Relay's generated password. The Burrow comes first: a passkey is registered only off a code an enrolled Burrow displays (docs/specs/relay.md → Setup tokens and the pairing QR).
-
The setup password. Needed only if the step-2 offer card is gone or the Burrow is elsewhere: have the user run
manage show-passwordin their own terminal, which warns before printing. Never ask for the value, and never print it into the conversation. -
The Burrow. On this same machine, launch the build made with
DORMOUSE_RELAY_ORIGIN(Prerequisites), open Settings → Network (the baseboard's Settings button), and choose Anywhere, through your Relay's host: a new install's default, Nowhere, refuses enrollment (docs/specs/remote-network.md→ "Policy"). While the offer is unspent, its card enrolls in one click; "Enroll with the setup password…" covers a spent offer or a Burrow on another machine (docs/specs/relay.md→ "Remote control, in the Settings dialog"). Enrollment persists, so later launches connect on their own; the section then shows the Relay and its connection.A Burrow that offers only "Enroll with hosted.dormouse.sh" is a stock build, not a Relay problem.
-
The phone, and only then the code. On the phone, open
https://<laptop>.<tailnet>.ts.netin Safari and confirm it leads with Scan a setup code. For push, add Pocket to the Home Screen and pair inside the installed app (docs/specs/pocket-app.md→ Installable web app). A setup code is live for five minutes, so that first load — bundle, service worker, Home Screen install — must not happen inside the window. With the phone waiting on that screen, press Set up a phone in Settings → Network; scanning or pasting the code creates the passkey and signs them in, bound to this exact origin, with no password typed on the phone. -
A real session. The scan runs straight into pairing: read the two digits off the phone, type them into the modal on the laptop, and approve — the last thing anyone does. The phone answers its own biometric prompt and lands on the machine's terminal. Only now have HTTPS proxying, the WebSocket upgrade, and the security flow been exercised together.
-
State. Confirm
account.json,burrows.jsonandvapid.json— pluspush-subscriptions.jsonif push was enabled — now exist instate/. Record ownership and checksums without printing contents; checkpoint 5 checks them against a reinstall.
Updating is choosing a checkout and rerunning the same command:
git -C <checkout> log --oneline -1 # decide deliberately what to install
./deploy/local/install-macos.sh # or .\deploy\local\install-windows.ps1
# or ./deploy/local/install-linux.shProve it once, while the user is watching:
- Rerun the installer from the same or a newer checkout.
- Confirm the release changed as expected and that the
state/checksums from checkpoint 4 andconfig/relay.envare unchanged. - Run
manage rollback, confirm the previous release comes back healthy, then return to the desired release.
manage uninstall removes the service definition, installed code and run/, keeps config and state and reports where they are, and keeps manage itself. manage purge is the separate, irreversible deletion behind a typed confirmation phrase; run it after uninstall, and it prints the one command that clears whatever is left.
Make these explicit: the relay is down while the laptop sleeps, is shut down, has Tailscale disconnected, or is logged out; the installer does not follow main, so updates happen only when the user reruns it; the HTTPS origin is tied to the laptop's Tailscale node name, so renaming or re-enrolling that node means redoing the passkey and every Burrow enrollment; and Tailscale network policy still controls which tailnet members reach the laptop — review existing grants if the tailnet has other users.
Confirm the install root, especially config and state, is covered by an encrypted backup off the laptop — Time Machine, File History, Déjà Dup/restic/borg. Check the coverage rather than assuming it: %LOCALAPPDATA% is excluded from File History's default library set and from OneDrive's Known Folder Move, and ~/.local/share from dotfile-oriented backup rules, so on both the install root is very likely unprotected until added explicitly. A second directory on the same disk is not a backup; these files hold Burrow bearer credentials and a VAPID private key. Rehearse a small restore without overwriting live state.
Report concisely: the Pocket URL and its WebAuthn-origin significance; the exact installed Git SHA and whether the build was dirty; where runtime config, state, release metadata and logs live; the rollback command; backup status and restore location; any skipped acceptance test or remaining manual Burrow/Pocket setup; the update and availability limits of checkpoint 6; and the installed manage status, manage verify, manage logs and manage restart commands.
Never print the setup password or any credential in the handoff.
- Dormouse Relay runtime and state contract:
docs/specs/relay.md - Dormouse trust model:
docs/specs/remote-security-model.md - Burrow installations:
docs/specs/standalone.md,docs/specs/vscode.md - Install Tailscale on macOS
- Tailscale variants on macOS
- Install Tailscale on Windows
- Manage scripts with launchd
- Windows Task Scheduler
- ScheduledTasks PowerShell module
- Tailscale Serve
For pairing-storage failures on iOS, Android, or desktop, open https://<relay-origin>/diagnostics/index.html in the affected browser and choose Run checks, then Copy results; no setup code is needed. For persistence across app or phone restarts, follow the page's restart test, and remove its test data afterward in each context where you prepared one. Inspect a report before sharing, since it includes browser/version information; each result is evidence for that one browser or installed app only. The diagnostic contract is docs/specs/pocket-app.md -> "The capability harness".
None of the three service managers runs the user's interactive shell or PowerShell startup files, so a PATH that works in a terminal proves nothing about any of them.
- The service works only while the source checkout exists: an installer bug — the release must be self-contained — not a reason to keep the checkout around.
manage verifychecks it directly. - The service loops or will not start: macOS —
plutil -lintthe plist,launchctl print gui/$UID/sh.dormouse.relay, and~/Library/Logs/Dormouse Relay. Windows —Get-ScheduledTaskInfo -TaskName 'Dormouse Relay'forLastTaskResult,Export-ScheduledTask -TaskName 'Dormouse Relay'for the definition, and<install root>\logs, whererun-relay.ps1timestamps each start and exit intorelay.err.log(a crash loop is a run of those lines). Linux —systemctl --user status dormouse-relay.service,journalctl --user -u dormouse-relay.service -n 50, and~/.local/state/dormouse-relay/logs. - The task shows
Readyrather thanRunningafter a reboot: the at-logon trigger fires on interactive sign-in, not at boot — the per-login limit, not a fault. tailscale serveis refused for a non-root user (Linux): grant the operator role (Prerequisites). Preflight checks it before building, so a late hit means the check regressed or could not read the role — the release is already installed and running, so finish withmanage serverather than reinstalling./api/helloanswers but the unit is not active (Linux): something else holds port 3100 and the install correctly refuses to claim it.ss -lntp 'sport = :3100'names that process — unless it cannot see it, as under WSL withnetworkingMode=mirrored, where the listener may be a Windows process (a Windows Dormouse Relay install does exactly this). Stop it, or install on a host not sharing loopback.- The HTTPS URL returns 502: check the loopback health endpoint first, then
tailscale serve status; service and Serve configuration have separate lifecycles, andmanage servere-applies a mapping a dev session repointed. - Port 3100 is visible on the LAN or the Tailscale IP: stop. Confirm
DORMOUSE_BIND_HOST=127.0.0.1inconfig/relay.env. Tailscale access control is not a reason to expose the plaintext backend. - The installer stops on an origin mismatch: it is refusing to invalidate the registered passkey and every enrolled Burrow. Establish whether the node was renamed or re-enrolled, then restore the old name or plan the re-enrollment.
- Pocket loads but passkey setup fails: compare the browser URL byte-for-byte with
DORMOUSE_ORIGINinconfig/relay.env; confirm HTTPS and the node hostname. - A Burrow cannot connect while Pocket can: that Burrow build almost certainly bakes a different origin; its
DORMOUSE_RELAY_ORIGINmust matchDORMOUSE_ORIGINbyte for byte, and it reads an enrollment for any other as none. - State disappears: verify the absolute state path for this platform's install root and the installed config. Never initialize a new account until the old state is located or restored.
A per-login agent is down whenever its machine is — fine until the user controls a Burrow that is not this laptop. The phone reaches the origin and the Burrow dials out to it, so the relay need not run on the laptop: run the Linux installer with --linger (Prerequisites) on any always-on tailnet machine — a spare box, a NUC, a small VM — and that node's own MagicDNS name becomes the origin:
./deploy/local/install-linux.sh --lingerThat is an origin change, a deliberate migration rather than an upgrade path: the passkey and every Burrow enrollment are redone against the new DORMOUSE_ORIGIN, and every Burrow is rebuilt with it (Prerequisites). That machine needs the same backup as any other install (checkpoint 6).
Managed cloud accounts and deployment belong to docs/specs/hosted.md -> "Application boundary".
Must keep one idempotent installer per platform. Rerunning it updates the installed release from the current checkout; it never pulls, fetches, switches branches, or schedules an updater.
The security properties this deployment is audited against, and everything that enforces them, are the "Network posture (self-hosted)" and "Credentials at rest" FAIL IF lines in docs/specs/security-remote.md. Those lines bind all three installers — a control present in one and absent from another is a finding.
Each release is self-contained: the production Relay tree, lib/dist-pocket, and a copy of the exact Node binary the build ran under, so the service depends on neither the source checkout, nor Homebrew/nvm/a version manager, nor pnpm's store, nor the user's interactive PATH — none of launchd, Task Scheduler, or the systemd user manager reads any of those.
Source of truth: deploy/local/install-macos.sh, deploy/local/install-windows.ps1, deploy/local/install-linux.sh.
Service and install root are in the table at the top of this file; logs and service-definition paths are under "What the installer does". The Windows KeepAlive and current/previous rows deviate because the macOS mechanism has no unprivileged Windows equivalent (rationale).
| macOS | Windows | Linux | |
|---|---|---|---|
| RunAtLoad | plist RunAtLoad |
the at-logon trigger, LogonType=Interactive (no stored password), RunLevel=Limited |
WantedBy=default.target; survives logout only with opt-in --linger, which the installer never enables silently and verify reports rather than asserts |
| KeepAlive | plist KeepAlive |
the supervision loop in bin\run-relay.ps1; Task Scheduler's RestartCount is defence in depth, not the mechanism |
Restart=always, RestartSec=10 |
| Stopping it | launchctl bootout takes the process tree |
ends only the powershell.exe; before every start the installer and manage reap its children by install-root image path and command line, never image name |
systemctl --user stop takes the whole cgroup |
current/previous |
symlinks, swapped with rename(2) on the link path |
current.txt/previous.txt naming a release id, swapped with rename(2) on the file |
symlinks, swapped with rename(2) on the link path |
0700 / 0600 |
modes under umask 077; verify checks mode and owner |
an owner-only DACL; verify also checks owner SID |
modes under umask 077; verify checks mode and owner |
| Entry | /bin/bash bin/run-relay |
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File bin\run-relay.ps1, at an absolute interpreter path |
ExecStart=/bin/bash "<root>/bin/run-relay" |
- One replica; an update is a short intentional restart. Relay transient state is in memory (
docs/specs/relay.md→ Guardrails), so Burrows and Pocket clients reconnect across a release switch; no zero-downtime swap to attempt. - Never overwrite an existing release directory while staging. A colliding release id fails without deleting its contents.
- State outlives code.
config/andstate/sit outsidereleases/and survive every update, prune and uninstall; purging is separate and explicitly confirmed.config/relay.envpreservation isdocs/specs/security-remote.md→ "Credentials at rest". Must read its last assignment for each key, stripping only one matched pair of double quotes, in the installer, service wrapper, and management commands.scripts/installer-verify-test.mjsexercises the unix readers against the shipped wrapper parser. run-relayexportsDORMOUSE_ENROLL_TOKEN_FILEnamingrun/enroll-offer.json(docs/specs/relay.md→ Configuration); when the offer is minted and rotated isdocs/specs/security-remote.md→ "Credentials at rest".- Every installer requires
PORT=3100inconfig/relay.env, matching its Serve mapping (rationale). Bind host, Serve, Funnel and origin rules aredocs/specs/security-remote.md→ "Network posture (self-hosted)". - A failed update is a failure. The candidate release is health-checked on an ephemeral port against a throwaway state dir before
currentmoves; if the live service then fails to answer,currentis restored topreviousand the installer exits nonzero — rollback succeeding is not success. The restore clearspreviousonly oncecurrentis back on that release, so the pointers are not left naming one release (bothverifyandrollbackrefuse that state) and a failed restore keeps its rollback target. The restore then confirms which release answered (next invariant). - A 200 does not say who answered. An orphan of an older release holding the loopback port answers
/api/helloexactly like a healthy current one, so every check whose contract is which release is running proves the responder's identity: the post-switch health check (rolls back and exits nonzero on a mismatch), the rollback restore,manage verify, and every command that waits for health (manage rollback,manage restart).run-relaypassesDORMOUSE_RUNTIME_FILEandDORMOUSE_RELEASE_ID(docs/specs/relay.md→ Configuration), and the Relay writesRuntimeInfoonly once bound, so the identity check is a file read, a port match and a liveness check. It cannot go in/api/hello, which is unauthenticated and reachable through the HTTPS proxy. Empty means unknown, never "nobody" — a stale file with a dead pid, a Relay started outside the installer, and a foreign port-holder all fail the comparison. Linux still leads withsystemctl --user is-active, which catches a responder no port lookup can see: a foreign network namespace, or WSL withnetworkingMode=mirrored.manage statuson all three reports what the pointers say by design. Source of truth:relay/src/runtime-file.ts.
Each fails silently unless encoded in every installer it names; single-platform traps live as comments at their code:
pnpm deploy --prod --legacypoisons the workspace. (All three.) Snapshot and restore pnpm's workspace-state file on every exit, including failed installs (rationale).mv -f tmp linkfollows a symlink to a directory. (macOS, Linux.) Userename(2)on the link path and assert thatcurrentadvanced (rationale).
bin/manage (bin\manage.ps1, with a manage.cmd shim, on Windows) carries: status, verify, logs, restart, show-password, serve (re-apply the Serve mapping after a dev session repointed it), rollback, uninstall, and the separately-confirmed purge.
uninstall must leave manage behind, so purge stays reachable for config/ and state/. purge deletes run/ along with state/ and config/, and once uninstalled prints the one command removing the install root and, on macOS and Linux, the log directory outside it. Source of truth: cmd_uninstall / cmd_purge in deploy/local/install-linux.sh and deploy/local/install-macos.sh; Invoke-Uninstall / Invoke-Purge in deploy/local/install-windows.ps1.
Two test-only hooks, each refused unless DORMOUSE_INSTALL_TEST=1: DORMOUSE_INSTALL_ROOT puts the whole install under a throwaway path, and — Linux only — DORMOUSE_INSTALL_ORIGIN supplies the origin so Tailscale is never consulted. .github/workflows/ci.yml pins the Linux install/update path in a temp root. Test mode stops before systemd and Serve; macOS and Windows have no runtime CI coverage, so deploy-lint checks all three installers textually.