Hierarchical Claude Code containers with one curated bridge to your Mac.
MacBook ────────────────────────────────────────────────────────────────
│ hive CLI · VS Code attach · config/ (the two curated files)
│
│ docker ┌────────────────────────────────────────────┐
│ socket ──────► │ root control plane (docker CLI + │
│ │ hive CLI + Claude Code) │
│ │ ├─ node a ├─ node b │
│ │ │ └─ node a2 │ │ hive-net
│ │ (any depth, tracked via labels) │ (internal —
│ └───────────────────┬────────────────────────┘ no way out)
│ │ only crossing point
│ ┌─────────┴─────────┐
│ host.docker.internal ◄── │ bridge │ ──► internet
│ (only what's listed in │ squid 3128 │ (allowlisted domains for
│ config/forwards.conf) │ socat forwards │ sealed nodes; everything
│ └───────────────────┘ for nodes you `net on`)
Three guarantees, all enforced by Docker networking rather than convention:
- Nodes are sealed. Every node sits on
hive-net, aninternal: truenetwork. No internet, no Mac, not even with root inside the container. - The bridge is the only door, and you curate both sides of it. Outbound HTTP(S) goes through a domain allowlist (config/egress-allowlist.txt); access to your Mac exists only as the explicit TCP forwards in (config/forwards.conf).
- Only
rootholds the Docker socket. It is the one container that can spawn or destroy others — a Claude instance inrootcan orchestrate the whole tree, while Claude instances in nodes can't touch the hierarchy.
alias hive="$PWD/bin/hive" # or add bin/ to PATH
hive up # build + start root and bridge
hive claude root # log in (per-node: each container has its own credentials)
hive ssh setup # once: lets the Claude desktop app open sessions in nodes
hive doctor # verify the isolation actually holds
hive new api # spawn a node (fresh volume workspace)
hive new web --bind ~/code/web # ...or mount a Mac directory as /workspace
hive sh api # zsh inside
hive claude api # interactive claude inside
hive claude api -p "run the tests and summarize failures" # headless
hive tree # see the hierarchy
hive rm api --purge # remove node + its workspace volumeOn each start, a container renders its own identity into
/etc/claude-code/CLAUDE.md (Claude Code's Linux managed-policy memory path),
so the Claude inside it loads — before anything else, every session — who it
is, that it's sealed behind the bridge, how egress and host forwards work, and
its role. root additionally learns it's the control plane and how to drive
the tree; a node learns it's a leaf and to ask you for network changes rather
than fight the allowlist. The text is derived from the container's role and
environment (name, parent, whether it has the proxy or --uplink), so it's
always accurate after any restart or rebuild — nothing to maintain by hand.
This lives on the container's own filesystem, so it never collides via the
shared ~/.claude volume nor clutters your bind-mounted /workspace. Confirm
it's loaded inside a session with /memory.
--parent builds arbitrary trees; the hierarchy is logical (Docker labels),
while spawn privilege stays physical (only root has the socket):
hive new backend
hive new backend-tests --parent backend
hive treeOrchestration pattern: run hive claude root and let that Claude drive the
fleet. hive is the same control plane whether you run it on your Mac or
inside root — root holds the Docker socket and has hive on its PATH
(symlinked to the live /workspace/bin/hive), so from a shell in root you
can hive new, hive <node> net on, hive tree, and hive claude <node> -p "..." its children, then collect results through the /shared volume. The
one split: lifecycle commands (build / up / down) stay on the Mac,
because they manage the container set that includes root itself. Each
container has its own hive-claude-<name> volume, so credentials and history
are isolated per node — you authenticate each container separately.
The bridge runs Squid, which applies two layers of "allow": the curated
domain allowlist (every node) and a per-node open set of source IPs (the nodes
you net on). Both files below live in config/ on your Mac, mounted
read-only into the bridge. Edit, then hive bridge reload.
- egress-allowlist.txt — which domains the
hive may reach. Ships with Anthropic, npm, PyPI, GitHub, and Debian.
hive bridge logsshows what's being refused when something fails. - forwards.conf — which Mac services the hive may
reach. One line per opening, e.g.
postgres 15432 host.docker.internal:5432; nodes then connect tobridge:15432. Nothing is forwarded by default.
hive bridge conf prints the generated proxy config and active listeners.
Each node has two live switches. They are applied at the bridge, not inside
the node — so they take effect on the node's very next request, instantly, even
in a shell that was already open (the node's environment never changes). Flip
them from the Mac or from inside root:
hive <node> net on # full, unfiltered internet for this node
hive <node> net off # back to sealed (allowlist only)
hive <node> github on # give this node your GitHub token
hive <node> github off # remove it
hive <node> status # show bothnet on adds the node's source IP to the bridge's "open" set in Squid and
reconfigures it live (squid -k reconfigure), so that node bypasses the
allowlist while every other node stays sealed. net off removes it. Because
the node still reaches the world only through the bridge, nothing about the node
changes — there is no proxy to go stale, no reconnect, no recreate. The open
set is remembered in config/open-nodes so it survives a bridge restart.
If instead you want to open one domain for the whole hive while keeping every
node sealed, add it to egress-allowlist.txt and
hive bridge reload. hive bridge logs shows each request as TCP_DENIED or
TCP_TUNNEL/200, so you can see exactly what was refused.
github on does the login for you — no token to paste. The first time you
run it, hive starts GitHub's device login (using gh inside root, which
reaches GitHub through the bridge), prints a URL and a one-time code, and waits
while you approve in your browser. It then caches the resulting token at
~/.config/hive/github-token (chmod 600, outside the repo) and installs it in
the node — git's credential helper for git clone https://github.com/... and
the gh CLI for gh pr / gh repo clone. Every later hive <node> github on
is silent (the cached token is reused). hive <node> github off deletes the
token from that node; hive github --forget drops the cached copy. No allowlist
change is needed (github.com is already permitted), and the token never enters
an image layer.
GitHub won't mint a token without you authenticating once — that single browser
approval is unavoidable. After it, hive handles everything. (For scripts, a
token piped in — echo "$T" | hive <node> github on — skips the login. You can
also pre-seed one with hive github <token>.)
Prefer SSH keys? Plain [email protected] needs port 22, which the bridge doesn't
tunnel. Use GitHub's SSH-over-443 endpoint: add ssh.github.com to the
allowlist (CONNECT on 443 is already permitted), put your key in the node, and
use remotes like ssh://[email protected]:443/owner/repo.git.
Full internet + published ports from creation:
hive new web --uplink -- -p 3000:3000.
For workflows where a node should drive your Mac — e.g. a node that develops hive itself, then asks the Mac to redeploy the running hive — there's a deliberate, opt-in channel:
hive hostd start # Mac: start the daemon + a bridge forward
hive <node> host on # bless a node (live — injects a secret token)
# then, inside that node:
host "cd ~/hive-containers && git pull && hive build && hive up"It's a live, per-node switch like net/github — host on/off inject or
remove the token in the running container, no recreate. hive new x --host is
just sugar for new + host on.
How it works: host <cmd> POSTs the command (with the node's private token) to
the Mac daemon via bridge:8765; the daemon runs it as you and returns the
output. The daemon binds loopback only (127.0.0.1) — Colima's host gateway
forwards arrive there — so the command port is never exposed on your LAN, only
through the bridge. Every command is logged (hive hostd logs). Default working
directory is your hive checkout, so deploy one-liners are short.
⚠️ This is real remote code execution on your Mac. Ahost onnode can run anything as your user — combined withgithub on, a compromised node could push code and have your Mac run it. It is off by default, gated by a secret token that lives only inside blessed nodes, and bound to loopback — but treat anyhost onnode as fully trusted, andhost offit when you're done.
To work on container tooling — hive itself, say — inside a node, give it a real nested Docker engine:
hive new hivedev --bind ~/hive-containers --dind # a node with its own dockerd
hive hivedev sh 'docker run --rm hello-world' # runs in the nested engine--dind uses the hive/dev image (Claude tools + a full Docker engine), runs
the node --privileged, and starts dockerd inside it with /var/lib/docker
on a dedicated volume so overlay2 works (no overlay-on-overlay). The nested
engine pulls through the bridge, so hive hivedev net on first if it needs
images from outside the allowlist.
Like the other switches it's also a live per-node toggle — but because
--privileged is a creation-time capability, hive <node> dind on/off
recreates the node (transparently preserving its workspace and its
net/github/host state). This is the one switch that recreates rather than
flips in place. It's --privileged under runc today; under Kata the micro-VM
provides the isolation and the same setup applies unchanged.
The intended loop: develop hive in the node → git push → host "cd ~/hive-containers && git pull && hive build && hive up" to redeploy the hive
running on your Mac.
The desktop app's SSH sessions run entirely inside a node while the app is your interface — file pane, terminal-free permission modes, the lot.
hive ssh setup # once: dedicated key + ~/.ssh/config block + key installThen in the app: Code tab → environment dropdown → + Add SSH connection,
host hive-<node> (e.g. hive-root), port and identity file empty. Every
node you hive new afterwards is immediately connectable the same way.
How it stays sealed: there is no sshd listening anywhere. The ~/.ssh/config
block uses ProxyCommand docker exec to spawn sshd -i on the connection's
stdio, so SSH rides the Docker socket — nodes gain no open port, nothing on
the hive network changes, and only this Mac can connect. The Docker context
is baked into the config at setup time, so it keeps working when another
engine (e.g. colima) holds the default context.
Hive lives on one engine — its seal is a Docker network, which can't span
VMs. Which engine is pinned in config/docker-context
(currently desktop-linux); every hive command targets it no matter which
context your shell has active, so colima start stealing the default context
never breaks or relocates the hive. Plain docker keeps following your
active context for other projects.
To let hive nodes reach a service from a colima project: publish its port to
the Mac as usual (-p 5432:5432), then curate it like any Mac service —
colima-pg 15432 host.docker.internal:5432 in forwards.conf. To migrate hive
itself to another engine: change config/docker-context, then
hive build && hive up && hive ssh setup.
hive devcontainer ~/code/myprojectdrops a.devcontainer/that joins the project to the hive as a labeled node — then "Reopen in Container".- Or attach VS Code to any existing node: "Dev Containers: Attach to Running
Container" →
hive-<name>. hive join <container>connects a container you created some other way tohive-net(it appears under guests inhive ls).
Every node (and root) is built from hive/node, which ships Claude Code, the
GitHub CLI, Node and Python, and a broad toolset so the agent rarely has to
install anything:
- network suite —
nmap,tcpdump,dig/nslookup,mtr,traceroute,nc/ncat,socat,whois,iperf3,ngrep,arp-scan,iproute2,nftables/iptables,ethtool,openssl - datastore clients —
psql,mysql,redis-cli,sqlite3 - build & dev —
build-essential/gcc/make,pkg-config,git,tmux,jq,ripgrep,fd,bat,httpie,htop,strace,shellcheck,rsync
Add more in images/node/Dockerfile and hive build.
| Need | Do |
|---|---|
| Full internet for one node, instantly | hive <node> net on |
Direct internet at creation + working -p published ports |
hive new x --uplink -- -p 3000:3000 |
| Extra docker flags (env, ports, gpus…) | everything after -- goes to docker run |
| A different base image | hive new x --image myimage (any image works; Claude tooling comes from hive/node) |
| Open egress entirely | put a single * line in the allowlist (deliberate act, on purpose) |
| New tools in the base image | edit images/node/Dockerfile, hive build |
Sandboxing AI coding agents is a well-trodden, active area — the building blocks here are not new:
- Anthropic ships a reference devcontainer firewall:
iptablesdefault-DROP plus anipsetdomain allowlist, per container, which is what makes--dangerously-skip-permissionssafe. @anthropic-ai/sandbox-runtimewraps the Claude Code process in Seatbelt/bubblewrap with filesystem + network allowlists (host process, not a container).- Docker AI Sandboxes route all egress through a host proxy that enforces a network policy (and inject credentials).
- agent-sandbox (mattolson) forces traffic through a sidecar proxy with per-hostname/path egress rules.
- Conceptually the closest is Qubes OS, where AppVMs reach the network only
through a shared
sys-net/sys-firewallVM.
So the egress-allowlist idea is mainstream. What hive assembles — and what I haven't found packaged together — is:
- a hierarchy of agent containers: a
rootcontrol plane that holds the Docker socket and orchestrates an arbitrary-depth tree of nodes; - a single shared curated bridge that does both egress filtering and host-port forwarding — one auditable crossing point for the whole fleet, rather than a firewall baked into each container;
- a per-node, live egress toggle (
hive <node> net on/off) via Squid source ACLs — instant, with no change to the node or its environment; - desktop-app / IDE access over socket-tunnelled SSH, with no open ports;
- one CLI tying it together, each container told where it is via a managed
CLAUDE.md.
In one line: the allowlist is mainstream; hive's contribution is the shared bridge + hierarchy + per-node instant toggle + host forwards + desktop SSH as one small, auditable system — roughly "Anthropic's firewall devcontainer meets a Qubes-style network VM."
- The seal on nodes is the internal network — in-container root/sudo doesn't break it, which is why nodes get passwordless sudo for convenience.
- The egress allowlist is domain-level filtering, not DPI; an allowlisted domain (e.g. github.com) is still a data channel.
hive <node> net onopens that one node to the whole internet (Squid keys off its source IP); other nodes stay sealed. It's per-node, instant, and reversible, but while on, that node has no egress restriction at all.root+ the Docker socket = control of the Docker VM and every container. Treatrootas trusted infrastructure: it's your orchestrator, not a sandbox for untrusted work — untrusted work belongs in nodes.- Claude credentials are per-node: each container has its own
hive-claude-<name>volume (root's ishive-claude-root), so a login in one node is not visible to any other. Authenticate each node separately; delete its volume to revoke just that node (hive rm <node> --purgedoes this).
bin/hive the CLI (symlinked into root via /workspace)
compose.yaml root + bridge + networks + volumes
config/ ← the two files you curate
images/{node,root,bridge,dev}/ Dockerfiles + entrypoints (dev = --dind)
templates/devcontainer/ what `hive devcontainer` installs