9.1 KiB
hyperhive
a swarm of claude-code agents, each in its own nspawn cage, gossiping over unix sockets. config changes flow as git commits, the operator approves them in a browser, every deploy is a tag. cyberpunk-themed dashboard included. 💜⚡
A host-side Rust daemon (hive-c0re) spawns nspawn-isolated agent
containers, runs each one's claude turn loop, and brokers messages
between them. A privileged manager agent (hm1nd) drives the swarm —
proposing new agents, editing their NixOS modules, escalating
ambiguous decisions to the operator. Every lifecycle change (spawn,
config edit, destroy) is gated on a human ◆ APPR0VE click in the
dashboard. Every approved change lands as a fast-forward on a git
tag, so the deploy history is just git log.
Why this exists: claude code is great in one window. claude code is exponentielle across many — but only if you can keep the agents from stepping on each other, give them durable identity across restarts, and stop them from eating production. hyperhive is the substrate: identity = unix socket, communication = sqlite-backed broker, config = git, deploys = tagged commits, blast radius = container. The operator stays in the loop without becoming the bottleneck.
host (NixOS, runs hive-c0re.service)
│
├── operator
│ ├── browser → :7000 hive-c0re dashboard (containers, approvals)
│ ├── browser → :8000 / :8100-8999 per-agent web UIs (live SSE, send, login)
│ └── CLI → /run/hyperhive/host.sock JSON-line admin protocol
│
├── hive-c0re (Rust daemon)
│ ├── lifecycle nixos-container CRUD + per-agent flake generation
│ ├── broker sqlite messages + tokio broadcast (powers SSE + wake-ups)
│ ├── approvals sqlite queue, two kinds: ApplyCommit (config) + Spawn
│ ├── auto_update rebuilds any container whose recorded flake rev is stale
│ ├── dashboard axum HTTP + async-form actions + SSE message flow
│ └── sockets /run/hyperhive/{host,manager,agents/<n>}/mcp.sock
│
└── nixos-containers (each bind-mounts its socket dir → /run/hive,
│ credentials dir → /root/.claude,
│ durable notes dir → /state;
│ manager additionally gets /agents RW,
│ /applied RO (deployed-tag mirror),
│ /meta RO (swarm-wide deploy flake))
│
├── hm1nd hive-m1nd serve : claude turn loop +
│ MCP (send / recv / request_spawn / kill / start /
│ restart / update / request_apply_commit /
│ ask / answer / remind) + web UI on :8000
│
└── h-<name> hive-ag3nt serve : claude turn loop +
MCP (send / recv / ask / answer / remind + agent-declared
extras via hyperhive.extraMcpServers) + web UI
on a hashed :8100-8999
The turn loop
One message in, one turn out. The harness pops a single inbox message
(Recv long-polls server-side, wakes the instant a broker Sent
event fires) → builds a wake prompt → spawns
claude --print --continue --output-format stream-json --mcp-config …
→ streams JSON events into the per-agent SSE bus + a sqlite history db
→ claude drives any further recv/send itself via the embedded MCP
server. --continue preserves the prior session so context spans turns
without rebuilding from history every wake. If the turn dies mid-stream,
the next wake picks up clean — sessions are durable but cheap to discard.
Operator surfaces
Per agent (:8100-8999): terminal-themed live tail with a
textarea prompt; slash commands /help /clear /cancel
/compact /model <name> /new-session; granular state badge
(idle / thinking / compacting / offline) with age timer +
last-turn duration chip + model chip; cancel-turn + new-session
buttons in the state row; sticky-bottom auto-scroll with "↓ N new"
pill; event history backfilled on page load; collapsible inbox +
collapsible journald viewer + collapsible agent.nix viewer per
agent on the dashboard; deployed-sha chip per container (read
from meta's flake.lock).
Dashboard itself (:7000): the swarm-wide view — every
container with status chips, every pending approval with inline
diff, every open question with operator-answer affordance, every
recent message on a unified live stream. A terminal compose box
under the message-flow lets the operator drop messages into any
agent's inbox: @name picks the recipient with auto-complete
from the live container list, sticky across sends, POSTs
/op-send which lands in the broker as
{from:"operator", to:<name>, body} — same shape any sub-agent
sees as a regular inbox message. No special channel, no
out-of-band notification system. If the operator says it, the
broker carries it.
The config-edit loop
Inverted flow — agents propose, the operator disposes. The manager
edits files under /agents/<name>/config/ — agent.nix is a plain
NixOS module function
{ config, pkgs, lib, ... }: { ... }, and arbitrary sibling files in
the commit are preserved → commits → submits the sha via
request_apply_commit. Hive-c0re immediately fetches that commit from
the proposed repo into the applied repo and pins it as proposal/<id>
— immutable from the manager's side from then on. Operator clicks
◆ APPR0VE → hive-c0re fast-forwards applied/<n>/main to the proposal,
runs nix flake lock --update-input agent-<n> against the host-wide
meta flake at /var/lib/hyperhive/meta/, builds via
nixos-container update <c> --flake meta#<name>, and either commits
the lock + tags deployed/<id> on success or git restores the lock +
annotates failed/<id> with the build error + rolls back
applied/<n>/main on failure. Denials leave a denied/<id> annotated
tag carrying the operator's note.
Meta's git log is the swarm-wide deploy audit trail (one commit per
successful deploy). Per-agent applied repos carry the tag-rich state
machine for inside-baseball decisions. The manager sees both —
proposed repos ship with an applied remote pre-wired, and /meta/
is RO-bound inside the container — so git fetch applied,
git show applied/refs/tags/deployed/<id>, git log /meta,
cat /meta/flake.lock all just work without constructing paths by
hand. See docs/approvals.md for the full state
machine + lock-flow walkthrough.
Structured Q&A
Agents don't have to guess. ask(question, options?, multi?, ttl_seconds?, to?) queues a structured question — default recipient
is the operator (dashboard renders a free-text / checkbox / radio
form), or pass to: "<agent>" to route the question into a peer
agent's inbox instead. The answer arrives later as a
HelperEvent::QuestionAnswered { id, question, answer, answerer }
in the asker's inbox; peer recipients respond via answer(id, answer).
Plus remind(message, delay_seconds | at_unix_timestamp, file_path?)
for self-scheduled wake-ups when an agent wants to nudge itself later
without holding a connection open.
Host config
Minimal flake.nix for a host that runs hive-c0re:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
hyperhive.url = "git+https://git.berlin.ccc.de/vinzenz/hyperhive";
};
outputs = { nixpkgs, hyperhive, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
hyperhive.nixosModules.hive-c0re
({ ... }: {
services.hive-c0re.enable = true;
# Free-text operator pronouns — defaults to "she/her", threaded
# through to every agent's system prompt as HIVE_OPERATOR_PRONOUNS
# so claude refers to you naturally in third person.
# services.hive-c0re.operatorPronouns = "they/them";
# ... rest of your host config (hardware, networking, users, …)
system.stateVersion = "25.11";
})
];
};
};
}
hive-c0re will then:
- open its admin socket at
/run/hyperhive/host.sock+ dashboard on:7000, - auto-create the manager container (
hm1nd) if missing, - auto-rebuild any managed container whose hyperhive rev is stale.
claude-code is unfree; hyperhive whitelists it for itself
(scoped: only claude-code, nothing else) inside the
claude-unstable overlay and harness-base.nix. Per-agent
containers evaluate their own nixpkgs instance so the operator's
host-level allowUnfree doesn't propagate in — the predicate has
to live inline. Nothing to set on the operator side.
Build / deploy
# inside the repo (devshell first; no global cargo)
nix develop -c cargo check
nix develop -c cargo clippy --workspace --all-targets -- -D warnings
# evaluate everything (rust+nix+toml fmt + clippy)
nix flake check
# deploy to a host that imports `hyperhive.nixosModules.hive-c0re`
cd ~/Repos/<nixos-config-repo>
nix flake update --update-input hyperhive
sudo nixos-rebuild switch --flake .#<host>
No overlays on the host's pkgs — the module pulls hive-c0re's package
straight from hyperhive.packages.<system>.default. Just import the
module and the service is wired up.