Run persistent ACP agents in isolated containers on a box you own.
One agent is one file. hived makes reality match the files. Each agent gets its
own container, its own network, and its own credentials — and the credentials it
needs at runtime are held by a broker rather than baked into the container.
Buzz-first, because that is the surface this was built against. ACP-shaped underneath, because that is where the portability is.
# /etc/hive/agents/scribe.toml — commit this; it holds no secrets
[identity]
pubkey = "8f3c…"
relay_url = "wss://relay.example"
owner_pubkey = "a91b…"
[harness]
id = "claude"
[agent]
respond_to = "owner-only"
[[mcp]]
name = "parachute"
transport = "http"
url = "https://vault.example/mcp"
credential = "mcp/parachute" # a NAME, resolved by the broker$ hive secret put nsec/scribe < agent.nsec
$ hive secret put harness/claude < oauth-token
$ hive secret put mcp/parachute < vault-token
$ hive statusBuzz ships no runtime, deliberately — it provides identity and a surface, and
says agents "can run on your laptop, in the cloud, or at the edge". The runtime
is left to you. BackendKind::Provider is the seam for one, and upstream's own
issue text notes there is no open-source implementation of it.
The nearest prior art is Paradigm's Centaur: Kubernetes, per-thread rather than per-agent, Slack-only. Everything else in the space is either an ephemeral code sandbox (E2B, Modal, Cloudflare) or a vertically integrated product with no reusable layer.
hive is the small version: one box, no orchestrator. If it needs a cluster, it has failed.
- A container per agent, with its own network. Agents cannot reach each other, your other Docker networks, or other hosts on your private network.
- Nine ACP harnesses in one image — claude, codex, goose, grok, opencode,
kimi, amp, omp, cursor — all pinned, and all verified to answer an ACP
initializeon the architecture the image was built for. - A credential broker. MCP credentials are served per-connection over a per-agent unix socket and never enter the container.
- Declarative specs. An agent is a TOML file. Reconciliation is idempotent, and the plan is a pure function you can read.
# on the host
git clone https://github.com/Unforced-Dev/hive && cd hive
./images/agent/build.sh # builds the agent image (~4 GB)
cargo install --path crates/hived --path crates/hive-cli
sudo mkdir -p /etc/hive/agents /var/lib/hive/secrets /run/hive
sudo hived --once # one reconciliation pass, then exit
sudo cp packaging/hived.service /etc/systemd/system/ && sudo systemctl enable --now hivedhive doctor checks the things that are usually wrong.
On macOS the daemon runs in a container, because it has to. hived
bind-mounts a per-agent unix socket into each agent container, and a socket
created on the macOS side of Docker's Linux VM cannot be connected to from
inside it. A native hived would look healthy while every broker-delivered
credential failed. DECISIONS has the detail.
Works with any Docker on macOS. Colima is a good fit for an always-on box since it needs no GUI session:
brew install colima docker
colima start --vm-type vz --vz-rosetta --mount-type virtiofs --cpu 8 --memory 8
# The socket directory lives in the VM. /run is tmpfs, so this does not survive
# a VM restart — recreate it whenever Colima restarts.
colima ssh -- sudo mkdir -p /run/hive && colima ssh -- sudo chmod 755 /run/hive
docker build -f images/hived/Dockerfile -t hive-daemon:latest .
docker run -d --name hived --restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /run/hive:/run/hive \
-v hive-agents:/etc/hive/agents \
-v hive-secrets:/var/lib/hive \
hive-daemon:latest
docker exec hived hive status-v /run/hive:/run/hive is path-matched deliberately; mounting it anywhere else
inside the container breaks agent socket mounts in a way that only shows up
inside the harness.
hive talks to the daemon over a control socket, which is inside the VM for the
same reason the daemon is — so the CLI has to run on that side too. Install the
wrapper rather than typing docker exec at every command; the container is an
implementation detail and should not be part of the interface:
$ install -m 0755 packaging/hive-wrapper.sh ~/.local/bin/hive
$ hive status
$ hive secret put nsec/scribe < agent.nsecCredentials go in over stdin, so they never reach shell history.
Then point the Buzz desktop shim at the container rather than at an SSH host —
set hived container to hived and leave hive host blank.
Docs: ARCHITECTURE — what it is and how it works · DECISIONS — why, and what was rejected · TESTING — a hands-on walkthrough against a real relay
$ hive shell <agent> # exec into the running agent
$ hive shell --scratch <agent> # side container on the agent's volumes
$ hive restart <agent> # drop the container; hived recreates it--scratch is the one for an interactive login (codex login,
claude setup-token). It starts a separate container from the same image with
the agent's state volume and its shared volumes mounted, on the agent's
network, with no relay connection — so the reconciler cannot replace the
container underneath you mid-flow, and it works when the agent is crash-looping
and exec would fail. Anything written under /home/agent/state persists;
hive restart makes the harness pick it up.
Each agent gets its own Docker volume at /home/agent/state, and every
harness state directory lives inside it — claude, codex, kimi, grok,
amp, cursor, omp, opencode, plus config, data and work. Harnesses
that hardcode $HOME/.foo get a symlink into the volume, made by the entrypoint.
So an agent's skills, credentials and history are its own. Nothing is shared by
default. The one shared path is /opt/grok, which is the grok binary —
root-owned, read-only to agents, and 127 MB you do not want copied per agent.
To share deliberately, name a volume:
[[volume]]
name = "uni-workspace"
target = "/home/agent/work"Two agents naming the same volume edit the same tree while keeping separate
skills and credentials. Validation refuses a target inside /home/agent/state,
because mounting a shared volume over private state is the exact failure this
design exists to prevent.
hive never generates a key — you always supply one (hive secret put nsec/uni),
which is also how the Buzz shim ships the key the desktop already made.
buzz-acp takes a scalar BUZZ_RELAY_URL, so the same agent in two communities
is genuinely two containers and therefore two specs. They are still ONE identity,
and the private key should exist in exactly one place — name it explicitly:
# uni.toml
[identity]
pubkey = "8f3c…"
relay_url = "wss://home.example"
# uni-other.toml — same pubkey, same key, different relay
[identity]
pubkey = "8f3c…"
relay_url = "wss://other.example"
credential = "nsec/uni" # defaults to nsec/<file-name>Without credential the key name follows the file name, and you would store the
same private key twice — where one copy goes stale the first time you rotate it.
Each relay-instance still gets its own container, network and state volume. Share
files between them with a [[volume]] if you want that; skills and harness state
stay separate unless you say otherwise.
The same identity on the SAME relay is refused. Two processes would answer as one agent: every mention gets two replies, both charged to the owner, interleaved in the thread — which reads as the model repeating itself rather than as a deployment mistake. Both specs are held, never removed, so adding a bad file cannot tear down an agent that was already running.
Edit the spec. hive replaces the container; the agent keeps its pubkey, its state volume and its files, and thread history lives on the relay rather than in the container — so the conversation survives. Both harnesses' state persists side by side in the volume, so switching back and forth is free.
hive (CLI) ─────┐
├──► hived ──► hive-core ──► Docker
buzz-backend- │ │ (no secrets)
hive (SSH) ───┘ └──► hive-broker ──► secrets
│
└── per-agent socket ──► hive-headers
(in container)
hive-core and hive-broker do not depend on each other. Core defines a
CredentialSource trait; the daemon wires the broker in. A bug in reconciliation
cannot read the credential store.
| crate | what it is |
|---|---|
hive-spec |
spec types and validation. No I/O. |
hive-core |
harness catalog, container backend, network policy, reconciler |
hive-broker |
the only component that sees secrets |
hive-headers |
tiny helper that runs inside the container |
hived |
the daemon |
hive-cli |
hive |
buzz-backend-hive |
Buzz desktop provider shim |
Being straight about this is more useful than a longer feature list.
Docker is not a security boundary against hostile code. It is a namespace boundary. hive is the right tool for "my own agents, which I do not want reaching each other or my network" and the wrong tool for running code from someone who wants in.
"Credentials never enter the container" is only true for MCP servers, and only
on Claude Code. A harness authenticates to its own model API and there is no
hook to intercept that, so model credentials are injected. headersHelper is
MCP-specific. What hive offers is a smaller blast radius, not zero. Three tiers,
worst to best:
| delivery | used for | docker inspect sees it? |
|---|---|---|
| env | CLAUDE_CODE_OAUTH_TOKEN, XAI_API_KEY |
yes |
file ([[file]]) |
codex auth.json — no env form exists |
no |
broker ([[mcp]]) |
MCP tokens, Claude only | never enters the container |
Secrets are 0600 files, not encrypted. Encrypting them with a key stored on the same disk protects against nothing an attacker who can read the files cannot also do. The boundary is file permissions and root. If that is not enough for you, the answer is a KMS or a hardware token, not a local key file.
Egress rules are printed, not applied. hive firewall <agent> emits the
iptables rules with an explanation of each; you review and run them. hive does
not silently rewrite your firewall.
br_netfilter. If it is absent — and it is on many hosts — container-to-
container traffic on a shared bridge never traverses iptables, and no firewall
rule can block it. hive gives each agent its own network with
com.docker.network.bridge.enable_icc=false, which is the only control that
works in that world. If you test isolation, probe a peer by raw IP: testing
by name only proves DNS is not resolving, and reports success on a wide-open
network.
Docker publishes ports with DNAT, and it happens before the filter chains. A
rule written against a published port matches nothing, while the service keeps
answering through some broader rule — so iptables -L looks correct and the
counter sits at zero. Read the packet counters, not the rules. hive firewall
emits --ctorigdstport with --ctdir ORIGINAL for that case, and plain
--dport for a host process, because the two are genuinely different.
cargo test --workspace # unit tests, no daemon needed
./build.sh # build + test in a pinned container
./build.sh --docker # also run integration tests against a real daemonThe Docker integration tests create and destroy real containers. They exist because the unit tests verify what hive decides, and most of the expensive bugs here have been in what Docker actually does.
Tests are named after the failure they prevent, not the function they cover —
observer_defaults_on_because_remote_agents_are_otherwise_invisible,
a_crash_looping_agent_is_held_not_recreated. Almost every one of them is
scar tissue from something that went wrong on a real box.
hive is not a fork, a competitor, or a patch waiting to be upstreamed. It sits under Buzz in the stack and is deliberately separable from it.
Buzz is moving fast — ~850 commits a month from a full-time team — and its energy
is in the surface: the desktop client, search, invites, threads, plus first-party
agent capability (buzz-agent, buzz-workflow). Hosting is explicitly out of
scope; the docs say agents "can run on your laptop, in the cloud, or at the edge".
Two things follow. First, the seam hive fills is real and has stayed open: as of
v0.5.0 BackendKind::Provider still has no open-source implementation, and
buzz-acp's McpServer is still {name, command, args, env} — stdio only, no
url, no headers — so HTTP MCP servers remain unreachable through the protocol.
That is why hive writes harness config files directly, and that part should
shrink if upstream adds the variant.
Second, an agent host wants to outlive any one surface. hive-core knows about
ACP, Docker and credentials; "connect to Buzz" is one adapter and one env-var
mapping. If a second ACP surface appears, hive follows it.
Early. It runs, it is tested, and it has not been run by anyone but its authors. Interfaces will move.
Apache-2.0.