Skip to content

Security: Azentrius/stellwerk

SECURITY.md

Security Policy

Reporting a vulnerability

Please do not open a public issue for security problems.

Report them privately through GitHub instead: Report a vulnerability (tab Security → Report a vulnerability in this repository).

Please include:

  • what is affected (hub, display, hook setup, Codex forwarder, firewall rule, stellwerk update, autostart, hardware monitor setup; version or commit),
  • steps to reproduce,
  • the impact you expect.

We aim to acknowledge reports within 7 days. This is a small project, so a fix may take longer; we will keep you informed and credit you in the advisory unless you prefer to stay anonymous.

Supported versions

Version Security fixes
Newest release yes
Older releases no
main development state, not a supported installation

Stellwerk is in its 0.x line: only the newest release is supported. Security fixes go into a new patch or minor release; older releases get no backports.

To stay on a supported version, update to the newest release (see "Updating" in the README): download its stellwerk-X.Y.Z.zip and SHA256SUMS into one folder, then

gh attestation verify stellwerk-X.Y.Z.zip --repo Azentrius/stellwerk    # optional, needs gh
stellwerk update stellwerk-X.Y.Z.zip    # checks the zip against SHA256SUMS first

How fixes are announced

A fixed vulnerability is announced with:

  • a GitHub security advisory in this repository (tab Security), with a CVE ID where one is assigned,
  • an entry in CHANGELOG.md marked as a security fix,
  • the release notes of the fixed release, which come from CHANGELOG.md.

To be notified, watch this repository's releases on GitHub (Watch › Custom › Releases). With a GitHub token set up, the display also says when a newer release is out.

What Stellwerk does

The hub can allow tool calls of AI agents: a decision on the display lets Claude Code run a command or change a file. That is why it is built to be reachable only from the machine it runs on and only by the display it serves:

  • it listens on 127.0.0.1 only (fixed in the code), with a firewall rule that blocks the port for other machines as a second layer;
  • hook requests need a bearer token and Content-Type: application/json; the token file is readable by the current Windows user only and the token is never logged;
  • the display signs in before it gets anything: a start file readable by the current Windows user only posts a key derived from the hook token, and only the hub's answer to it carries the page token the WebSocket needs; the display keeps it in storage of the hub's origin (address and port), which other sites and ports cannot read, and sends it only in the WebSocket handshake; there is no cookie, since cookies of 127.0.0.1 go to every port and so to other local services; the address alone gives no token; the key is in no address and no log; tools/check-browser.ts checks this in a real Edge with a second local service;
  • decisions travel only over the display's WebSocket, which needs the right Host, the right Origin and the page token from the sign-in; a broken or oversized message (over 4 KB) or an unreadable request target closes only its own connection;
  • every request is checked against Host (DNS rebinding);
  • nothing is allowed automatically;
  • the hub runs and ends one task: "Stellwerk hardware", with schtasks /Run and /End and fixed arguments, no shell, so that LibreHardwareMonitor runs while the hub runs; that task was registered by an administrator and starts only the program in its own folder in Program Files;
  • the hub starts no programs but one: on a tap on the display it hands a link to the Windows shell (explorer.exe with the link as its only argument, no shell), which opens it with the program registered for it. The display only names a session or a pull request; the hub builds the link itself from what it shows and opens only four forms, checked whole: claude://claude.ai/epitaxy/local_… (the desktop app's own id of a session, which its hooks send in a header), codex://threads/<UUID> (a chat of the Codex app) and https://github.com/<owner>/<repo>/pull/<n> (a pull request of the watched owner that it lists) and <uptimeKumaUrl>/dashboard/<id> (a failing monitor it shows, at the address set up), at most one a second. Only the setup commands run icacls, powershell, netsh and schtasks, with fixed arguments and without a shell; serve restricts the token file and the display's start file with icacls when it creates or changes them;
  • the hub connects to two other machines at most. One is api.github.com (fixed in the code), for the list of open pull requests and the newest release of Azentrius/stellwerk (to say that there is one; nothing is downloaded), and only once a GitHub token is set up: fine-grained only, meant to read pull requests and Actions and nothing else, kept like the hook token, never logged, never sent to the display; GitHub's answers are checked before use, redirects are not followed, and of the "next" links of long lists only those on api.github.com are followed, at most 10 pages per list;
  • the other is Uptime Kuma at the address set up with stellwerk kuma key (http or https, host and port, no path), only for /metrics, only once an API key is set up (which in Uptime Kuma 2 opens /metrics and nothing else), kept like the hook token, never logged, never sent to the display; redirects are not followed, and of the answer the hub keeps only each monitor's name and status; its file uptime-kuma.json holds only the ids of failing monitors and since when they fail;
  • besides that the hub asks only LibreHardwareMonitor on 127.0.0.1:8085 for data.json, when the hardware monitor is set up, with the password from LibreHardwareMonitor's settings file, and never sends that password to the display;
  • for the context meters the hub reads session transcripts named in hook events, only under %USERPROFILE%\.claude\projects and %USERPROFILE%\.codex\sessions (after following links), only from their end, and keeps only the token counts, the model, the context window and, for Codex, who approves and the usage of the plan's limits, and from the first line only who wrote it (originator), to tell Codex' command line from its app; nothing else of a transcript reaches its state, the display or its file plans.json;
  • of what tools return, the hub looks only at the desktop app's answer to get_usage in a PostToolUse hook and keeps only the plan's limits from it;
  • for the names of Codex sessions the hub reads Codex' session index (%USERPROFILE%\.codex\session_index.jsonl, a fixed path, never one from hook data) from its end and keeps only the name of a session it shows; for their projects it reads the Codex app's state file (%USERPROFILE%\.codex\.codex-global-state.json, a fixed path) after it changed and keeps only project names and which chat belongs to which; the title of a Claude Code session comes from its transcript like its token counts;
  • for the display after a restart, the hub keeps the sessions it shows in its file sessions.json: id, folder, title, project, how full the context is and the last activity; never a prompt, a question, a tool's input or a request, and a request never comes back after a restart;
  • Codex events reach the hub through Stellwerk's forwarder, which Codex runs as a command hook: it reads the hook token from the token file, posts to 127.0.0.1 only, prints only the hub's answer and ends at once without a hub; the hub checks these events like all others;
  • stellwerk update takes a release you downloaded only if it matches the SHA256SUMS next to it, unpacks it into its own folder (refusing any file name that would leave it), runs npm ci --omit=dev there and the new version's autostart install (and codex install when Codex' hooks run Stellwerk's forwarder), with fixed arguments and no shell; it ends and runs only the hub's own task, and the display's when the display moves to another screen, and removes only folders named stellwerk-X.Y.Z in its own releases folder that neither Codex' hooks nor the command itself still use (none while Codex' hooks cannot be read); it downloads nothing itself;
  • stellwerk hardware install downloads exactly one LibreHardwareMonitor archive, pinned in the code by URL, size and SHA-256, unpacks it only into its own folder in Program Files (refusing any file name that would leave it), turns on LibreHardwareMonitor's password and blocks its port for other machines; that program runs with administrator rights, from a folder only administrators can change;
  • the start of the hub and the display with Windows runs as the current user without elevation;
  • the display loads only the hub's own files under a strict Content Security Policy and shows hook data and pull request titles as text only.

Especially relevant

Reports are particularly welcome for:

  • ways to allow or deny a request without the display, or from another machine, another user account or a web page,
  • ways around the token, the Host or Origin check, the display's sign-in or the page token,
  • requests that end the hub instead of only their own connection,
  • hook data that the display renders as HTML or script,
  • the hook token, the GitHub token or the API key of Uptime Kuma appearing in a log, a URL, a page or any file other than its own,
  • the hub connecting anywhere but api.github.com, Uptime Kuma's /metrics at the address set up and LibreHardwareMonitor on 127.0.0.1:8085, or to them without being set up,
  • a hook event that makes the hub read a file outside the transcripts folders, or anything of a transcript beyond its token counts and limits reaching the display,
  • the forwarder printing anything but the hub's answer, sending the token elsewhere, or making Codex wait beyond its hook timeout; stellwerk codex install or uninstall changing more of Codex' hooks than Stellwerk's own,
  • the hub opening anything else than those four forms of links, a link the display wrote, or a session or pull request it does not show; or the desktop app's id of a session reaching the display,
  • stellwerk update setting up a zip that does not match the SHA256SUMS next to it, unpacking anything outside its own folder in %LOCALAPPDATA%\stellwerk\releases, removing folders other than its own previous releases or one that Codex' hooks may still run, ending tasks other than Stellwerk's own two, or closing Edge windows other than the display's,
  • the hub running or ending any other task than "Stellwerk hardware", or that task starting anything but LibreHardwareMonitor in its own folder,
  • stellwerk hardware install accepting another file than the pinned one, writing outside its folder, or leaving LibreHardwareMonitor's web server reachable without the password or from other machines,
  • an unanswered or slow hub blocking Claude Code beyond the hook timeouts,
  • stellwerk hooks install or uninstall changing more of Claude Code's settings than Stellwerk's own entries,
  • stellwerk autostart install registering tasks that run with more rights or other programs than the ones it shows.

There aren't any published security advisories