One container per coding-agent session, on a host of your own, built with the Dev Container CLI. Each session gets its own clone of a workspace repo; you reach it over SSH straight into the container.
agentbox is three things:
- A devcontainer Feature,
ghcr.io/novibe-org/agentbox/agentbox: sshd, helix, yazi, lazygit, delta, ripgrep, Cloudflare's security-audit skill, and the mounts that share session state across containers. - A CLI,
agentbox, that creates hosts and creates, opens, lists and deletes sessions from your machine. - A session API,
agentboxd, on each host, which creates its sessions and sets them up, whether your machine stays connected or not.
Sessions are isolated from each other and from your laptop, not from your accounts. Every session sees the same Claude Code login, GitHub login and git identity, and clones and pushes GitHub over HTTPS with that login. Your SSH agent is still forwarded in while you are connected. An agent in a session can push wherever you can.
- Install the CLI:
brew install novibe-org/tap/agentbox, which brings OpenTofu along. - Create a session host:
agentbox host create devbox --provider hetzner. - Make a workspace repo whose devcontainer uses the Feature.
agentbox new <id> <owner/workspace> --host devbox, then log in once.
A host is a server that agentbox host create made: Docker, Node, the Dev Container CLI and agentboxd, with a volume at /var/lib/agentbox, owned by uid 1000, that holds the shared state mounted into every session:
| Host | Container | Holds |
|---|---|---|
/var/lib/agentbox/claude |
/agentbox/claude |
Claude Code login, settings |
/var/lib/agentbox/gh |
/agentbox/gh |
GitHub CLI login |
/var/lib/agentbox/git |
/agentbox/git |
git identity (config) |
/var/lib/agentbox/pnpm-store |
/agentbox/pnpm-store |
pnpm package store |
The security-audit skill is installed in Claude Code's managed skills folder, /etc/claude-code/.claude/skills/, so it is in every session without touching the shared config.
agentboxd creates these directories if they are missing, and clones each session's workspace repo into /var/lib/agentbox/sessions/<id>, mounted at /workspaces/<id>. A session's container carries the label agentbox.session=<id>. The container user must be uid 1000, as node is in the Node images.
A workspace can share its own state the same way: keep it in another directory under /var/lib/agentbox and add a mounts entry to its devcontainer.json. The directory must exist before the session is created, or Docker creates it empty and owned by root.
agentbox host create creates the host with OpenTofu and the module in modules/hetzner: a server with Docker, the agentbox user and agentboxd of the CLI's version, a volume mounted at /var/lib/agentbox, and a firewall that lets in SSH only, or nothing for a host on your tailnet. Session clones and the shared logins live on the volume; containers and images live on the server's own disk.
Create an API token with read and write access in the Hetzner Console, under your project's Security > API tokens, then:
export HCLOUD_TOKEN=<token>
agentbox host create devbox --provider hetznerThe server is a cpx32 (4 vCPU, 8 GB) in Nuremberg with a 50 GB volume. Each host keeps its OpenTofu state and its own SSH key in ~/.agentbox/hosts/<name>; keep that directory, as it is what deletes the host later. agentbox host create refuses a name you already have, and returns once agentboxd reports the host healthy. When a newer agentbox first uses a host, it brings the host's agentboxd to its own version, and the sessions on it keep running; it waits while a session is being set up.
The CLI writes every host it creates, and the way into sessions, to ~/.ssh/agentbox.conf, and puts Include agentbox.conf at the top of ~/.ssh/config. ssh devbox reaches the host, and <id>.agentbox a session.
With --tailnet the host joins your tailnet and its firewall opens no port: you reach it over the tailnet only, still with OpenSSH and the host's own key. The module in modules/tailscale creates a single-use auth key tagged tag:agentbox, and the host's SSH entry names it by its MagicDNS name.
Create an OAuth client with write access to auth keys for tag:agentbox, then:
export TAILSCALE_OAUTH_CLIENT_ID=<id> TAILSCALE_OAUTH_CLIENT_SECRET=<secret>
agentbox host create devbox --provider hetzner --tailnetA host you already have moves onto your tailnet with agentbox host update devbox --tailnet. Its sessions keep running; its firewall closes only once you reach the host over the tailnet, so a policy that keeps you out leaves it open as before.
Tailscale's default policy lets every device reach every other, so the coding agents on the host could reach your other devices. Give tag:agentbox no access of its own in your tailnet policy, in place of the default rule that allows everything:
{
"tagOwners": { "tag:agentbox": ["autogroup:admin"] },
"grants": [
{ "src": ["autogroup:member"], "dst": ["tag:agentbox"], "ip": ["22"] }
]
}Hosts agentbox did not create have no agentboxd, and this version runs no sessions on them. Hosts created with DevPod, such as an earlier claude-box, keep working with agentbox 0.1.0 and Feature :2.
agentboxd serves the API in agentboxd/openapi.yaml on a Unix socket, /run/agentboxd/agentboxd.sock, that only the agentbox user can open. No port is open for it: the CLI reaches it over SSH with ssh <host> agentboxd dial-stdio, as Docker's CLI reaches dockerd. It keeps one record per session, whether its setup finished or why it failed; the labelled containers and the clones hold the rest.
ssh devbox 'curl -s --unix-socket /run/agentboxd/agentboxd.sock http://agentboxd/health' # what the host can do
ssh devbox journalctl -u agentboxd # its logThe CLI and agentboxd change together, so the CLI refuses a host whose agentboxd is on another version.
A workspace is a git repo whose .devcontainer uses the Feature and clones the projects it works on:
# .devcontainer/setup.sh
set -euo pipefail
for repo in app plugins; do
[ -d "projects/$repo/.git" ] || git clone "https://github.com/myorg/$repo.git" "projects/$repo"
done
(cd projects/app && pnpm install --frozen-lockfile)Sessions clone and push GitHub over HTTPS with the shared GitHub login, or without credentials for a public repo before anyone has logged in; the Feature rewrites git@github.com: URLs to HTTPS, so existing setups keep working. agentboxd runs the setup without your machine, so it cannot use your SSH agent.
agentbox rm checks the workspace repo and every repo under projects/ for unpushed work before deleting.
Good habits for a workspace repo:
- Restart with the host:
runArgs: ["--restart=unless-stopped"], as above, brings sessions back after the host or Docker restarts. - Ignore
projects/in its.gitignore. The cloned repos stay independent;git add projects/would turn them into gitlinks. - Keep
setup.shidempotent: guard each clone, as above, so a rebuild doesn't fail on what is already there. - Install from lockfiles (
--frozen-lockfile,npm ci). With the shared pnpm store, a second session installs mostly from cache. - Check in
.claude/settings.jsonwith the plugins and marketplaces the sessions need, so every session starts with the same Claude Code setup. - Add a
CLAUDE.mdthat names the repos underprojects/and what each is for.
Install it with Homebrew, which also installs OpenTofu:
brew install novibe-org/tap/agentboxbrew upgrade agentbox picks up new releases. Without Homebrew, download a release and keep it on your PATH:
curl -fsSL https://github.com/novibe-org/agentbox/releases/download/v0.2.1/agentbox-darwin-arm64 -o ~/.local/bin/agentbox
chmod +x ~/.local/bin/agentboxThe CLI is one binary with nothing else to install: agentbox-darwin-arm64 for Apple silicon, agentbox-darwin-x64 for Intel Macs, agentbox-linux-x64 for Linux. agentbox --help lists the commands.
It is written in TypeScript with Commander and compiled with Bun; see ADR 0002. npm ci && npm run build builds it from a checkout into dist/agentbox.
Name the host and the workspace repo, or run from inside a checkout of the repo:
agentbox new fix-login myorg/workspace --host devbox # create a session and open it
agentbox new fix-login --host devbox # same, from this directory's origin
agentbox open fix-login # reconnect
agentbox ls
agentbox rm fix-loginagentbox new asks the host's agentboxd to create the session, records its host in ~/.agentbox/sessions/<id>, and opens it once it is set up. The host sets it up whatever happens to your machine meanwhile. A session id is unique across your hosts. open, rm and <id>.agentbox find the session by its record; <id>.agentbox goes straight into the session's container through the host, and starts the container first if it is stopped.
open uses cmux when run inside it, plain ssh otherwise.
| Variable | Default | Meaning |
|---|---|---|
AGENTBOX_MACHINE |
SSH host for sessions created without --host |
|
AGENTBOX_REPO |
this directory's origin |
Workspace repo for new: a git URL, owner/name or owner/name@branch |
AGENTBOX_PUBKEY |
~/.ssh/id_ed25519.pub |
Public key authorized in each new session |
HCLOUD_TOKEN |
API token of the Hetzner project hosts are created in | |
TAILSCALE_OAUTH_CLIENT_ID, TAILSCALE_OAUTH_CLIENT_SECRET |
OAuth client of the tailnet that --tailnet hosts join |
Sessions created before --host have no record and go to AGENTBOX_MACHINE. Some tools run ProxyCommand without your shell profile, so set it where they see it too, such as launchctl setenv AGENTBOX_MACHINE agent-host on macOS.
AGENTBOX_PUBKEY must match the key SSH offers to *.agentbox. If you use another key, set both the variable and an IdentityFile with IdentitiesOnly yes in the *.agentbox block.
The logins live on the host, not in the container, so the first session sets them up for every later one. A private workspace repo needs the GitHub login before its first session, as the host clones it: log in on the host once with ssh -t devbox GH_CONFIG_DIR=/var/lib/agentbox/gh gh auth login.
claude # log in to Claude Code
gh auth login # log in to GitHub
git config --global user.name "Your Name"
git config --global user.email "you@example.com"Deleting a session keeps them; only deleting the directories under /var/lib/agentbox on the host removes them.
cmux opens links clicked in the terminal in its own browser. To open them in your Mac's browser instead, while keeping the session's forwarded dev servers in cmux, add this to ~/.config/cmux/cmux.json and reload with ⌘⇧,:
"browser": {
"hostsToOpenInEmbeddedBrowser": ["localhost", "*.localhost", "127.0.0.1", "::1", "0.0.0.0"]
}Set "openTerminalLinksInCmuxBrowser": false instead to send every link out, localhost included.
- One session per task. Name it after the task (
fix-login) andagentbox rmit once the work is merged. Sessions are cheap; a stale one still holds a clone and a container. - Push before you leave a session.
rmrefuses to delete unpushed work, but losing the host loses it. - Let agents push branches, not
main. They act with your accounts; branch protection on your repos is the guard rail. - Rebuild instead of repairing. If a session's environment drifts,
agentbox rmandagentbox newwith the same id start clean from the workspace repo. - Bring a stopped session back with
agentbox open <id>, which starts its container. Sessions without the restart policy stay down after the host reboots until then. - Watch the disk.
ssh devbox df -h /var/lib/agentboxshows the volume;docker system pruneon the host clears old images after Feature updates.