Dormouse

Security

What Dormouse promises, what it does not, and the audit that holds it to the difference.

This page is the spec the audit runs against, published from the repository — not a summary of one. It shows the guarantees for the local application, remote control, and the release pipeline; running your own Relay is on the self-host runbook, and what reaches your machine is on the supply-chain disclosure. The audited checklists behind all three live beside the spec, in the specs directory on GitHub.

Dormouse holds shells, source trees, credentials, and local files. Its dependencies and release pipeline determine what code reaches a machine; remote control admits an authorized phone as a person at the keyboard; loopback listeners receive requests from pages in the user's browser.

Remote control supports relay-backed sessions and direct one-time connections. The Relay can be self-hosted; Hosted implements admin-entitled enrollment, account-scoped relay routing, and Pocket (Hosted). Hosted account sign-in alone grants no terminal access. Pairing and presence remain the Burrow's decision. Hosted also serves the one-time phone page and forwards only that connection's handshake ciphertext, authorizing nothing (Hosted rendezvous); its deployment pipeline and Cloudflare account are part of a one-time session's trust base.

A self-hosted Relay runs on hardware the user owns; the default installer keeps it private to the tailnet, while the application boundary permits a public HTTPS origin (SELF_HOST.md). Remote access starts only when a Burrow enrolls with a Relay or opens a one-time link; a link admits one phone for one session.

Guarantees

The nightly audit checks all of them; audit in the last column marks a property no cheaper check pins.

GuaranteeRulePinned by
Terminal output cannot write your clipboard or steal focus. File access requires a user action or a running designated Tool's gated OSC 367 open. OSC 52 only offers a copy format; links require confirmation or an allowed local preview, and deceptive links have no open action.Terminal outputlib/src/lib/terminal-protocol.test.ts, lib/src/lib/external-links.test.ts, lib/src/lib/terminal-link-activation.test.ts, lib/src/components/ExternalLinkModalHost.test.tsx
A page in a browser pane cannot forge a host message. In VS Code every host message carries a per-boot token it cannot read, and the standalone adapters have no inbox for it to post to.Browser paneslib/src/lib/platform/vscode-adapter.test.ts
Only your own account can drive your terminals through dor. The socket sits in a directory only you can open, and its token never crosses the wire.The dor control socketstandalone/sidecar/dor-control-server.test.js
A loopback listener grants a stranger nothing it could not get from the upstream directly.Loopback Listenersscripts/loopback-lint.mjs
Current persistence writers never save terminal scrollback. Standalone snapshots are owner-only; VS Code controls access to its own storage. Older snapshots may contain transcripts.Persisted statecargo test in standalone/src-tauri (the owner-only half); audit
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 Authorizationremote-lib-common/test/security-guarantees.test.mjs
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 checkslib/src/remote/client/one-time-e2e.test.ts, scripts/e2e-lint.mjs
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 metadatascripts/e2e-lint.mjs
Push notifications are opt-in, and a push is sealed to the one phone that receives it.Push sealingremote-lib-common/test/push-seal.test.mjs
A stolen or synced passkey buys sign-in, not a terminal. Every connection also needs the phone's own paired key and user presence the laptop verified itself: a fresh proof bound to that connection, or one it verified earlier for that phone, lapsing after five idle minutes or twelve hours.Passkeys, Presence proofs, Presence windowremote-lib-common/test/security-guarantees.test.mjs
The Burrow bounds remote session state and handshake admission independently of the Relay. Deadlines use its own clock.Burrow boundslib/src/remote/burrow/burrow-bounds.test.ts, relay/test/malicious-relay.test.mjs
Under Settings → Network → Nowhere, a new install's level, Dormouse opens no connection on its own: no relay socket, one-time link, push, managed voice, or update check. What you click, and what your terminals, browser panes, and agents reach, restored ones included, are yours.Network policylib/src/host/remote/service.test.ts, lib/src/host/managed-voice-host.test.ts, standalone/src/updater.test.ts
Under Local networks only, terminal traffic crosses only the networks you allow. Handshakes, pairing, alerts, and managed voice still pass through Hosted from any network.Direct path, One-time connectionlib/src/host/remote/local-networks.test.ts, lib/src/remote/burrow/burrow-direct-only.test.ts
Under Anywhere, a one-time connection is direct-only; a paired phone may relay through Hosted, end-to-end encrypted. Cloudflare STUN sees this computer's public address as a phone connects.Direct path, One-time connectionlib/src/remote/burrow/one-time-runtime.test.ts, lib/src/remote/burrow/burrow-direct-only.test.ts, lib/src/host/remote/service.test.ts
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 originlib/src/host/relay-origin.test.ts
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.Credentials at restscripts/deploy-lint.mjs
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 posturerelay/test/setup-password-store.test.mjs, relay/test/config.test.mjs, relay/test/token-bucket.test.mjs, relay/test/cors.test.mjs, scripts/deploy-lint.mjs
Push, when enabled, cannot be aimed back into the tailnet.What crosses the boundaryrelay/test/push-endpoint.test.mjs
Merging to main and creating a tag are admin-only, except the hosted/ tag a dedicated App records after a reviewed Hosted deploy, and every workflow this repository authors pins its actions by commit.GitHub Actions Policiesaudit
The bot maintainer cannot merge, tag, or read a release secret, and its token never enters its own environment.Automated Maintainer (tend).github/workflows/workflow-audit.yaml, nightly
Publishing the extension takes a second human's approval.VS Code Extension Releasesaudit
Desktop binaries are signed locally. CI never holds production signing or updater keys, and the signing script verifies CI's attestations and hashes first.Desktop Releasesscripts/sign-and-deploy.test.mjs

What is not defended

  • A process running as you. dor, its socket, and every file mode bound other local accounts, never a program already running under your own account; an agent holding dor has exactly the power of the person at the keyboard, and the Tool trust prompt is no boundary against it (The dor control socket).

  • The Windows dor pipe carries no ACL of ours. A named pipe has no directory to harden, so an unguessable name and the token handshake are the whole of it (The dor control socket).

  • A local HTML or SVG document you open can send its contents off the machine. The file viewer's content policy confines what the page loads, not where its scripts navigate (Local-file viewer).

  • A mermaid sanitizer bypass in a Markdown document you open. Its diagrams reach the editor page through mermaid's strict sanitizer, not the editor's allowlist, so a bypass would run script holding the editor's save and image routes (Local-file viewer).

  • A Tool trust grant on an upstream URL trusts the URL a checkout claims. Any directory whose own .git/config claims an already-granted upstream shares that grant; a folder-only grant is not shared (Dor Tool configuration).

  • What VS Code does with the pane state it stores. Structure persists in VS Code's own storage under its modes, never a transcript (Persisted state).

  • 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).

  • Whichever account first approves a live Hosted enrollment code owns the computer it enrolls; yours is then refused as already approved. That account still pairs no phone without the two digits typed here (Trust boundary).

  • The bot's upstream is pinned by tag, not commit, so a hostile upstream could change what the bot runs without a diff here. Accepted: the trust equals what the harness already holds (Automated Maintainer).

  • The repo-level snapshot-testing tokens are reachable by any workflow the bot can author. Accepted with rotation; each service's dashboard shows abuse (Automated Maintainer).

  • The workflow audit trusts what it classifies as routine. A Renovate pin bump trusts the ref Renovate picked inside that action's repository; a bot commit under a forged human author passes when an admin push that replaces nothing carries it in, such as a cherry-pick or a rebase onto a new branch, or when it was first pushed more than a quarter ago (Automated Maintainer).

Known gaps

Gaps rather than accepted risks: we intend to close them.

  • Browser-pane scripts share loopback cookies across grant ports. HTTP and WebSocket cookie headers are stripped, but document.cookie remains shared; cookie-authenticated iframe pages are unsupported (Loopback Listeners).

  • Windows screenshots and pasted clipboard images are only as private as the temp directory they land in: private by default, exposed if it is shared or loosened (Browser panes).

  • Neither VS Code's peer-link token, its Tool trust receipts, nor the recovery.json beside them carries a Windows ACL applied by Dormouse. They are written owner-only by unix mode, which Windows makes a no-op; standalone locks its state directory instead (Persisted state).

  • Revocation has no mechanism: nothing in the product revokes a paired phone (Revocation propagation).

  • There is no structured audit trail covering connects, attaches, denials, or writes, so a self-hoster cannot answer "did anyone connect to my laptop last night"; each pairing records its approval, and owner-local logs report some rejections (Trust boundary).

  • A fork PR the bot checks out mid-run reaches it with that fork's instruction files. The harness pins project-instruction paths to the base branch only for the checkouts it makes before the agent starts (Automated Maintainer).

  • The workflow audit's window can be evaded. A pusher-set committer date hides a commit from every later window, and a branch pushed, run with repo-level secrets in scope, and deleted before the nightly fetch is in none (Automated Maintainer).

  • The audit agent can reach more than its domains need. They share AUDIT_PAT, and it shares its job, and a token that can post issues, with the steps that encrypt and privately file its findings; EMBARGO_TOKEN is out of its environment, not its reach (Domains, Embargo).

  • The audit's reporting can misstate a run. A PASS can be accepted with no merged report, an open failure issue past the first page of the issue listing is never reconciled, and quoted VERDICT: lines can push later verdicts out of the issue's preserved head (Outcomes and reporting).

  • The notarization password sits on a command line for up to half an hour per architecture; the remedy is known and not yet done (Desktop Releases).

  • Pocket Home Screen camera verification requires real iOS hardware (Device verification).

  • Any local process can drive agent-browser's browser. Its Chrome's debugging port and its daemon's stream server take unauthenticated loopback connections, so another local account can run script there, read file:// URLs, or watch it; a web page cannot, since both refuse a foreign Origin (Loopback Listeners).

How the guarantees are checked

On every pnpm test, lints turn the cheap half of these specs into build failures: scripts/spec-lint.mjs (the specs' own conventions and word budgets), scripts/e2e-lint.mjs (one Noise suite, no negotiation, no plaintext path), scripts/deploy-lint.mjs (every installer control, on all three platforms), and scripts/loopback-lint.mjs (a new loopback bind references a guard). Each carries a self-test that re-introduces the thing it forbids and requires the lint to go red. scripts/installer-verify-test.mjs executes the installer helpers the lints can only read.

Every night, and before every VS Code release, .github/workflows/security-audit.yaml audits the repository against these specs. Domain subagents, each owning the specs below, run every FAIL IF as a mechanical check with evidence, then read their domain adversarially for what no check names. A failure, or a run reaching no verdict, holds the release and files a public issue labeled security-audit-failure with each domain's verdict, its finding counts, and the failed sections' names; details stay private until fixed. A later pass closes it. security-audit.md is the contract. pgstencil audits the packages Hosted consumes in its own repository.

DomainSpecsCovers
application-securitysecurity-local.md, security-remote.mdlocal boundaries, remote control, and everything no other domain claims
hostedsecurity-hosted.mdHosted accounts, the one-time rendezvous, and the pgstencil provenance link
supply-chainsecurity-supply-chain.mdthe dependency graph, the lockfile, the disclosure and its generator
ci-and-secretssecurity-ci.md, security-audit.md, this specGitHub Actions, the bot, releases, secrets, and the audit itself

Source of truth: root scripts in package.json; native jobs in .github/workflows/ci.yml; .github/workflows/security-audit.yaml and its release gate in .github/workflows/release.yml.

Reporting a vulnerability

Must report vulnerabilities privately through GitHub's Report a vulnerability form, visible only to the reporter and maintainers. Never open a public issue or email the maintainer. Include the version or commit, deployment (self-hosted Relay, Hosted, standalone app, or VS Code extension), and shortest reproduction. Every advisory is acknowledged with intended next steps; there is no bounty or promised response time. A coordinated-release requirement is communicated in the advisory.

  • FAIL IF private vulnerability reporting is disabled on the repository (gh api repos/diffplug/dormouse/private-vulnerability-reporting must report enabled: true) (rationale).