Dashboard
A read-only local web view of the queue, decision log, sessions, pins and protection score. It never approves anything.
launchsafe-firewall ui opens a read-only web view of what the firewall knows, for you: the morning queue with proposed changes as a side-by-side diff, the decision log with an explanation for every entry, the sessions and why each is or is not trusted, the protection score, MCP pins and decoys. It runs on this machine only, for as long as the command runs, and it never approves anything.
launchsafe-firewall ui # prints a private address, opens it in your browser
launchsafe-firewall ui --no-open # prints the address only
launchsafe-firewall ui --project ~/code/app # doctor's project-level checks for that projectZero new dependencies: Node's http module, server-rendered HTML, one small script for the copy buttons, no build step.
Screens
| Screen | What it shows |
|---|---|
| Overview | One focal point: how many actions are held for you, with one "Review queue" action (or "Nothing needs you"); one line each for protection (score and how many things to improve), the last decisions and unattended mode; recent sessions as rows (status, id, one-line reason) |
| Protection | The score, the checks to improve first (each expands to doctor's detail), passing checks behind a disclosure, where the firewall is installed |
| Session | Trusted or not, everything that made it untrusted, its logged decisions |
| Queue | One row per held action (100 a page, oldest first): what, why in a few words, when, Review |
| Queue item | Title, one line of facts, the rule's sentence, the exact approve, reject and show commands, then the change as a side-by-side diff against the file as it is now (a unified diff on a phone) or the masked input; rules, trust sources and every reason behind a disclosure |
| Decision log | A dense table newest first, 100 a page, a segmented filter by verdict or type, and whether log verify passes |
| Log entry | explain <seq> for that entry; the raw signed entry behind a disclosure |
| Pins and decoys | Pin changes waiting for review with the accept command, pinned servers (another agent's under its name), planted decoys (present or missing), sessions that touched a decoy |
| Vault | Each stored secret as its reference (vault://<name>), kind, fields, store and where its value may go; whether the index verifies against the approval key; the vault add and vault allow commands to run in the terminal. Never a value or a fingerprint |
| Gateway and mail | The MCP servers behind the gateway (client, mode, transport); every withheld item from the one quarantine store (an MCP result by tool and server, a message by sender and subject) with its signals, status and age, never the withheld text or its preview; paired phones by label and the relay's state, never keys or credential ids; whether run can sandbox agents here |
Every screen draws on the same code the CLI uses, so the two never disagree. Paths in lists are shown relative to the project or as ~/..., cut in the middle when long, with the full path in the tooltip. Every page is computed when it is requested; reloading shows the current state. doctor's checks are cached for 60 seconds because they start claude --version and the other agents' version probes.
The look is the LaunchSafe product look (Onest, ink on a plain canvas, hairline rows, light and dark from the OS setting). The accent colour marks only something held or found: pending queue items, pending MCP pin changes, a tripped decoy. doctor's statuses use a dot and a word (ok, warning, failing), never colour alone. The Onest font (SIL OFL 1.1) is embedded in the package and served by the dashboard itself.
What it must not become
The dashboard adds a listening socket to a machine where an agent runs commands as the same user, a browser that visits untrusted pages, and text the agent or a repository wrote. So it must not be:
- A way to approve, reject or widen anything without you. The firewall's human channel is the approval passphrase typed in your own terminal. A web endpoint that changed the queue would be a new side door, reachable by anything that can send an HTTP request to 127.0.0.1.
- A way for the agent to read what it otherwise could not. The data shown is what
status,queue,log,pins,canary listandexplainalready print, and those read-only commands are allowed to the agent today. The dashboard shows no more: no keys, no approver file, no decoy values, no session secret fingerprints, no input that was masked at enqueue. - A target for a web page. A page you visit can make the browser send requests to
127.0.0.1:<port>(cross-site requests, DNS rebinding). - An injection surface. Queue reasons, summaries, proposal content, MCP launch lines and log entries are agent- or repository-written. Rendered as HTML they could run script or make a line read differently from what it holds.
- A source of network traffic. The firewall makes no network calls except
update; the dashboard must not load fonts, scripts or images from anywhere.
Security design
Read-only, always. The server answers GET and HEAD only; every other method is 405, and there are no CORS headers, so a preflight never succeeds. No handler calls anything that writes the queue, session state, pins, decoys, policy, settings or the log. A test fingerprints the firewall's authoritative files before and after requesting every page and requires them unchanged.
A stored value is never shown. Every page renders with the vault's view (names and fingerprints, read on the server and never sent): any line of text that contains a stored value, in the clear or encoded, is replaced by "[hidden: this contains the value of vault://name]".
Approving stays in the terminal. Each held item and each pending pin change shows the exact command (launchsafe-firewall queue approve <id>, queue reject <id>, pins accept <server>) with a copy button. You run it in your own terminal, which asks for the passphrase there. A passphrase field in the browser was decided against: it would teach you to type the passphrase into a web page (an agent can start its own server on another local port and draw a perfect copy of the dashboard; the terminal prompt is a place the agent cannot draw), the browser's process, extensions and autofill are outside the firewall's control, and a write endpoint, however guarded, is a state-changing request reachable by any local process that obtains the token.
A per-launch token in every URL. ui generates 32 random bytes (256 bits) when it starts. Every path is under /<token>/; a request without it, or with a wrong one, gets the same 404 as an unknown path (compared in constant time). The token lives only in the process's memory and in the line printed once to your terminal; it is never written to a file, a log entry, an environment variable or a command line. Stopping ui (Ctrl-C, a closed terminal, or 30 minutes without a request) makes every old address dead.
Lifetime. Only a request that passed the Host, Origin, method and token checks restarts the 30-minute idle clock. ui is tied to the terminal that started it: it stops on SIGINT, SIGTERM and SIGHUP, and it polls its parent process every two seconds and stops when the parent changes.
Bounded pages. The queue list is 100 held items a page. The log screens do not parse the log per request: each log file is scanned once into a small index, cached together with the log verify result and keyed by every file that makes up the log, so an append, rotation or edit rebuilds the index on the next request. Every field a page renders is cut to 2,000 characters with a visible "truncated" note, a tool input shows at most 50 keys or items per object, and a block is cut at 100,000 characters.
Opening the browser without putting the token on a command line. A command line is visible to every process of yours (ps). So ui opens a one-time launch address (a separate 128-bit code, valid once and for 60 seconds), which redirects to the token address. On macOS the browser is opened by /usr/bin/osascript with the script on standard input; on Linux by xdg-open from /usr/bin, /usr/local/bin or /bin only, root-owned and not writable by anyone else. If the code was used already, the browser shows "This launch address was already used", which tells you to stop and restart. --no-open skips this.
Host, Origin and fetch-metadata checks. The server listens on 127.0.0.1 only, on a random port. Before anything else every request must have:
Hostexactly127.0.0.1:<port>. A rebinding page's requests carry its own hostname and are refused (421).localhostis refused too.- No
Origin, or exactlyhttp://127.0.0.1:<port>(nulland other origins:403). - No
Sec-Fetch-Site, ornoneorsame-origin.cross-siteandsame-siteare refused (403); every port on 127.0.0.1 is the same site, so another local web app (including one the agent started) is refused as well.
Content Security Policy and headers. Every response carries a policy that allows only the dashboard's own style, script, font and image, no connections, no forms and no framing, plus X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer (the token never leaves in a Referer), Cache-Control: no-store, X-Frame-Options: DENY, cross-origin isolation headers and a Permissions-Policy that turns off every powerful feature. No inline script or style exists. Pages contain no forms that submit anywhere and no external URLs.
Escaping. Every value is rendered through one escaping template: &, <, >, " and ' become entities, and control characters, DEL, bidirectional overrides and zero-width characters become visible \uXXXX escapes (the same escaping the terminal output uses), so a reason cannot hide or reorder text on the review screen. Links are built from validated identifiers and URL-encoded.
Who may start it. ui is declared state-changing, so the firewall denies it when the agent runs it (FW-TAMPER, like queue approve), however it is reached, and the command itself refuses to start without a real terminal or as a descendant of a Claude Code session. The token is printed to whoever starts it: started by the agent, the agent would hold the token and could also open your browser at a page of its choosing. Nothing is lost: the agent can already run the read-only commands the dashboard draws from.
Residual risks
- A process running as the same user can read the browser's history, where the token address is stored, and can read a terminal pane that an agent is allowed to read (some desktop apps give the agent your terminal panel: run
uiin a terminal the agent cannot see). What it gains is the read-only view: the same data the read-only commands print. It cannot approve anything: that needs the passphrase in the terminal, and the hook accepts only approvals signed with the key pinned in the hook command. - The dashboard is as current as the moment the page was rendered; it does not push updates.
doctor's checks run in the dashboard's process with your environment, exactly asdoctorwould.