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 and the release pipeline; remote control and self-hosting are on the self-host runbook, and what reaches your machine is on the supply-chain disclosure. The five audited checklists behind all three live beside the spec, in the specs directory on GitHub.

Dormouse is a terminal, so users trust it with shells, source trees, credentials, and local files. Three things sit on that boundary. The dependency graph and release pipeline decide what code reaches a machine. Remote control, pairing a phone with a laptop, is the one feature that accepts input from the network, and an authorized phone is a person at the keyboard. And the loopback listeners Dormouse binds for its own surfaces accept input from any page in the user's browser, a boundary precisely because it does not look like one.

Only the self-hosted deployment ships. The relay runs on hardware the user owns, reachable from their own tailnet (SELF_HOST.md). Nothing about remote control applies to a Host that never enrolls with a server: enrollment is where the relay, the phone, and push begin. Cloud-hosted operation is staged, and its boundary is re-analyzed before that code ships (security-remote.md).

Guarantees

Each guarantee names the spec that states the rule and what pins it on every pnpm test. The nightly audit (below) checks all of them; audit in the last column means nothing cheaper does.

GuaranteeRulePinned by
A program printing to your terminal cannot reach past the screen. It can raise an alert, set a title, or mark a prompt; it cannot write your clipboard, open a link without your confirmation, read a file, or steal focus.Terminal outputlib/src/lib/terminal-protocol.test.ts, lib/src/lib/external-links.test.ts
A page in a browser pane cannot impersonate Dormouse. It can post to its parent, but 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
Terminal scrollback is never written to disk, and the session state that is written belongs to your account alone.Persisted stateaudit
Merging to main and creating a tag are admin-only, 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 offline. CI never holds a signing or updater key, and the signing script verifies CI's attestations and hashes first.Desktop Releasesaudit

What is not defended

Stated so the audit does not rediscover them and a reader deciding whether to run this knows what they are taking on.

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

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

  • 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 Chromatic token is reachable by any workflow the bot can author. Accepted with rotation; abuse is visible in Chromatic's dashboard (Automated Maintainer).

  • Two signing secrets travel on a command line for the life of one local call, because their tools offer nowhere else (Desktop Releases).

Known gaps

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

  • The standalone log file is written at the umask, readable by another local account wherever the temp directory is shared, and records the dor socket path; no terminal output reaches it (Persisted state).

  • The workflow audit's window has two evasions: a backdated committer date, and a branch pushed, run, and deleted before the nightly fetch (Automated Maintainer).

  • The audit's three subagents share one credential. Their contexts are separate; AUDIT_PAT is not (Domains).

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

How the guarantees are checked

On every pnpm test, four 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; a rule without one is a claim, not a check. scripts/installer-verify-test.mjs executes the installer helpers the lints can only read.

Every night at 04:21 UTC, and before every VS Code release, .github/workflows/security-audit.yaml audits the repository against these specs. Three 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 that reaches no verdict, files a public issue labeled security-audit-failure and holds the release; a later pass closes it. Open issues are live; closed ones are the record of what tripped and what changed. scripts/security-audit-local.sh runs the same prompts locally. security-audit.md is the contract.

DomainSpecsCovers
application-securitysecurity-local.md, security-remote.mdthe local application's boundaries, remote control, and every path no other domain claims
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

Every pull request that adds, removes, or upgrades a production dependency fails CI until the regenerated disclosure is committed (Disclosure).

Every release ships attestations and hash manifests from CI, verified locally before anything is signed (Desktop Releases).

Reporting a vulnerability

Report privately through GitHub's Report a vulnerability form, which opens an advisory visible only to you and the maintainers. It is the right channel for anything here, and for remote control most of all: a public issue describing a live path into a Host's ACL is a disclosure, not a report.

Never open a public issue, and never email the maintainer. Include the version or commit, the deployment (self-hosted server, standalone app, VS Code extension), and the shortest reproduction. Every advisory is acknowledged with what we intend to do about it. There is no bounty, and a fix that needs a coordinated release says so in the advisory rather than promising a date; this is a one-maintainer project and nothing here promises a response time it cannot keep.

  • FAIL IF private vulnerability reporting is disabled on the repository (gh api repos/diffplug/dormouse/private-vulnerability-reporting must report enabled: true): the advisory form is the only channel this spec offers, and a disabled one sends a reporter to a public issue.