Auth and hardening
The web mirror is designed for trusted networks. The hardening below narrows the surface but doesn’t replace network controls — bind to 127.0.0.1 or set a token before exposing it to anything you don’t fully control.
Token auth
Section titled “Token auth”Set webMirrorAuthToken in Settings → Network → Token. Once set, every request must present it:
- Query string:
?t=<token>— easiest for<a href>links. - Header:
Authorization: Bearer <token>— preferred for programmatic clients.
Comparison is constant-time via timingSafeEqualStr. The token can’t be brute-forced one byte at a time by latency probing.
If the token is wrong:
- HTTP requests get
401 Unauthorizedwith no body. - WebSocket upgrades are rejected before the handshake completes.
?t=… is scrubbed from the URL after first auth
Section titled “?t=… is scrubbed from the URL after first auth”When the page loads from a ?t=<token> link, the browser captures the token at module-load time and then removes it from window.location via history.replaceState as soon as the first WebSocket open succeeds. Reconnects keep authenticating because the token survives in module scope — only the URL gets cleaned. Net effect: the token can’t leak via screenshare, the back/forward stack, copy-paste of the URL, or Referer headers from outbound links.
If the initial connection fails (401, network error), the URL is intentionally left intact so the failure stays debuggable — you still see the token you supplied in the address bar and can copy/edit it.
Origin enforcement
Section titled “Origin enforcement”WebSocket upgrades are rejected when the Origin header is set and doesn’t match Host. This prevents browsers on a different site from hijacking the connection over a forged WS request.
Native clients that omit Origin (e.g. curl, ht, custom WebSocket clients) still connect — only browser-originated requests carry Origin, and a browser can’t fake it.
Per-frame size caps
Section titled “Per-frame size caps”Every client → server frame is size-capped:
- 256 KiB per envelope (the JSON wrapper).
- 64 KiB per
stdinpayload (after envelope unpacking).
Oversized frames are dropped silently; the connection stays open.
Rate limiting
Section titled “Rate limiting”A token bucket limits each connection to 256 frames per second. Excess frames are dropped silently. Generous enough that normal typing and resize bursts pass; tight enough that a misbehaving client can’t flood the server.
Resize clamping
Section titled “Resize clamping”surfaceResizeRequest envelopes are validated:
colsclamped to[10, 500].rowsclamped to[4, 500].- Unparseable values are rejected entirely (no fallback default).
Session IDs
Section titled “Session IDs”Resume tokens are 128-bit hex from crypto.getRandomValues. No predictable structure — guessing a valid resume id is the same as brute-forcing 128 bits of entropy.
Bind address
Section titled “Bind address”Default bind is 0.0.0.0 (all interfaces). Set webMirrorBind to 127.0.0.1 to make the mirror reachable only from the laptop itself — useful when you want the URL but don’t want it on the LAN.
As of v0.3.161 this bind address is honored on auto-start as well — not just when toggling the mirror on manually (see the note under Token auth).
RPC socket token
Section titled “RPC socket token”The ht CLI talks to the app over a Unix socket. The socket file is created with mode 0600, so only the same OS user can open it. That’s the baseline boundary.
A second layer landed in v0.3.163 and is on by default since v0.4.12: Settings → Network → “Require RPC socket token”. The socket requires a per-boot token for state-mutating commands — typing into panes, killing processes, creating splits, installing extensions, and the like. Read-only diagnostics stay open even without the token, so a token mismatch can still be diagnosed:
ht versionht identifyht doctor- tree / read-screen and other inspection commands
The bundled ht, the pi / Claude bridges, and the extension SDK read and present the token automatically — nothing to configure on those paths, which is why the default could flip without breaking first-party workflows. Older external ht installs lose mutating commands until updated (their read-only diagnostics keep working); turn the setting off only for a third-party client that speaks the socket protocol directly and has not been taught to send __token.
The token is written to a file named socket.token (mode 0600) beside the socket. Set the env var HT_RPC_TOKEN_PATH to override that path.
Threat model — be explicit: this is defense-in-depth against an opportunistic same-user process that speaks JSON-RPC to a well-known socket path. It is not a hard security boundary — any same-user process can also read the 0600 token file. It raises the bar; it doesn’t seal it.
Threat model — what’s NOT covered
Section titled “Threat model — what’s NOT covered”- Network sniffing. The wire is plain WebSocket, not TLS. Anyone on the LAN with packet capture sees stdout. Use a VPN or stick to loopback for sensitive workflows.
- Privilege escalation inside τ-mux. A logged-in mirror has full PTY access — same as sitting at the laptop. The token is the gate.
- Browser exploits. The mirror serves
innerHTMLfrom sideband HTML panels. If you render attacker-controlled HTML, you’re exposed.
Source
Section titled “Source”src/bun/web/server.ts— auth, origin, rate-limit, size-cap logic.