AI
Coding agents as a room: the clients this machine installs, holt giving each agent its own checkout, and the lifecycle around them. Any git repo, any client.
Run two coding agents in the same checkout and they fight: one switches the
branch out from under the other, uncommitted edits collide, and closing a pane
mid-thought can lose work. holt fixes that by giving every agent its own
checkout, and haus puts it on your PATH, wired to a keybind.
Press ⌘A in any repo's terminal tab and you get a fresh agent session in an
isolated git worktree: its own checkout on a worktree-<name> branch, branched
from the repo's local HEAD, living outside the repo under
~/.cache/claude-worktrees/<repo>/<name> so your working tree stays clean and no
build tool trips over it. ⌘⇧A does the same in place of the focused pane
rather than beside it; the pane it replaces is only suspended, so quitting the
agent brings your shell back with its scrollback. ⌃⌥⇧A is the resident
variant: the one agent per tab allowed to edit the checkout you're looking at.
holt calls what the first two make a lane (one agent's branch, checkout and
terminal, as a single thing you can name), and that's the word its output uses.
Not a worktree: a parked lane has no checkout on disk and is still a lane you
can resume. (⌃⌥⇧A makes no lane at all; it's an agent in the checkout you
already have.) Where a lane's terminal lives is the
Development room's call rather than holt's:
haus.hearth.lanes.backend puts it in a zellij pane by default, or in a window
of its own.
It works on any git repo, and it isn't really a haus feature: it's
its own tool, which haus takes as an input
and puts on your PATH. You don't have to be a haus user to run it.
Closing a pane never loses work
Closing a ⌘A pane doesn't just delete the checkout. Uncommitted edits are first
parked as a wip: commit on the branch, and only branches whose work has
already merged get reaped: everything unmerged survives on its branch, and the
agent's transcript survives with it. The checkout is disposable; your work isn't.
Enable it
haus.ai = {
enable = true;
clients = [ "claude" "opencode" ];
default = "claude";
};That brings the clients you named, holt, and the worktree lifecycle wiring.
Nothing else in the house has to be on for it to work.
Resuming
Because the work is on the branch and the chat is in your client's transcript, a
closed pane is never a dead end. Run holt bare to see everything you can pick
back up, across every repo:
🌫 lanes you can resume (holt <name>, or <repo>/<name>)
repo name state agent last commit
nebelung sparkle parked claude 2 hours ago — wip: auto-saved on pane close
pounce quick-emoji live codex 10 min ago — add fuzzy weightsName one and holt rebuilds the checkout and picks the conversation back up through whichever client that lane was made with:
holt sparkle # resume it
holt nebelung/sparkle # qualify when the name exists in two repos
holt sparkle --pick # choose from that client's session list insteadBy default it continues the newest conversation. --pick opens the client's own
picker, for when a lane has more than one.
Spawning from the launcher
For a lane named after the task rather than the random
luminous-twirling-codd an unattended agent would coin, summon
the launcher and run Spawn Agent: pick a repo, describe
the job, and it creates the checkout and opens a session in that repo's zellij
tab. Naming the branch from the prompt is the whole point.
How you commit the prompt picks what happens next:
| Key | Does |
|---|---|
↵ | Spawn on what you typed |
⇧↵ | Newline; a task is usually a list, not a sentence |
⌘↵ | Grab a screenshot first, then spawn; cancel the capture and you're back in the box with your text intact. On an empty box it just attaches the shot and hands the box back |
⌥↵ | Open your drafts; the box is saved on every dismissal, so a paragraph lost to a stray click is still here |
Codex receives the screenshot as an image; Claude Code and OpenCode get its path in the first prompt.
Which client gets spawned
haus.ai.clients = [ "claude" "opencode" ]; # what's installed
haus.ai.default = "claude"; # what ⌘A spawnsai.default must be one of ai.clients, and the rebuild says so by name if it
isn't rather than failing later, inside the pane, after the checkout already
exists. clients defaults to an empty list, which is exempt from that check
(there's nothing to be absent from) and means haus installs no client itself.
All of it hangs off one master switch, haus.ai.enable: holt on your PATH, the
⌘A keybinds, the tab marks, the statusline, the client wiring. Rooms are
independent, so the switch is yours (or your desktop's) to set; hacker turns
it on and names both options above. Turn the room off and ai.clients is ignored
whole rather than half-wiring the machine, so a host can drop the room without
having to blank the list its desktop set.
holt records the client with the lane, so changing the option only affects new
spawns: a parked Codex task still reopens through codex resume --last, and an
OpenCode one through opencode --continue.
Overlay a client, don't add one
Want a patched build of a client? Overlay the package (claude-code,
codex, opencode) rather than putting your own derivation in home.packages
beside it. Two derivations shipping the same bin/claude collide in one profile,
and the rebuild fails with two given paths contain a conflicting subpath.
Never git stash: park instead
git stash looks like it belongs to the checkout you're standing in. It doesn't.
The stash is one ref in the repo's shared .git directory, so every worktree
of a repo, and the main checkout, push and pop the same stack. Two agents
stashing at once means either can pop the other's entry into a tree that has
never seen those files, and stash@{1} is no safer, being positional.
So park instead:
holt park "half-done refactor" # whole dirty tree → one wip: commit on THIS branch
holt unpark # rewind it: changes back, uncommittedholt park commits everything (tracked edits, untracked files, deletions) as a
single wip: commit on the branch only this pane has checked out. Ignored files
are never swept in, so parking can't smuggle a secret into history. It's the same
thing the pane-close hook does, on demand, and holt lists the result as a
resumable lane rather than a numbered entry in a shared pile.
unpark won't rewrite what you've pushed
If the wip: commit has already been pushed (it's visible in an open PR),
holt unpark refuses, because rewinding it locally turns a file restore into a
force-push. It prints the escape hatch (git reset --mixed HEAD^) and leaves the
commit alone. Parking on a branch that isn't an agent branch works, but says so:
a wip: commit shouldn't drift onto a branch you push.
Working on another repo: holt child
Sometimes an agent in one repo has to change another one. Don't reach for a
raw git worktree add: that skips holt's registry, so the statusline never
learns to track the lane and its PR goes invisible.
cd "$(holt child ~/code/other-repo)"That makes a lane on the other repo, registers it as a child of your current
pane so its PR shows up where you're working, and prints only the new checkout
path, hence the cd "$(…)".
Pointing agents at haus itself rather than at your own projects? Contributing covers what's different in a family repo: building a lane's branch without merging it, and the one activation an agent is refused.
Cleaning up
Most cleanup is automatic: closing a pane reaps a merged branch, and every holt
listing self-heals first by sweeping parked branches whose PR has since merged.
Some things end a pane without the graceful hook, though: a reboot, a crash, or
a holt child checkout, which no pane owns. For those, sweep on demand:
holt reapholt reap is deliberately conservative. It leaves anything dirty, anything
unmerged, and any checkout a pane is still open in: the one you're
standing in included, and any other session's too, because a just-merged branch
looks exactly like a finished one while its agent is still sitting in it. Only
landed work nobody is standing in gets removed.
Every deletion is written to a ledger before the branch dies, so a sweep is
never the end of anything: holt reaped prints what went, why, and the
git branch line that brings it back. And a lane that will never land (its
PR closed unmerged, the repo archived) is holt drop <name>, which waives the
merged-branch requirement and nothing else.
When a session outran its PR
You merge the PR, keep talking to the agent, and it makes a few more commits. Those commits are now in an odd place: the PR that covered them is closed, and GitHub deleted the branch's remote counterpart when it merged, so they're committed, unpushed, unreviewed, on a branch nothing is watching.
holt never reaps such a branch, and it says so everywhere: the listing marks the
row live+3 (three commits past the merge), holt reap names it instead of
sweeping it, and the statusline shows an orange 3^. One command clears it:
holt reship # push this branch, open the follow-up PR
holt reship sparkle # …or name one, from anywhereIt re-creates the remote GitHub deleted and opens a PR against the default branch, listing what's new. If a PR is already open, it just pushes to it.
The lookalike is marked differently and wants the opposite treatment: ~3
means the branch has diverged: its tip doesn't build on what merged, so the
checkout is stale rather than ahead. holt reship refuses it. Remove that lane
instead of pushing it.
Knowing which agent needs you
Run agents in eight tabs and the question is always the same one: which of them is sitting on a permission prompt? The zellij tab bar answers it, with a coloured dot in front of the name of any tab holding agent panes:
| Mark | Means |
|---|---|
● peach, and the tab washed peach | an agent is blocked on you: a permission prompt, or a question |
◐ sky | an agent is mid-turn, working |
○ green | it finished its turn; nothing is waiting |
| (nothing) | no agent panes in that tab |
The dot shows the most urgent state in the tab, so peach always wins and the tab that needs you never hides behind one that's merely busy. (The peach wash is dropped on the tab you're already looking at.) It's the shape that carries the state, not the colour, and there's no count: how many agents a tab holds is the bar's popup to answer, and the bar is the same signal for when you're out in the GUI rather than in the terminal.
Both are reported by each client's own lifecycle hooks: the agent says what it's doing; nothing scrapes the screen for it. haus wires OpenCode's plugin and Codex's hooks for you. Claude Code's four are yours to wire, and haus deliberately leaves them alone.
What haus does write into `~/.claude/settings.json`
The worktree hooks that Claude Code's own --worktree flag fires:
WorktreeCreate and WorktreeRemove, pointing at holt hook create /
holt hook remove, plus the statusline, permissions.defaultMode and a few TUI
keys. It merges into the file rather than replacing it, touches no other hook
event, and re-asserts on every rebuild, because Claude Code rewrites its own
settings on its own schedule and a hand-written hook can go with them. You'd find
that out at pane close, by losing a lane's parking.
The statusline HUD
haus points Claude Code's status bar at a small HUD driven off holt's registry, so
you can see where every agent stands without leaving the pane. Row one is this
session's own lane: a status token, its GitHub PR pill, then its name. The
rows below are the lanes this session spawned, across whatever repos they live
in, eight at most with a +N more line under them. Rows that still need
attention sort above landed ones, so a clipped HUD can only ever hide something
already reapable.
The status token is one mutually-exclusive slot:
| Token | Means |
|---|---|
⏏ | the branch has landed; closing the pane reaps the lane |
N^ | N commits on the branch, not merged yet |
N^ (orange) | the PR merged and N commits landed since, covered by nothing; holt reship |
+A -D | uncommitted line changes, when nothing's committed yet |
● (muted) | nothing to report: clean tree, nothing ahead |
Flush right on row one sit a few per-session chips: how far behind your pinned
revision you are (⇡N, what haus update would pull), context used, session
cost, the permission mode, and a short model tag. Context is coloured by token
count, not percentage, because the percentage means different things on a 200k
and a 1M model. Permission mode follows the same "no news is good news" rule as
the status token: the default shows nothing, everything else is a positive mark.
Command summary
| Command | What it does |
|---|---|
holt | List every parked and live lane across all repos (self-heals first) |
holt <name> | Resume one: rebuild its checkout, pick its conversation back up |
holt reap | Sweep every landed lane now (keeps dirty, unmerged, and any open pane) |
holt reaped | The ledger of what was swept, why, and the one line that brings a branch back |
holt drop <name> | Retire a lane that will never land: a PR closed unmerged, an archived repo |
holt child <repo> | A lane on another repo, as a child of this pane |
holt park [label] | Set the dirty tree aside as a wip: commit; use instead of git stash |
holt unpark | Undo it: changes back in the working tree, uncommitted |
holt reship [name] | The PR merged and the branch kept going: push it, open the follow-up PR |
holt new [name] | A lane on this repo, with the default client opened in it |
holt spawn <repo> <name> | A named lane for a spawner with no pane of its own; what Spawn Agent calls |
Add --json to the listing for a machine-readable version, --version for
holt's own.
Works with
Three contributions, each appearing only when its receiving room is on, and none of them switching that room on for you:
- Development: the agent keybindings and pane lifecycle inside the multiplexer;
- Bar: the agents pill, amber when a session is waiting on you;
- Launcher: Spawn Agent, which names the worktree after the job.
Remove it
haus.ai.enable = false;The clients, holt and the wiring go, including from a desktop that named
clients, because the room's switch is what decides, not the list. What stays is
everything on disk: the worktrees you created and their branches are ordinary
git, and removing the room never deletes work.
Options
Every setting, with types and defaults: AI.
Focus
One quiet switch (bar pill, palette command and CLI) for macOS Do Not Disturb, your Slack status, and your own hooks on both edges.
Changing your Mac with an agent
Your machine is text and every rebuild is reversible, so an AI agent can reconfigure it safely: the skill haus ships, the loop it follows, and the one rebuild it refuses.