The shape of it
Before the problems, the dimensions — because they explain why the problems are where they are.
App.tsx alone is 3,847TODO/FIXME across all of Hearth's source
Hearth is disciplined: 25 source files, one clearly-biggest module (server.ts at
1,100 lines), and almost no deferred-work markers in the code. Its tests are as large as the
implementation, which is the ratio you want when the thing being tested is a durable state
machine that must not lose a turn.
Ember is the opposite shape. Five files exceed 1,500 lines, and App.tsx at 3,847 is
where most of the application lives. That is a real contributor barrier — not because long files
are inherently wrong, but because a newcomer cannot find the seam they want to work on.
What this predicts
Everything blocking a release is in the shell — packaging, naming, paths, scripts, documentation — not the core. That is good news: it means the hard technical work is largely done, and the remaining work is unglamorous and mechanical.
Six blockers
Each is verifiable in a single command, and each alone makes a public release a mistake.
Hearth has no licence
Ember ships an MIT licence with a revenue-based rider. Hearth — the piece people would
actually fork and build against — has no LICENSE file and no licence field.
Absent one, default copyright applies: nobody may legally copy or redistribute it.
The Windows installer defaults to your home directory
Both PowerShell scripts ship with C:\Users\Noemi as a default parameter value.
A stranger who runs the documented command gets a scheduled task pointing at a repository
that does not exist, and a data directory in someone else's name.
The fix is small — derive from $HOME/$env:USERPROFILE, resolve the
repo from $PSScriptRoot — but until it is done the Windows path is unusable by
anyone else, and Windows is half the audience.
Neither package can be published, and both names are taken
Both manifests are private: true with no bin, no files,
no repository and no author. Hearth has no CLI entry point at all,
so there is nothing to put on a path. And the unscoped names are already occupied:
This needs a naming decision before anything else: scoped packages (@hearth/…),
or different names entirely. Everything downstream — the install command, the docs, the
Homebrew formula — depends on it.
The README leads with your infrastructure
The first thing a stranger reads in Hearth's README is the container path on your Mac, your tailnet address, and the names of your three machines. Seventy-four personal identifiers across three documents.
TAILNET.md is a genuinely good document about running Hearth on several machines. It is also entirely about yours. It should become an appendix or a blog post, and the README should open with what Hearth is and how to run it in one command.
The HTTP server has no request-size limit
Hearth parses JSON bodies directly off the request. There is no Content-Length
check and no streaming cap, so a single oversized POST can exhaust memory in a
process that is also holding the SQLite handle for every conversation.
A token gates it, which bounds the risk to a holder of the token. For a self-hosted single-user tool that is arguably acceptable — but it must be a stated decision, not an omission.
A bearer token sits in a git remote URL
The GitHub access token is embedded in .git/config. It is not in the tree and
has not leaked — but it is one pasted git remote -v from leaking, and it teaches
every contributor the wrong habit. Rotate it and move to a credential helper.
Nine significant issues
Not disqualifying on their own. Collectively they are the difference between "works" and "supported".
| Ref | Issue | Evidence and consequence | Severity |
|---|---|---|---|
S-01 |
Hearth binds 0.0.0.0 by default |
A process that runs shell commands listens on every interface, not loopback. Defensible for tailnet access; it must be the documented default, or 127.0.0.1 should be, with binding outward an explicit opt-in. |
high |
S-02 |
The token is accepted as a query parameter | Convenient for WebSocket clients that cannot set headers. It also puts the token in browser history, proxy logs and Referer headers. Keep it, document it, and prefer the header everywhere else. |
high |
S-03 |
No CORS, so no browser-based client | Fine for Electron, which proxies through the main process. It directly contradicts the "build your own GUI" pitch for anyone writing a web client — they will discover it as a wall, not a documented constraint. | high |
S-04 |
11 end-to-end specs that nothing runs | Playwright suites cover crews, queues, attachments, images and freshness. They only run when a human remembers. A regression in any of those is invisible until someone clicks it. | high |
S-05 |
Hearth has no lint step | check is typecheck && test. Ember runs ESLint; Hearth, the process holding the database, does not. The cheapest way to catch an unused import that hides a missing call. |
medium |
S-06 |
18 Markdown files in Ember's root, most stale | UI_UX_AUDIT.md states "Target: Ember 0.10.0 at 9fa00fc" — the repo is at 0.49. Nine *_PLAN.md files describe work that shipped or was abandoned. Contributors cannot tell which are live. |
medium |
S-07 |
Hearth's README claims 224 tests; there are 233 | A small lie of the most corrosive kind: it makes every other number in the document suspect. Numbers in docs should be generated or omitted. | medium |
S-08 |
Diagnostics logs provider error text verbatim | Up to 500 characters of whatever the provider returned, to the console. A provider that echoes part of a key in an error — which happens — writes it to a log file that outlives the session. | medium |
S-09 |
buddy-arena/ ships inside the repo |
A development toy with its own server, sibling to src. Harmless, but it is the kind of thing that makes a reviewer wonder what else is in here. |
low |
Already strong
Worth stating plainly, because these are the things most projects get wrong and this one did not.
contextIsolation: true, nodeIntegration: false, and a genuinely
tight CSP (script-src 'self', object-src 'none',
base-uri 'none', frame-src 'none'). The preload exposes a narrow,
typed surface rather than a general IPC pipe. This is better than a large fraction of shipped
Electron applications, and it should be said out loud in the README.
The remote-access password uses scryptSync with a random salt and
timingSafeEqual. Hearth's bearer check hashes both sides to a fixed length before
comparing, so it does not leak the token's length. Provider credentials never round-trip —
GET /providers reports only the credential type.
A nightly VACUUM INTO — a consistent snapshot of a live WAL database, not a file
copy — with thirty kept and old ones pruned. That is the correct primitive, and most tools
this size do not have it at all.
AGENTS.md states that all copy is marketing, especially error messages, and
requires failures to name the actor and offer a next step. That is unusually good product
thinking. It should be a public contributor guideline — it signals care that most agent
tooling does not have.
The security story
This is a program that runs commands and holds keys. The job is not to remove that power but to bound it visibly, and to say so before the features.
The framing
"This program can run commands on your machine" belongs in the first paragraph of the README, before the feature list. Anyone who keeps reading has consented to the actual thing. Anything softer is a misleading sense of safety, and a misleading sense of safety is worse than a stated absence of one.
What still has to be written
| Document | Content | Effort |
|---|---|---|
SECURITY.md | How to report a vulnerability, what response is promised, and a private channel. Expect scrutiny the moment a stranger runs this. | small |
| Threat model | Two pages: assets (keys, shell, filesystem), the trust boundary (the bearer token), and what is explicitly out of scope. Local-first tools are judged by whether they admit their limits. | small |
auth.json posture | Provider keys are plaintext on disk. Either encrypt at rest against an OS keychain, or state plainly that the file is 0600 and disk encryption is the user's responsibility. | medium |
| Tool allow-list | An operator should be able to say "no shell" without forking. The registry makes this nearly free; it needs a documented surface. | medium |
| Binding & token note | Document 0.0.0.0 and the query-parameter token, or change them (S-01, S-02). Silence here is the finding. | small |
| Dependency audit | Both pi packages are pinned exactly, which is right. Add an audit step so a CVE in a transitive package is noticed rather than discovered. | small |
The contract is the product
If the pitch is "replaceable GUI", then the wire format is what you are actually shipping. Today it exists only as Ember's piApi.ts.
Hearth's HTTP surface is already clean: conversations, entries with optional view documents, providers and sign-in, the inbox queue, approvals, questions, schedules, directory browsing — all bearer-authenticated, all JSON. A second client is possible today. It is not practical, because the only specification is 827 lines of TypeScript written for a different purpose.
What a second client author needs, in order
- An OpenAPI description generated from the routes and checked in CI so it cannot drift. This is the single highest-leverage artefact: it produces SDKs, enables exploration, and makes a bug report precise.
- A stability promise. Which routes are stable, which are experimental, what the version prefix means. Upstream pi-durable changes without notice; the HTTP surface should not inherit that.
- The
entries?docs=1concept, documented as primary. It is not an optimisation — it is what lets a tail read carry the in-flight generation, which is what makes live reasoning possible. A client that misses it will look broken in exactly the way Ember did. - An event-stream description. What may be relied on versus what is a hint to be treated as an optimisation.
- One worked example written by someone who is not the author. A forty-line script that sends a message and polls. Showing is cheaper than specifying, and it is the only real test of documentation.
A browser-based client cannot call Hearth cross-origin because there are no CORS headers. That may be the right security posture for a local server — but it means "build your own GUI" is currently true only for native clients, and the documentation must say so or the API must support an explicit opt-in origin.
Phases
Ordered by dependency, not by appeal. Phase 1 is a day of work and unblocks everything else.
Make it legal and impersonal
- Decide the Hearth licence (Apache-2.0 recommended — it is the piece people build against, and a rider on the server deters exactly those integrators).
- Strip personal paths from both PowerShell scripts; derive from
$HOMEand$PSScriptRoot. - Rotate the GitHub token; move to a credential helper.
- Rewrite Hearth's README to open with what it is, not where yours runs. Move TAILNET.md to an appendix.
- Fix the test-count claim, or remove the number.
Decide the name, then make it installable
- Pick scoped package names —
hearthandemberare both occupied. - Add
bin,files,repository,author, and dropprivate. - Give Hearth a real CLI entry point:
hearth,hearth provider login,hearth token. - Compile to a single-file binary with Bun so nobody installs a runtime.
- Docker image, then a Homebrew tap.
Make it safe to change
- CI for both repositories: typecheck, lint, test, build. Add the lint step Hearth is missing.
- Run the eleven Playwright specs in CI on at least one platform.
- A large-transcript fixture asserting that opening a session issues a tail request and not a full one — the bug that hung chats would have been caught by one assertion.
- Formatting and a commit hook so contributions are not whitespace wars.
Make it auditable
- Publish
SECURITY.mdand the threat model. - Resolve B-05 (body limit), and decide rather than omit S-01, S-02, S-03, S-08.
- Decide the
auth.jsonposture: keychain, or documented0600plus a plain statement. - Ship a documented tool allow-list so "no shell" is a setting, not a fork.
- Redact provider error text before it reaches a log.
Make it buildable against
- Generate and publish the OpenAPI description; check it in CI.
- State the stability promise and the event-stream guarantees.
- Publish the "build your own GUI" tutorial with a working minimal client, written by someone else if possible.
- Archive the stale plan documents into a
docs/history/folder or delete them.
Make it findable and survivable
- Trademark the name before it is popular — not to stop forks, but so a fork cannot pass itself off as official. The licence should say the code is free and the name is not.
- Decide CLA versus DCO before the first outside pull request.
- Packaging: Electron signing and notarisation. Without it, macOS and Windows refuse to launch the app by default, and that is a recurring cost, not a one-off.
- State the support boundary: which OSes, which providers, and that self-hosting means you run it.
Release gates
A checklist meant to be answered yes or no. Anything ambiguous is a no.
Deliberately out of scope for 1.0
| Deferred | Why that is the right call |
|---|---|
| Ember managing Hearth's process | Keeps the client replaceable and the supervisor story honest. Revisit once the wire contract is public. |
| Auto-update | Retrofitting is painful, but shipping it before signing exists is worse. |
Splitting App.tsx | Worth doing, not worth blocking on. The contributor barrier is real but not a release criterion. |
| Plugin or extension marketplace | The registry is extension-shaped and will invite this. Say no publicly, early, or it arrives as a thousand issues. |
| Multi-user or hosted Hearth | Every trust assumption here is single-user local. Hosting it is a different product with a different threat model. |
| Mobile client | The HTTP surface makes it possible, and that is the point — someone else can build it. |