A private operating system for autonomous work.
Persistent operators that run on CLIs you already own. Crews that share a brief without sharing state. A gate in front of every consequential action. All of it on your machine.
Quick start · The films · How it works · Security
3,054 tests, green on macOS, Linux and Windows · 160 routes · 597 UI controls declared across 63 files · connector catalog fetched live
An operator wants to run a shell command. It does not get to.
A card appears in the transcript naming the exact command, the consequence in plain English, and who asked. You allow once, or you deny.
The answer is written to a ledger.
Playing now, with no click. 04 · Nothing runs until you answer is the full film. The decision in it is a real row — Forge · Bash · mkdir -p /tmp/helmryth-demo && printf 'READY' > … — still readable at GET /api/decisions long after the recording stopped.
A chat window is stateless. You paste context in, you copy an answer out, and when you close the tab the thing you were working with is gone — its memory, its permissions, its half-finished job.
Helmryth keeps the other half. An operator is a standing role: its own engine, its own remit, its own capabilities, its own workstreams and its own cost ledger, all of which survive a restart. A crew is several operators sharing one brief and one transcript. A gate is the moment an operator wants to do something consequential and has to ask you first — in the transcript, with the exact command, and a record of what you decided.
Nothing here calls home. The control plane is a local HTTP server bound to IPv4 loopback, the renderer is one known origin, your API keys are write-only, and telemetry is off unless you turn it on.
Every film on this page was recorded in one take against a live build — real engines, real model
replies, real containers, real connectors. Nothing is mocked or re-enacted. Seven of them are played
back faster to trim dead air while a model was thinking — most sit near 1.2x and the longest,
29-takeover and 36-isolated-workbench, run at 1.45x. The token counts, dollar amounts and error
states on screen are the ones the app produced.
Every headline below links to its full film. The quoted string under each one is read off that film's own frame.
Why every loop on this page is named .png
GitHub renders a repo-relative
<video> as nothing, and puts a click-to-play control in front of anything it recognises as a .gif — so a page of honest .gif loops sits motionless until each one is clicked. Browsers dispatch on the file header, not on the extension. These files are unmodified GIF89a: 860×538, 10fps, exactly 12.0 seconds, cut by scripts/film/loop.mjs straight from the delivered master. Nothing is re-encoded to achieve it, and renaming them to .gif would break every one.
A run is an object inside a role, not a scroll position — its own transcript, its own token tally, its own unsent draft — and four of them can sit open in one operator without a word crossing between them. A crew's brief is one written document that every operator reads before each of its turns, and they all write back into one transcript.
14 · One brief. Three operators. Ordered turns. — Name one operator and only it answers; switch to full crew and every operator takes a turn in roster order, in the same thread.
| 02 · Archive a role. Restore it whole. Archiving takes an operator off the roster without destroying it; one click restores it whole. Their workstreams remain intact until you delete the operator. |
03 · Three operators, one shared workstream Name a crew, tick its operators, then set how unaddressed work routes through them. Choose the operators who will share one workstream. |
| 26 · Read the repo, staff the crew It reads a project folder and proposes a crew, with the files behind each role, before anything exists. Detected via react, vite, index.html, vite.config.ts |
08 · Roles load. Nothing else does. Loading a crew from the library brings its named roles and nothing else of yours. Only operator roles and appearance are loaded. Your work… |
| 11 · Four runs. One operator. No bleed. Each run keeps its own transcript, token count and unsent draft; switch away and back and the draft survives. Hold the Windows signing evidence until the notarisation… |
12 · Type into a run already moving A direction sent mid-run lands inside that same run, or is visibly held until the step clears. Rivet is running — Enter adds direction now |
| 13 · Rewrite the direction. The run forks. Edit a sent direction in place and commit; the run reruns, and the first outcome is kept, not replaced. Enter commits · Shift+Enter adds a line · Esc keeps… |
10 · The roster, one chord away Press ⌘K anywhere, type three letters of a name, and land inside that operator's transcript. Search operators, crews, workstreams… |
The loop at the top of this page is from this band. A gate is not a confirmation dialog — it is the point where the operator stops and the record starts. These six films are the refusals: what a standing rule will and will not cover, what a credential is allowed to touch, what happens when an inbound payload tries to issue orders, and what an operator is never permitted to schedule for itself.
20 · Nothing key-shaped survives the transcript — Paste a key into a command and it comes back masked in the gate card, in the reply, and in the raw protocol log on disk.
| 18 · Grant one command, not a blanket Grant options names the exact key it will remember; the next unrelated command still stops at a gate. Always allow Bash:mkdir |
19 · A key the transcript never holds The operator raises a gate for the key, stores it on the device, and resumes without recording it. Stored locally; never written into the workstream. |
| 22 · The payload never becomes an order A webhook body demanding your API keys arrives fenced as untrusted data; the operator names it and refuses. That's a prompt-injection attempt riding inside the web… |
24 · It can only propose the schedule Told to put itself on a cadence, it can only raise a gate carrying the exact executable draft. Schedule: Monday, Tuesday, Wednesday, Thursday, Friday… |
| 25 · Imported methods arrive switched off A skill pulled from GitHub stays disabled until you read the whole file; edit it on disk and it disables itself again. I reviewed the complete method and understand its sour… |
31 · A hedge is not consent Answer a gate out loud with "yes, if you think it's fine" and the operator asks you again, plainly. Sorry — is that a yes or a no? |
Every operator has an execution surface, and there are four of them: an isolated Linux desktop in a container on your own machine, a hosted desktop, your own Mac, or off. Off is the default. On top of that sits a browser the operator drives on its own profile, and a connector catalog it can only reach through connections you made by name.
36 · A Linux desktop, on your machine — Switch the execution surface to Isolated and the operator gets a graphical Linux desktop in a local container, with no cloud account anywhere in the path.
| 28 · The operator gets its own browser A real page loads inside the app on the operator's own profile, and you can take the wheel on the same tab. Profiles hold logins and cookies. Operators assigned to… |
29 · It will not type your password At a sign-in wall the operator stops, hands you the keyboard and waits until you give the surface back. You hold this surface. Rivet is waiting until you return it. |
| 30 · Two locks before it touches macOS The app asks you to acknowledge host control, and the local server rejects any API call that skips it. {"error":"Auto mode on this Workbench requires confir… |
06 · Search the live connector catalog Type in the catalog and results filter against the live broker; connecting one is an explicit, named act. Connect the services your operators may use during a run. |
Three ways a run begins while nobody is looking at the app: another AI over MCP on the loopback interface, a clock, or a single HTTP request. The plate is the first of them.
27 · Another AI can staff your roster — Claude Code, over MCP on 127.0.0.1, creates an operator and briefs it; it appears in the roster while nobody touches the app. Ends on exit 0 · 9.0s.
| 07 · Put an operator on a schedule Pick the operator, write what it should deliver, choose the hour and the weekdays, and the calendar fills. Each occurrence starts a fresh run for its operator. |
23 · One curl wakes an operator Copy one command and an operator starts; send it twice for one run; rotate the secret and the replay is rejected. curl -sS -X POST 'http://127.0.0.1:8621/hooks/wh_p6t… |
Everything an operator does is written down while it happens: a step ledger that counts up live, a price per operator, and the verbatim protocol frames underneath both. Deleting a run does not delete the record of the deletion.
16 · The raw protocol your engine spoke — One switch turns a run into a timestamped step ledger; one lens shows the verbatim frames the engine sent and received, including your own prompt exactly as it arrived.
| 15 · Close the window. Work continues. The run ledger records every step as it happens; reopen mid-run and it has moved on without you. 8 recorded steps · live |
17 · Every run priced as it runs Steps, tokens and cost per operator, with subscription spend captioned equivalent rather than billed. Cost equivalent — on your subscription, not billed. |
| 21 · Delete a run, keep the receipt Removing a run itemises the transcript, attachments, trace files and decision rows it destroyed, and names what it kept and why. Workspace checkpoint history was retained because snap… |
|
Three receipts that weigh nothing, need no video, and reproduce against your own build.
The host-workbench acknowledgement is enforced by the server, not the UI. Handing an operator
your real macOS session takes a warning in the app and an explicit flag on the wire. The same
PATCH without it is refused:
$ BOT=$(curl -s http://127.0.0.1:8799/api/bots -H 'Origin: http://127.0.0.1:5199' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["bots"][0]["id"])')
$ curl -sD- -X PATCH http://127.0.0.1:8799/api/bots/$BOT \
-H 'Origin: http://127.0.0.1:5199' \
-H 'Content-Type: application/json' \
-d '{"computer":"local","autoApprove":true}'
HTTP/1.1 400 Bad Request
{"error":"Auto mode on this Workbench requires confirming the warning first (acknowledgeLocalAuto)"}
It is the combination that is gated, so both fields have to be in the body — asking for the
workbench alone is not the dangerous request and is answered 200. Only a body that also carries
"acknowledgeLocalAuto":true grants it (server/index.ts:6291-6295).
The origin gate is exact-authority. A request carrying a wrong Origin is answered
403 forbidden: cross-origin request (server/index.ts:4213), and localhost is not accepted as
an alias for 127.0.0.1.
The decision ledger outlives the run. GET /api/decisions replays what you answered, newest
last; ?limit=0 and ?limit=nope are both refused with 400 limit must be a positive whole number
(server/index.ts:7148, server/decision-log-wiring.test.ts).
Then run the suite yourself:
pnpm test # full suite
pnpm lint # oxlint
pnpm check:electron # desktop hardening assertionsThe traceability matrix and the dated execution evidence behind every number on this page are in
docs/qa/.
┌───────────────────────────────────────────────────────────────┐
│ Electron shell nodeIntegration off · contextIsolation │
│ on · sandbox on · contextBridge only │
├───────────────────────────────────────────────────────────────┤
│ React renderer one known origin, e.g. :5199 │
├───────────────────────────────────────────────────────────────┤
│ Local core :8799 IPv4 loopback only · exact-origin gate │
│ ├── engine drivers claude · codex · opencode · openai… │
│ ├── permission broker unix socket, per turn, fail-closed │
│ ├── capability broker 500 connectors, credentials held │
│ ├── cadence scheduler + webhook receiver on :8800 │
│ └── SQLite + JSON ~/.helmryth │
└───────────────────────────────────────────────────────────────┘
The permission broker is the interesting part. When an operator's engine wants a tool that its permission mode would otherwise silently deny, the request is routed over a per-turn unix socket to Helmryth, which renders it as a card in the transcript and waits for you. If the broker cannot start, the turn fails closed — an unanswerable request is denied, never auto-approved.
| Node.js | 24 or newer (engines.node: ">=24") |
| pnpm | 10.33.0 (packageManager) |
| An operator CLI | at least one of Claude, Codex, OpenCode, Grok, Cursor, Qwen, Kimi, Droid… |
| Docker | optional — only for the Isolated Workbench |
Helmryth does not ship a model. It drives the CLIs you already own and are already signed in to, which is why the setup step is a scan rather than a form.
git clone https://github.com/Helmryth/HelmRyth.git
cd HelmRyth
pnpm install
# terminal 1 — the local control plane (defaults to :8799, webhooks on :8800)
pnpm dev:server
# terminal 2 — the renderer (defaults to :5199)
pnpm dev
# terminal 3 — the desktop shell
pnpm dev:desktopThen open the desktop window and walk the four setup steps. That is the whole install.
What to watch for. The setup step titled Runtime inventory — choose how work moves is not a configuration form. It is a live scan of the CLIs already installed on the machine, with their real version numbers, and an install command for the ones that are missing.
One thing that will bite you. The core accepts requests from exactly one renderer origin. If you change the UI port, set the same
HELMRYTH_UI_ORIGINin all three terminals or the core will answer403 forbidden: cross-origin request. This is deliberate — it is what stops a web page you visit from driving your operators. See Local control-plane request boundary.
pnpm test # full suite
pnpm lint # oxlint
pnpm check:electron # desktop hardening assertionsEverything is optional. Helmryth runs with no configuration at all.
| Variable | Default | What it does |
|---|---|---|
HELMRYTH_PORT |
8799 |
Core HTTP port |
HELMRYTH_WEBHOOK_PORT |
HELMRYTH_PORT + 1 |
Webhook receiver |
HELMRYTH_UI_PORT / HELMRYTH_UI_ORIGIN |
5199 |
The one renderer origin the core will accept |
HELMRYTH_DATA_DIR |
~/.helmryth |
Profile, transcripts, decision ledger |
COMPOSIO_API_KEY |
— | Enables the 500-connector capability catalog |
OPENAI_COMPAT_API_KEY |
— | Key for any OpenAI-compatible endpoint (OpenRouter, Groq, local) |
OPENAI_COMPAT_URL · OPENAI_COMPAT_MODEL · OPENAI_COMPAT_PROVIDER |
— | Endpoint, model and provider preference for that key |
HELMRYTH_OPENAI_IMAGE_KEY |
— | Image generation for operator sigils |
In packaged builds, credentials go through Electron's safeStorage into the OS keychain. In every
build they are write-only across the API: GET /api/config returns configured flags, never
values.
This is a local agent runner that executes real commands on your machine. The boundaries are deliberate and tested, not aspirational.
- Exact-authority origin gate. A request carrying a wrong
Originis refused, andlocalhostis not accepted as an alias for127.0.0.1— which is what closes DNS rebinding, since a browser always sendsOrigincross-origin. A request with noOriginat all (curl, a native client) is still served, so the boundary is the loopback bind plus the origin check, not the origin check alone. - Desktop hardening.
nodeIntegration: false,contextIsolation: true,sandbox: true, all IPC through acontextBridgeallowlist. External URLs open only if the scheme ishttp:orhttps:— an allowlist, not a denylist, sojavascript:,file://,data:,chrome://and every scheme nobody has thought of yet are refused by default (electron/external-url.mjs). - MCP credentials never touch argv. They are written to a
0600temp file and passed by path — anything on argv is readable by any local process throughpsfor the life of the turn, and the temp directory is removed when the session ends (server/drivers/claude.ts). - Gates fail closed. A permission broker that cannot bind denies; it never degrades to allow.
- Write-only secrets. Keys go in and never come back out over HTTP.
- Telemetry off by default, and there is no sink configured in this tree.
- Webhook secrets are show-once. The creating
POSTreturns the credential so you can install it;GET /api/webhookshas no secret field at all, so listing endpoints can never re-reveal one.
These are counted by scripts/check-qa-coverage.mjs, which fails the build if the code drifts from
the documented census:
| Tests passing | 3,054 registered across 274 files — 3,035 pass on macOS (19 POSIX-only skips), 3,045 on Linux (9) — plus 89 in seven child suites |
| HTTP routes | 160 unique, each mapped in the traceability matrix |
| Renderer controls | 597 declared across 63 files |
| Runtime IPC channels | 76, plus 10 preload event topics |
| Contrast | 21 colour pairs measured by pnpm check:contrast, no failures |
The route, control, file and IPC counts are build gates rather than claims: pnpm check:qa-docs
pins each one and fails when the code and the count drift apart. The pass count is whatever
pnpm test prints on the day you run it. The route-to-control traceability matrix is in
docs/qa/.
- the desktop app and the local control-plane server
- the companion sidecar for phones
- the Cloudflare workers for the capability broker and control plane
- docs, QA traceability, and packaging metadata
Packaged distribution endpoints are not provisioned here. If you publish Helmryth for others, configure your own release channel, signing assets and checksums.
Do not rename the film assets. Every -loop.png under docs/media/ is an animated GIF89a saved
under a .png name, for the reason given beside the films above. Renaming one to .gif silently
kills the loop. They are cut by scripts/film/loop.mjs from the delivered masters, never re-encoded.
- Local control-plane request boundary
- Capability broker and connectors
- MCP server
- Computer use and the Workbenches
- The isolated Linux desktop
- Voice mode
- Production setup
- Bring your own VPS
- QA traceability and execution evidence
Bug reports, questions and patches are all welcome.
- CONTRIBUTING.md — how to set up, what to run before opening a pull request, and the evidence a change is expected to carry
- Discussions — questions and early ideas, so the issue tracker stays for things that are broken
- SECURITY.md — how to report a vulnerability privately. Please do not open a public issue for one
- CODE_OF_CONDUCT.md — the behaviour expected of everyone taking part
Helmryth is released under the Apache License 2.0.
Attribution and third-party notices live in NOTICE and third_party/. Those records preserve upstream history and licensing; they are not marketing copy.
HELMRYTH · The work moves. You hold the helm.





