The hako agent in a browser tab. One binary, your files, your models, any device on your network.
site · hako-code · hako (engine) · hako-edit · changelog · org
![]() tool calls as chips · files and sessions per folder |
![]() :theme · the suite's 19 palettes, live
|
![]() settings · and a plain answer on where your data lives |
| No backend. Static files and your own device. No database, no account, no telemetry, nothing proxied. A conversation, a key and a file never reach a server of ours — there isn't one. |
One agent, three shapes. The same hako.c compiled natively and to WebAssembly. Hosted page, container, or hako --serve — same loop, same tools, same session format. Nothing reimplemented in JavaScript. |
Every device you own. Run it from the machine with your files and drive it from the couch, or open the page on a phone and let the agent run there. --token when it leaves the loopback. |
—— I ——
-
Three ways to run it, one piece of software.
hako.cis compiled twice — natively for the binary, and to WebAssembly for the browser. What differs is only what sits underneath, which is what decides whether there is a shell and a real filesystem.how files shell · local models hosted a static page, e.g. hako.mithraeum.studiobrowser storage, or a folder you grant (Chromium desktop) no container docker run -p 8080:80 hako-studiosame as hosted — the agent runs in your browser, not in the container no binary hako --servereal folders on that machine yes -
What the browser build cannot do, because a browser sandbox does not permit it: run a shell, run local hako models (a subprocess), or complete an OAuth sign-in (provider token endpoints refuse cross-origin requests). It takes API keys, and those go from the page straight to the provider. Work is kept in browser storage unless a real folder is granted — clearing site data erases it, so save what matters out of the files list.
-
It is the agent.
hako --serveis a mode of the agent binary, not a wrapper around it: the same loop, providers, tools and session format that run in the terminal, carried to the browser over HTTP + Server-Sent Events. Nothing is reimplemented in JavaScript, and there is no child process to keep alive. Updatehako, and studio updates with it. In the browser the same code runs as WebAssembly against a platform seam — a JS reimplementation of the agent loop is the one change this project will not take. -
Shared state with the CLI (binary). Sessions,
~/.hako/credentials, skills,~/.hakorcand per-project trust are the same files the terminal uses.:resumein either surface picks up the other's conversation. In the browser the same layout exists, in that origin's storage. -
Tool calls ask first (binary). Every
write_file,edit_file,run_shelland read pauses for allow once · deny · always, mirroring the CLI'sy/n/ascoping — project-wide for reads and writes, exact-command for shell. A wasm turn cannot pause to ask, so the browser build does not gate: there the boundary is the sandbox itself. -
Any folder, per-folder sessions. Browse, open a project, and the sidebar lists that project's conversations. Resume renders the full stored transcript; delete removes the jsonl. A session belongs to a directory, never to a provider or a model.
-
Local models first (binary). Installed weights, loose
.mlf2files found in~/Downloadsor any folder you add (symlinked into place — never copied, never moved), and:pullfor the rest. Cloud catalogs come from the provider. The browser build lists only what a page can reach, so local models and OAuth providers are hidden there rather than offered and broken. -
19 themes — the same list as hake and hako,
washiby default — and a thinking indicator with elapsed time. Both persist on your device. -
Your code sessions, from anywhere (invite beta). Run
:remote onin a terminal hako and sign in at hako.mithraeum.studio on any device: ☰ → code lists the sessions running on your computers, one per shared folder, and opening one turns the page into that session. Every byte is sealed in the tab (Noise, byhako.wasm) and opened only by your computer; the relay in between routes ciphertext and keeps no chats. Details below. -
Works on a phone. Drawer navigation, touch targets, keyboard-aware layout, add-to-home-screen. Serve it from the machine you are sitting at and drive it from the couch — or open the hosted page and let the agent run on the phone itself.
-
Nothing to install. libc and pthread. No framework, no CDN, no build step for the page. The UI is one HTML file, compiled into the binary — so a single executable is the whole product — and the same file is what the hosted page and the container serve.
—— II ——
make # build hako.wasm and assemble dist/
make serve # dist/ on http://localhost:8080, nothing cached
make serve-tls # the same over self-signed HTTPS, for a phone (OPFS needs a secure context)
make docker # container image serving dist/
make test # bundle sanity, the wasm behaviour tests, theme sync, and the browser testmake test runs make check (one file, nothing external, plus node tests of the wasm) and,
when playwright and a built ../hako-code/hako are present, make ui-test: a headless
browser drives the page against hako --serve and a fake engine — a turn, an approval, a
picker, uploads, desktop and phone widths, no JS errors.
The hosted page at hako.mithraeum.studio is rebuilt every night from hako-code main.
The toolchain is clang. make toolchain fetches wasi-sdk (~175 MB) into
~/.hako/toolchains; remove it with rm -rf. No emscripten, no npm, no
framework, and no build step for the page itself.
The wasm is built from hako-code:
a checkout beside this one wins, otherwise the released source is fetched.
That source must carry the platform seam (hk_http / hk_fs / hk_shell /
hk_time) — hako-code v0.2.3 or newer. Against anything older the wasm build
fails on a missing POSIX header.
The fullest of the three: real folders, a shell, local models. The UI is served
by hako itself:
hako --serve --dir ~/code/my-project
# open http://127.0.0.1:8787--serve start hako studio
--port N listen port (default 8787)
--bind ADDR bind address (default 127.0.0.1 — anything else exposes the agent)
--dir PATH project to open
--web DIR serve DIR/index.html instead of the built-in bundle (UI work)
--yolo auto-approve every tool call
On a phone or tablet on the same network: run hako --serve --lan --dir ~/code/my-project,
open the phone URL it prints, and add it to your home screen.
Add --token. --serve has no password; anyone who can reach the port can
run tools as you. --token requires a secret on every request and prints a URL
carrying it once, which the browser exchanges for a cookie:
hako --serve --lan --token --dir ~/code/my-projectOff your own network, use :remote on (below). Avoid hosted tunnels and open ports: a
tunnel's servers end the TLS and can read everything, and an open port puts the agent on the
public internet.
Inside a terminal hako, in the folder you want to share:
:remote on
The first time, a page opens to link this computer to your account (a passkey; accounts are
invite-only for now, ask at contact@mithraeum.studio). After that, sign in at
hako.mithraeum.studio on a phone or another computer and ☰ → code lists what is
running: one session per shared folder, named by folder and machine. Open one and you are in
the same chat as the terminal. Tools still ask, answered from whichever side is first;
:auto, :trust and the rest stay at the desk. :remote off ends it.
How it holds together:
- The account key never leaves your devices in the clear. It is made in the browser, wrapped by the passkey's PRF output, and handed to the computer once, sealed, over the link page.
- Sessions are end to end.
Noise_XXpsk3_25519_ChaChaPoly_BLAKE2b, with a pre-shared key derived from the account key, so only a device on your account can complete the handshake. - The relay routes ciphertext. It holds an account id, how you sign in (a passkey's public key, or a keyed hash of your email, never the address), the wrapped account key, and your devices' public keys and sealed names. No chats, no content, no request logs.
The relay is in relay/: one C file and libcrypto, GPL-3.0 like the rest.
make test there runs its tests and hako's against it; make remote-test here runs the whole
path in Chromium and WebKit. To use your own, point hako at it with HAKO_RELAY_URL (and
HAKO_STUDIO_URL for the page) and build the page with make site RELAY_URL=….
macOS blocks inbound connections to an unsigned binary while leaving loopback alone, and it
does it silently: the TCP handshake completes, then the socket is dead by the first read. The
phone spins forever, nothing is logged, and every layer above looks broken. Under --debug the
tell is read=-1 errno=57 (ENOTCONN) — or no request at all.
make therefore signs the binary on macOS, and prefers a stable identity over an ad-hoc one:
# once — a self-signed code-signing identity called "hako-dev"
D=~/.hako/codesign && mkdir -p $D && chmod 700 $D && cd $D
openssl req -new -newkey rsa:2048 -nodes -keyout hako-dev.key -x509 -days 3650 \
-out hako-dev.crt -subj "/CN=hako-dev/O=mithraeum" \
-addext "extendedKeyUsage=codeSigning" -addext "basicConstraints=critical,CA:false"
openssl pkcs12 -export -inkey hako-dev.key -in hako-dev.crt -out hako-dev.p12 \
-passout pass:hakodev -name hako-dev -certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1
security import hako-dev.p12 -k ~/Library/Keychains/login.keychain-db -P hakodev -T /usr/bin/codesignThen allow it through the firewall once (the firewall stays on — this is one entry):
H=/full/path/to/hako
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$H"
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$H"The first make after that raises a keychain prompt — answer Always Allow, not "Allow".
Why the certificate rather than the ad-hoc signature codesign -s - gives you: the firewall
matches a designated requirement. With a certificate that is identifier + certificate leaf,
which is stable, so the allow entry survives every rebuild. Ad-hoc signing is a bare code hash,
so every single rebuild invalidates the entry and the phone hangs again. Two flags apart, and
the difference between "set it up once" and "sudo before every test".
macOS notes:
- Signing and the Rez icon are mutually exclusive — a resource fork makes
codesignrefuse the binary — so the signed build carries no icon.make SIGN=0restores the old behaviour. - Loopback (
hako --servewith no--lan) never needs any of this.
Working on the UI itself:
make dev # run the agent with the UI served off disk — edit, reload, no rebuild
make embed # index.html -> ../hako-code/hako_web.h (what the binary carries)
make build # embed, then rebuild the agentOr from hako-code: ./hako --serve --web ../hako-studio serves this checkout, and
make WEB=1 embeds it. A plain make there builds the agent without the UI; hako --serve
then answers 503 and says which of the two to use.
—— III ——
| Area | What it does |
|---|---|
| ☰ chat · code | chat is this browser's own agent; code lists your code sessions (signed in only) and returns to the last one |
| sidebar | working directory · open folder… · settings · sessions in this folder · new session · reload |
| session row | click to resume with full transcript; ✎ renames it; × deletes the stored jsonl |
| approval box | allow once · deny · always — answered boxes stay settled across a reload |
| header | live provider/model and turn count, read from the agent's own output |
| files | browse the machine's folders and files; tap a file to put its name in your message; add from device uploads from the phone's Files/Photos into the open project |
| settings | theme · thinking indicator · extra folders to search for model weights · forget local settings |
Uploads land in the open project under a sanitized basename, never overwrite (collisions become
-1, -2), cap at 128 MB, stream to disk, and announce themselves in the transcript so the agent
knows the file arrived. Add the page to your home screen and it runs chromeless with its own icon.
:login and friends need a terminal — over --serve they answer "run this in the shell" rather
than parking the agent on a stdin nobody can reach.
Type : commands exactly as in the CLI — :trust, :usage, :sessions, :pull <model>,
:help. Bare :model, :provider and :theme open the page's own pickers; :sessions and
the model's own questions arrive as a choice box — tap an option and the agent carries on.
On a desktop Enter sends and Shift+Enter adds a line; on a touch device Enter is a newline and
send sends. While a turn is running that button becomes stop. Under 720px the sidebar is a
drawer, and the UI asks the agent what it supports before offering it — an older hako simply
shows fewer buttons.
—— IV ——
Running from the binary, on that machine's disk:
~/.hako/projects/<encoded-cwd>/sessions/ conversations (shared with the CLI)
<id>.jsonl the transcript, written by hako
<id>.title optional name you gave it (sidecar)
~/.hako/credentials provider keys and OAuth tokens
~/.hako/models/<id>/<id>.mlf2 local weights
browser localStorage theme, spinner, model search paths
browser IndexedDB (hako-account) when signed in: this device's key and your account key
Running in a browser, in that origin's private storage — the same paths, in a place no file manager can open:
/workspace your files, unless a folder is granted
/hako/.hakorc provider and model
/hako/.hako/credentials your API key, on this device only
/hako/.hako/projects/<encoded-dir>/… sessions, exactly as above
Clearing site data erases all of it. Save anything worth keeping out of the files list; on iOS that lands in Files. Granting a real folder (Chromium desktop) puts your work on disk instead, while the key stays on the device and is never written into the folder.
The server holds nothing. It keeps a 2048-event replay buffer in memory so a reload restores
the current view, and that is all — no logging, no history of its own, nothing written to disk
that hako did not write.
With :remote on, the relay holds what the account needs to work and nothing else: a random
account id, your passkey's public key or a keyed hash of your email, the passkey-wrapped account
key, device public keys and their sealed names. The computer keeps its keys in ~/.hako/remote
and <project>/remote (both 0600).
What you send to a cloud model goes to that provider under your own key. Local models go nowhere.
—— V ——
- Loopback by default.
--bindto anything else exposes the agent: anyone who can reach the port can run tools as you.--tokenrequires a secret on every request; without it, use a network you trust or a VPN. --yoloapproves everything, including shell commands and file writes, with no prompt. The approval boxes exist so you don't need it.- Requests must come from this host: the
OriginandHostheaders are matched exactly, so a look-alike name (127.0.0.1.example) is refused. The--tokencookie isSameSite=Strict, and tokens come from the OS entropy source. Session ids are validated against path traversal. - Remote devices get less than the desk. Over
:remote onor--serve --tokenevery tool asks (auto mode is ignored, "always" counts once),:auto,:trust,:mcpand:skill installare refused, and folders stay inside the one hako was started in. hako's own trust gate still applies: an untrusted directory refuses every tool until:trust.- In the browser these two are different by necessity. A wasm turn cannot pause to ask, so the approval gate and the trust gate do not apply there. The boundary is the browser itself: no shell exists, and the only reachable files are this origin's storage or a folder the user granted through the browser's own picker.
—— VI ——
If you share the belief that simplicity empowers creativity, feel free to contribute.
- Forking this repo
- Submitting a Pull Request
- Bug reports and feature requests
Please ensure your code follows the existing style: C99, tabs, snake_case fields, no
third-party libraries, and no agent logic in JavaScript — that belongs in hako.c.
— SEE LICENSE — · GPL-3.0
— deus sol invictus mithras —



