The Hackable AI: the agent built into hos. An agentic loop in C that talks to any OpenAI-compatible model, operates the machine with a small set of tools, and talks to you by voice.
hai say "what is eating the disk?" # one question, reply streamed
hai # a REPL
hai talk # push to talk (bind to a key)
Run hai inside a project directory and you get a separate agent for
that directory: it works there, reads its AGENTS.md or CLAUDE.md,
keeps its own conversations, and knows the main assistant is its
parent (it can read main's sessions and mail it). hai quit there
ends it; hai -s main ... reaches main from anywhere.
haid is the daemon: it holds the conversation, calls the model, runs
the tools (shell, files, notifications, speaking, asking you), keeps a
transcript, and summarizes when the conversation grows long. hai is
the client. They talk over a unix socket with a line protocol, so a
key binding, a terminal and a script can all drive the same session.
Any chat-completions endpoint with tool calling: OpenAI, Anthropic's compatibility endpoint, OpenRouter, Ollama, llama.cpp, vLLM.
# ~/.config/hackable/hai.conf
url = http://localhost:11434/v1
model = qwen3:8bor HAI_URL, HAI_MODEL, HAI_API_KEY in the environment; with no
key set, OPENAI_API_KEY is used. keycmd = pass show openai fetches
the key from a password store. haid --check prints what will be used.
Those settings are the model called default; give the others a
section each and switch between them while hai runs. Or keep every
model in a section and name the one to start on:
model = local
[model local]
url = http://hhs:11435/v1
model = qwen3.8:27b-128k
[model deepseek]
url = https://api.deepseek.com/v1
model = deepseek-chat
keycmd = printf %s "$DEEPSEEK_API_KEY"
timeout = 180
budget = 180000A section states only what differs (its model id defaults to the
section name) and may set url, model, key, keycmd,
temperature, timeout and budget. hai model lists them with the
one in use marked, hai model deepseek switches — the conversation
carries over, and everything after that runs on the new endpoint until
you switch back or the daemon stops. With a top-level model naming a
section there is no separate default entry; a section called
default takes that place instead. /model does it in the REPL
(tab completes the names), and a name works wherever a model is named:
HAI_MODEL=deepseek haid, childmodel in the config, a model given
to delegate. A name no section matches is a plain model id, as
before.
hai restart (or /restart) starts haid over with hai.conf and the
binary read afresh and the conversation resumed — after editing the
config or rebuilding; it keeps its pid, so a supervisor sees nothing.
Child agents die with it.
haid is a plain foreground daemon that logs to stderr, so any
supervisor runs it: a run script of exec haid -r (-r resumes the
newest conversation after every restart) is all one needs. It is not
tied to any of them.
The REPL is attached to the conversation, not to what it types: it opens on the conversation so far, then shows every run as it happens
- a message you typed, one that came by mail, one from another
terminal or a child agent reporting back - each under its sender's
address (the prompt is yours,
user@hai>, so the screen reads like a chat), and a secondhaion the same agent (orhai -s scout) shows the same thing, joining a run already under way where it is./open IDshows that conversation the same way. Typing while the agent works queues the line as the next message; when the agent asks something, the prompt turns into?and the next line is the answer;^Cstops the run. What you type is mail like everything else: written into the agent's own inbox, picked up in order with whatever else arrived, and stored as a turn of the conversation (a new one when there is none).
The REPL edits its line (arrows, emacs keys, history on up/down,
tab completion of /commands, session ids, tool and skill names),
!cmd runs a shell command locally, and /verbose expands the tool
calls (one line each otherwise) and shows their output, redrawing the
last few screens that way. With
bat installed, shell commands, file
edits, permission previews and the code blocks in a reply are syntax
highlighted; without it they are colored plainly.
Read-only commands (ls, cat, grep, git status, ...) run
without asking; anything else, and writing files, is confirmed with
you first, a file change shown as a diff: in the terminal, by voice, or with a yes/no menu (hmenu) if
neither is around. A deny list (rm -rf /, mkfs, shutdown, ...)
never runs. Answer always and that kind of command (or that tool)
stops asking for the rest of the session. hai mode auto (or /mode auto in the REPL) turns asking off altogether; hai mode plan puts
the agent in plan mode, where it investigates and writes a plan but
every change is refused; hai mode ask is the default, and hai mode
alone cycles through the three. haid -y (or autonomous = 1) starts
in auto. Extend
the lists in hai.conf:
[allow]
cmd = docker ps
cmd = systemctl status
[deny]
cmd = git push --forceSpeech to text is hstt (whisper.cpp,
local). Text to speech is piper (local): make voice fetches the
binary and an English voice into ~/.local/share. hai talk starts
recording, hai talk again stops it and hai answers aloud; questions
it asks are answered the same way. hai cancel interrupts anything.
Notifications follow along (listening, transcribing, what was heard,
the reply; never a tool call — a shell command does not pop up);
hai notify (or /notify) toggles them,
notify = 0 in hai.conf starts without, and asking hai aloud to stop
them works too.
hai has an address. Whatever is mailed to main@hai (by you, a script,
a cron job, a child agent) lands in the Maildir ~/.mail/hai/main
and starts a run as soon as haid is idle - the same way a line typed
into hai does. The reply comes back by mail to user@hai, and
anything hai would have had to ask you is left there as a question
instead of being done, unless a terminal is attached: then it shows
the run and gets the question, and the mailed reply is a copy.
Local addresses never leave the machine: hml send delivers @hai
into that Maildir tree and everything else over SMTP. Any
sendmail-shaped command works (sendcmd in hai.conf); reading the
inbox needs no hml at all, it is a directory.
printf 'Subject: disk\n\nWhat is eating the disk? Mail me a summary.\n' | hml send main@hai
hml search to:user@hai tag:unread # what hai left for you
hml show -- thread:<id> # the exchange, threadedThe send_mail tool gives hai the same channel: @hai addresses are
free, anyone else is real email and is confirmed with you first.
hai can also hand work to child agents: delegate starts a second
hai at scout@hai (its own Maildir ~/.mail/hai/scout/: its inbox
and its conversations), mails it the task, and gets the report back
by mail, as the tool result or, without waiting, as a later inbox
turn, so several children can work in parallel. The same name again
continues that child's conversation. Children delegate too: scout's
own child probe is scout.probe@hai, so an address tells you who
spawned whom (childdepth levels at most). A child can be sent to
work in a directory, where it also picks up the project's
AGENTS.md or CLAUDE.md and any skills in .hai/skills/, and can
run on a different model than its parent (childmodel sets the
default for all of them). Children die with the parent or on hai new; hai status lists them, hai tree every agent with what it is
on and whether it is asking.
There is one tree, and main is its root: every agent on the machine
runs with main's leave. A parent forks its own children (so they die
with it, and hai new kills them), but only with a ticket from main
- main issues itself one, a child asks over main's socket and only for
a child of its own name - and a newborn haid's first act is to present
the ticket to main, which also checks it was forked by the agent that
asked; a
haid --childstarted any other way stops on the spot, and a second haid on the same inbox is refused (the inbox is locked).hai agentsprints main's registry: every agent alive with its parent. Every agent's prompt names its parent and the root, andhai status/hai whoshow both.
By default main is a dispatcher (dispatch in hai.conf): quick
things it does itself, every job goes to a child named for it
(delegate never waits), and main answers at once with who took it,
so it is free for your next request while the work runs. A report
arriving later is an inbox turn: main tells you the result in a line,
mails it to user@hai and shows it as a notification. A question a
child asks - the ask tool, or a tool that needs confirming - travels
up by mail until someone can answer: a terminal attached to that
child, else its parent's, else main's desktop menu (hmenu); the
answer goes back down the same way. hai questions lists what waits,
hai answer TEXT answers the first, hai -s NAME answer a
particular child's directly. hai who says who is on the
conversation: the agent and you, the session's id, subject and files,
who wrote into it, the terminals attached and where replies go, the
group addresses several agents listen to (a message there runs on
whichever is free), and the agents above and below. Tell hai to be quiet for a while (a
meeting) and it runs hai silent 1h (a duration, or a time like
15:00; hai silent off ends it): notifications, popups and
children's questions are held until then, what was held shows as one
notification after, terminals attached still see everything, and a
voice exchange you start yourself is still spoken.
Conversations are mail too: every agent has one Maildir,
~/.mail/hai/<name>/, its inbox in new/ and every turn of every
conversation a message file in cur/, one thread per conversation -
readable in any mail client, searchable with hml, and nothing is lost when haid restarts (haid -r resumes
the newest, hai sessions and hai open ID switch). hai new (or
/new) starts a fresh one and keeps the old on disk; hai forget
(/forget) ends the live conversation and deletes it, hai forget ID deletes an older one. Delete a single message in your mail client
(or hml tag +deleted) and hai no longer sees it; hml recv removes
it. Reply to one of hai's mails from anywhere and the follow-up lands
in the same conversation. MAIL.md has the
format.
Built in: shell, files, speaking, asking, notifications, mail sending,
skills, delegating to child agents. Bundled as scripts in tools/:
git, search (ripgrep), diff, browse (hweb), mail (hml),
packages (xbps), windows, show, agents (the
other hai agents: who is alive, their status, turns and sessions). A tool is any executable that prints a JSON
schema for --schema, reads its arguments from HAI_ARG_*, prints its
result, and exits 3 with a question when it wants the user's yes first.
Drop your own into ~/.config/hackable/hai/tools/ or a project's
.hai/tools/.
MCP servers over HTTP join the same way: one section per server in
hai.conf, and its tools appear as NAME_<tool>:
[mcp talent]
url = https://talent.example.com/mcp
headercmd = printf 'Authorization: Bearer %s\n' "$TALENT_KEY"
readonly = whoami search_pool get_personheader = Name: value lines go along verbatim; headercmd runs a
command whose output lines are headers, so a token can come from the
environment or pass. Tools the server marks read-only run at once,
as do the ones named in readonly; the rest are confirmed like any
other change (always remembers). The server's own usage notes go
into the system prompt. haid --check lists the servers and what
they offered.
~/.config/hackable/hai/soul.md is who your hai is: plain markdown
you write - its voice, what it puts first when rules collide, what it
calls you, what it refuses even when allowed, what it does when
unsure. Twenty lines is plenty. It goes into every system prompt right
after the identity, so it colours how the agent speaks and decides
without being able to switch the operating rules off. An agent working
in a directory (a terminal agent, a delegate sent there) also reads
that project's .hai/soul.md, after yours. Edit either and it holds
from the next conversation (/new) or restart; each session records
the soul it ran with in its stored system message. The one thing the
agent may never do is change it: write_file, edit_file and any
shell command naming a soul file are refused, allow list or not.
haid --check shows which soul files were found; soul in hai.conf
moves the user's one.
skills/*.md are notes hai loads when a task needs them (how hos is
laid out, packages, the desktop, mail, the browser). Drop
your own into ~/.config/hackable/hai/skills/ - or ask hai to write
one: the skills skill tells it the format, where the file goes and
what makes a good one, so "remember how we deploy this for next time"
becomes a skill file. It is in the index from the next conversation
(/new).
make # needs libcurl headers (libcurl-devel on Void)
make check # tests, no network
make install # symlinks into ~/.local/binC11, libcurl, vendored cJSON and stb_ds. Configuration is compiled in
(config.h) with the conf file and environment overlaid.