Security model
What remote control and this deployment guarantee, rendered from the security spec the nightly audit reads. Each row names the spec that states the rule and what pins it on every build; the audit that checks all of them is described under how the guarantees are checked.
| Guarantee | Rule | Pinned by |
|---|---|---|
| Nothing but a human at the laptop can authorize a phone. The only path into a Burrow's ACL is typing, on that Burrow, the two digits the phone shows, and the Burrow makes every access decision. | Pairing, Burrow Authorization | remote- |
| A one-time connection is one session, confirmed at the laptop. Only typing, on the laptop, the two digits that phone shows authorizes it; nothing is saved at either end, and its terminal traffic runs only directly between the two devices, over a network Settings → Network allows; Hosted carries the handshake alone. | One-time connection, its checks | lib/, scripts/ |
| The Relay cannot read ceremony or terminal content or grant terminal access. One end-to-end channel per ceremony carries content under keys the Relay never holds; account data and routing metadata remain visible. | Trust Model, Residual metadata | scripts/ |
| Push notifications are opt-in, and a push is sealed to the one phone that receives it. | Push sealing | remote- |
| A stolen or synced passkey buys sign-in, not a terminal. Every connection also needs the phone's own paired key and a fresh presence proof bound to that connection. | Passkeys, Presence proofs | remote- |
| The Burrow bounds remote session state and handshake admission independently of the Relay. Deadlines use its own clock. | Burrow bounds | lib/, relay/ |
| A Burrow talks only to the one relay origin its build was pointed at, and a self-host build contacts Dormouse's servers only when you click a link. A stock build reaches only Hosted. | Relay origin | lib/ |
| The self-host installer restricts Relay credentials to the installing account, on macOS, Windows, and Linux; Burrow enrollment uses protected app storage or VS Code's secret storage. Installer owner-check gaps are listed below. | Credentials at rest | scripts/ |
| The self-host HTTPS origin may be public; its plaintext backend may not. The Relay generates its 256-bit setup credential with no operator-supplied value, Burrow enrollment is globally admission-limited, cross-origin browsers receive no grant, and terminal access still needs local Burrow approval. | The setup password, Cross-origin access, Network posture | relay/, relay/, relay/, relay/, scripts/ |
| Push, when enabled, cannot be aimed back into the tailnet. | What crosses the boundary | relay/ |
What is not defended
A compromised browser or operating system, on either end. Active XSS in the Pocket origin can use the phone's key and, with encrypted fallback storage, extract its private bytes (Client statics). Exactly two endpoints are trusted: the distributed Burrow binaries and the exact Pocket artifact the origin serves (Trust Model); a one-time session also trusts the page Hosted serves (One-time connection).
Traffic analysis. The Relay sees who talks to whom, when, how often, and how large each ciphertext is, and keystroke timing, never keystroke values (Residual metadata). An authorized session may move onto a direct connection between the two devices, after which the Relay sees that the session exists and nothing about its traffic (Direct path). Hosted's one-time rendezvous sees a handshake's timing, addresses, and frame sizes, and Cloudflare's STUN server sees every Hosted-served phone's public address, and under Anywhere this computer's (Direct path).
Push replay, when push is enabled. A push proves confidentiality, not freshness: a Relay that kept an envelope can re-deliver it (Push sealing).
Per-Burrow unlinkability, when push is enabled. One push endpoint per browser lets the Relay see every Burrow one phone registered (Residual metadata).
Phone-key durability. Clearing site data means pairing again. Nothing is compromised; a lost key authorized nothing on its own (Client static loss).
Availability. Remote terminal access needs an online Burrow and, for new relay-backed sessions, an available Relay (Goals; keeping it up).
Known gaps
Revocation has no mechanism. Revoking a lost phone is editing the Burrow's ACL file and restarting the Burrow (Revocation and the audit trail).
There is no structured audit trail covering connects, attaches, denials, or writes (same).
Pocket Home Screen camera verification requires real iOS hardware (Device verification).
The exact runtime and server dependencies installed by this runbook are listed in the supply-chain disclosure.
You do not have to follow this by hand. Clone the repository, start a coding agent in it, and say read @SELF_HOST.md and walk me through it. It will run the checkpoints below with you, one at a time.
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://. One idempotent installer per platform:
| OS | Installer | Service | Install root |
|---|---|---|---|
| macOS | deploy/ | LaunchAgent sh. | ~/ |
| Windows | deploy/ | Scheduled Task \Dormouse Relay | %LOCALAPPDATA%\Dormouse Relay |
| Linux | deploy/ | systemd user unit dormouse- | ~/. |
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.
Prerequisites
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/→ "Network posture (self-hosted)").specs/ security- remote. md 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/→ "Network posture (self-hosted)").specs/ security- remote. md 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=$USEROn 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
--. Switching later islinger loginctl enable-/linger $USER disable-, not a reinstall.linger 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_byte for byte theRELAY_ ORIGIN DORMOUSE_the installer writes toORIGIN config/:relay. env DORMOUSE_RELAY_ORIGIN=https://<laptop>.<tailnet>.ts.net pnpm dogfood:standalone DORMOUSE_RELAY_ORIGIN=https://<laptop>.<tailnet>.ts.net pnpm dogfood:vscodeThat self-host build has no one-time connection, no managed voice, and no auto-update — update it by rebuilding (
docs/→ "Relay origin").specs/ relay. md
What the installer does
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., and points tailscale serve -- 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.jsonLogs: ~/ on macOS, <install root>\logs on Windows, ~/. on Linux. Service definition: ~/, the Scheduled Task \Dormouse Relay, or ~/..
Until the first Burrow enrolls, run/ lets a Dormouse Burrow on this machine enroll in one click without the setup password (checkpoint 4, step 2); its lifetime is docs/ → "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).
Definition of done
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-wrapper for every credential name the installer knows. Plus what only the live system shows: macOS, loaded inrelay gui/with a plist that lints; Windows, task$UID Running, no execution time limit, restarts on failure, unelevated, unstopped by battery or idle,bin\run-still carrying the supervision loop; Linux, unit known to the user manager,relay. ps1 enabled, passingsystemd-.analyze -- user verify Loopback
/responds, 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 requiresapi/ hello systemctl --.user is- active Port 3100 is bound only to
127., and the plaintext port is unreachable on the laptop's Tailscale IP.0. 0. 1 tailscale serveproxies/to127.at the origin recorded in0. 0. 1:3100 config/. A failure prints therelay. env manage servecommand that re-applies it.config/,state/,run/,config/and an unspent offer are owner-only (relay. env docs/→ "Credentials at rest"); a spent offer is gone, andspecs/ security- remote. md verifysays so rather than failing.The current release pointer resolves to a release with
RELEASEmetadata, and neither the service definition nor therun-wrapper refers to the source checkout. An absent previous-release pointer warns (a first install); one naming the same release asrelay current, 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.
Checkpoint 1: preflight
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.; 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 --, the branch, and the SHA. Never pull or switch branches on their behalf; the installer installs exactly what is checked out.short 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'
Checkpoint 2: install
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. LaunchAgent / dormouse- 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-; do not run that yet.
Checkpoint 3: verify
# 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://, 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 --: 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 --: it keeps answering across a logout, and loginctl show- 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.
Checkpoint 4: first-run setup
The Relay has no account, no passkey, and no enrolled Burrow. Same sequence as docs/ → "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/ → 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-in their own terminal, which warns before printing. Never ask for the value, and never print it into the conversation.password The Burrow. On this same machine, launch the build made with
DORMOUSE_(Prerequisites), open Settings → Network (the baseboard's Settings button), and choose My Relay only: a new install starts at Nothing, which refuses enrollment (RELAY_ ORIGIN docs/→ "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 (specs/ remote- network. md docs/→ "Remote control, in the Settings dialog"). Enrollment persists, so later launches connect on their own; the section then shows the Relay and its connection.specs/ relay. md 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://in Safari and confirm it leads with Scan a setup code. For push, add Pocket to the Home Screen and pair inside the installed app (<laptop>. <tailnet>. ts. net docs/→ 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.specs/ pocket- app. md 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.andjson vapid.— plusjson push-if push was enabled — now exist insubscriptions. json state/. Record ownership and checksums without printing contents; checkpoint 5 checks them against a reinstall.
Checkpoint 5: updating, rollback, uninstall
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/are unchanged.relay. env 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.
Checkpoint 6: limits and backup
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 ~/. 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.
Official references
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
Troubleshooting boundaries
Phone capability diagnostics
For pairing-storage failures on iOS, Android, or desktop, open https:// 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/ -> "The capability harness".
Service and deployment failures
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 -the plist,lint launchctl print gui/, and$UID/ sh. dormouse. relay ~/. Windows —Library/ Logs/ Dormouse Relay Get-forScheduledTaskInfo - TaskName 'Dormouse Relay' LastTaskResult,Export-for the definition, andScheduledTask - TaskName 'Dormouse Relay' <install root>\logs, whererun-timestamps each start and exit intorelay. ps1 relay.(a crash loop is a run of those lines). Linux —err. log systemctl --,user status dormouse- relay. service journalctl --, anduser - u dormouse- relay. service - n 50 ~/..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./answers but the unit is not active (Linux): something else holds port 3100 and the install correctly refuses to claim it.api/ hello ss -names that process — unless it cannot see it, as under WSL withlntp 'sport = :3100' networkingMode=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_inBIND_ HOST=127. 0. 0. 1 config/. Tailscale access control is not a reason to expose the plaintext backend.relay. env 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_inORIGIN config/; confirm HTTPS and the node hostname.relay. env A Burrow cannot connect while Pocket can: that Burrow build almost certainly bakes a different origin; its
DORMOUSE_must matchRELAY_ ORIGIN DORMOUSE_byte for byte, and it reads an enrollment for any other as none.ORIGIN 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.
Keeping the relay up while the laptop sleeps
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 -- (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_, 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/ -> "Application boundary".