agentbox is an npm CLI that spins up isolated sandboxes ("boxes") for coding agents (Claude Code, Codex, others) to work in, so they can't touch the host. Seven backends share one provider abstraction: Docker (the default — one local container per box, isolated by per-box git branch in an in-container worktree against the bind-mounted host .git/), Daytona Cloud (--provider daytona — a managed remote sandbox seeded from a host git bundle + per-agent credential volumes, reached via SSH-token attach and an in-sandbox bridge relay), Hetzner Cloud (--provider hetzner — a bare VPS per box, pure OpenSSH ControlMaster comms, locked-down Hetzner Cloud Firewall, baked from a one-time agentbox prepare --provider hetzner snapshot), Vercel Sandbox (--provider vercel — a Firecracker microVM per box, persistent snapshots, public HTTPS preview URLs; nested containers (in-box docker) now supported and baked in, no SSH, baked from a one-time agentbox prepare --provider vercel snapshot), E2B Sandbox (--provider e2b — a Firecracker microVM per box, SDK-only comms, public HTTPS preview URLs, persistent pause/resume; uniquely among the cloud providers, E2B builds its base template directly from a Dockerfile via Template.build() — agentbox prepare --provider e2b runs the build), and Remote Docker (--provider remote-docker, spelled agentbox docker:<host> … — one container per box on a machine the user already owns, reached over an OpenSSH ControlMaster and driving that machine's docker engine. Cloud-shaped despite being docker: a bind mount can't cross a network, so the workspace is synced (git clone + carried-over stash/untracked) exactly as for the clouds, while the image (Dockerfile), checkpoints (docker commit) and DinD stay docker-shaped. No credential — it connects as you, over your own ~/.ssh/config).
- Boxes — one isolated sandbox per agent run. The shape differs by provider but the abstraction is one
Providerinterface (packages/core/src/provider.ts):- docker: container
agentbox-<id|name>;/workspaceis the in-container git worktree on branchagentbox/<box-name>; host's.git/is bind-mounted RW so commits land on the host immediately. Boxes pause/unpause for cheap context switching and survive stop/start;destroywipes the container + per-box volumes. The base image (agentbox/box:dev) is pulled from GHCR on first use (tagged by build-context fingerprint, seepullOrBuild/registryRefForShainimage.ts) and only built locally on a pull miss;--build/box.imageRegistry=""force a local build. Seedocs/development.md→ "Image: pull vs rebuild". - daytona (cloud): Daytona sandbox with
/workspaceseeded from a hostgit bundle create(incl. stash + untracked carry-over for the user's local state). Lifecycle goes through the Daytona SDK; agent credentials (~/.claude,~/.codex,~/.config/opencode) live in shared per-org volumes seeded from the host. Host↔box comms go through a per-box bridge URL (CloudFront preview) that the host relay'sCloudBoxPollerlong-polls. - hetzner (cloud): one Hetzner VPS per box (default
cx23/nbg1). Workspace seeded the same way (git bundle + stash + untracked tar). Per-box ed25519 SSH key minted on the host into~/.agentbox/boxes/<sandboxId>/ssh/and injected via cloud-init. Per-box Hetzner Cloud Firewall auto-locked to the host's egress IP (multi-probe fail-loud). All comms (exec, scp, port forwards, attach) flow over one persistentssh -fNT -MControlMaster per box;previewUrl(port)mintsssh -O forwardon demand. No agent credentials volume — credentials pushed via scp at create time (Hetzner has no shared-volume primitive).agentbox prepare --provider hetznerbakes a one-time base snapshot since Hetzner can't build images from a Dockerfile. - vercel (cloud): one Vercel Sandbox (Firecracker microVM, Amazon Linux 2023) per box. Workspace seeded the same way (git bundle + stash + untracked tar). Boots from a Vercel snapshot baked once by
agentbox prepare --provider vercel(no Dockerfile build). Persistent sandboxes auto-snapshot on stop and auto-resume onSandbox.get({ resume: true })→ pause/resume for free. Comms via the SDK:execruns asvscode(root →sudo -u vscode);previewUrl(port)returns the publicsandbox.domain(port)(HTTPS, no token), so the host relay'sCloudBoxPollerreaches the in-box bridge directly. In-box docker (DinD) is baked into the base snapshot anddockerdis auto-started on create/resume (launchDockerd:true, via the sharedagentbox-dockerd-start) — Vercel Sandbox now supports nested containers. No SSH (attach is a customattach-helper.jstmux bridge over the SDK). Max 4 exposed ports, regioniad1only. Seedocs/vercel-backlog.md. - e2b (cloud): one E2B Sandbox (Firecracker microVM, Debian 12) per box. Workspace seeded the same way (git bundle + stash + untracked tar). Key differentiator from Vercel/Hetzner: E2B builds its base image directly from a Dockerfile via the SDK's
Template.build()—agentbox prepare --provider e2bdrives the build and pins the resulting template id tobox.imageE2b.Sandbox.pause/Sandbox.connect(auto-resume) gives free pause/resume;Sandbox.createSnapshotis the reusable, id-addressed checkpoint primitive (same shape as Vercel). Comms via the SDK:execruns asvscode;previewUrl(port)returns the public{port}-{sandboxId}.e2b.appURL (HTTPS, no token; constructed locally so it doesn't wake a paused sandbox). In-box docker (DinD) is baked into the base template anddockerdauto-starts on create/resume (launchDockerd:true) — E2B microVMs support nested containers (full root + cap_sys_admin, verified 2026-06-23), contrary to the original "same as Vercel" assumption. No SSH — attach is a customattach-helper.cjsSDK-streaming PTY bridge overpty.create. 1-hour platform session cap on the Hobby tier (the attach helper caps at 55 minutes for headroom). Seedocs/e2b_backlog.md.
- docker: container
- In-box supervisor (
@agentbox/ctl) — reads/workspace/agentbox.yamland runs the declared tasks/services under a DAG scheduler. Ships asagentbox-ctlinside every box (docker, daytona, hetzner, vercel, e2b). - Host relay (
@agentbox/relay) — a host node process boxes call for things they have no credentials for (git push, checkpoint capture,cp/download) and to push status events. Keeps SSH keys out of the box. The cloud path drives the same relay viaCloudBoxPoller+executeCloudAction. - Checkpoints —
docker commit(+ periodicFROM scratchflatten) for docker; Daytona snapshots (sb._experimental_createSnapshot) for daytona; Hetznercreate_imagesnapshots (no-pause default — matchesdocker commit) for hetzner; Vercelsb.snapshot()(id-addressed; stores the snapshot id in the cloud-checkpoint manifest) for vercel; E2BSandbox.createSnapshot(id-addressed template, same shape as Vercel) for e2b. All flow throughprovider.checkpoint.create.box.defaultCheckpointis the cross-provider fallback;box.defaultCheckpointDocker/box.defaultCheckpointDaytona/box.defaultCheckpointHetzner/box.defaultCheckpointVercel/box.defaultCheckpointE2boverride per provider. - The full design — file-handling rationale, the checkpoint model, pause/resume strategy, what we explicitly rejected — lives in
docs/architecture.mdanddocs/create-and-checkpoints.md. Cloud-specific status lives indocs/daytona-backlog.md,docs/hertzner_backlog.md,docs/vercel-backlog.md, anddocs/e2b_backlog.md. Read them before making non-trivial changes to the lifecycle code.
- You have docker and you are authorized to run docker commands, inspect containers, run commands inside containers, etc.
- For cloud work: the Daytona API key + org id, the Hetzner
HCLOUD_TOKEN, the Vercel auth trio, and the E2BE2B_API_KEYall live in~/.agentbox/secrets.env(managed by the per-provideragentbox <provider> logincommands). You may use each cloud's SDK directly to inspect / clean up sandboxes when a test leaves an orphan, oragentbox prune --provider <name> -yfor the supported path. - For hetzner-cloud work: the base-snapshot id is recorded at
~/.agentbox/hetzner-prepared.json(written byagentbox prepare --provider hetzner); per-box SSH keys live under~/.agentbox/boxes/<sandboxId>/ssh/(private key never leaves host, dropped ondestroy). You may use the Hetzner REST API directly viacurl -H "Authorization: Bearer $HCLOUD_TOKEN" https://api.hetzner.cloud/v1/...to clean up orphan servers / firewalls / snapshots when a test leaves something behind.agentbox prune --provider hetzneris not yet wired (backlog item — the underlyingbackend.list()works). - For e2b-cloud work: the base-template id is recorded at
~/.agentbox/e2b-prepared.json(written byagentbox prepare --provider e2b, which drivesTemplate.build()from a Dockerfile). You can use thee2bSDK directly (node -e "import('e2b').then(({Sandbox})=>Sandbox.list())") to inspect or clean up sandboxes when a test leaves an orphan, oragentbox prune --provider e2b -yfor the supported path.
create, claude, codex, and opencode tee their progress to a file at
~/.agentbox/logs/<command>.log, and ~/.agentbox/logs/latest.log always points
at the most recent run. The log is rotated 1-deep — the previous run is at
<command>.log.prev.
When verifying a change:
- Don't pick a blind long timeout. Start the slow command in the background
(e.g.
node apps/cli/dist/index.js create -y -n smoke &), thentail -f ~/.agentbox/logs/latest.logto watch real progress. Stop waiting the moment the log shows what you need (e.g.box ... readyor a failed step). Don't sit on a 120s blocking call hoping it returns. - Interactive TUIs (
dashboard,claude,codex,opencode): drive them throughpnpm drive(the PTY harness atapps/cli/test/_harness/).pnpm drive start --name X -- node apps/cli/dist/index.js dashboard, thenpnpm drive screen Xto read the rendered terminal andpnpm drive send X "<C-a>q"to send keystrokes.pnpm drive --helpandapps/cli/test/_harness/README.mdcover the surface. - Typical create check:
node apps/cli/dist/index.js create -y -n smoke &, thentail -f ~/.agentbox/logs/create.loguntil you see the BEGIN/END markers for each step. If a step's END never arrives, you've found the hang — inspect that step rather than killing the whole command. - Test projects: use the
examples/directory mainly, or../agentbox-test-repoto test push/pull on a test repo setup on GitHub, and../agentbox-test-repo-ghfor the same repo but with https origin usingghtool. Also../express-servercan be used to test the setup wizard since it doesn't have anagentbox.yamlfile. - Use Agentbox inside Agentbox: start a container with
agentbox claude --shared-docker-cache --carry-yesto have a box ready with agentbox compiled and in the path and reuse docker cache for faster builds. For Images build usedocker build --network=host -t agentbox/box:dev -f apps/cli/runtime/docker/Dockerfile.box apps/cli/runtime/dockerinstead ofagentbox preparebecause the box runs withoutCAP_SYS_PTRACE. - Hub is a persistent daemon — always rebuild + restart it after any hub change.
agentbox hub(relay + Next UI on 8787) is long-lived and spawns the standalone build, so a running hub keeps serving stale code after you editapps/hub/**or any package it imports (@agentbox/relay,@agentbox/sandbox-docker, …). On dev, rebuild the standalone and restart before verifying:Thepnpm --filter @agentbox/hub build:standalone AGENTBOX_HUB_BIN="$PWD/apps/hub/dist-standalone/apps/hub/server.js" node apps/cli/dist/index.js hub restartAGENTBOX_HUB_BINoverride is load-bearing:resolveHubServerprefers the CLI-stagedapps/cli/runtime/hub/…/server.js(only refreshed by a fullagentboxCLI build) over the freshapps/hub/dist-standalone, so a barehub restartrespawns the stale staged bundle. Rebuild the imported packages too if you touched them. Same rule asagentbox relay restartfor relay code. For fast UI-only iteration run the hub directly withpnpm --filter @agentbox/hub hub:dev(tsx watch server.ts). Note:public/assets (logo, favicon) only work throughbuild:standalone(which stagespublic/) ornext dev/next start— never assume a static asset serves without one of these.
- TypeScript strict, ESM,
verbatimModuleSyntax— alwaysimport type { … }for types. - tsup builds each package's
src/index.ts→dist/. Don't reach into another package'ssrc/from a sibling; consume via the package name. - vitest for tests, default discovery (
test/**/*.test.ts). Keep unit tests pure — no docker, no network. Integration testing is manual for now (see README → Development). - eslint + prettier, flat config at repo root.
pnpm lintandpnpm formatare the commands. - commander for CLI surface; @clack/prompts for any interactivity. Don't add a third prompts/CLI lib.
- execa for shelling out to
docker(debuggable, no native deps). Don't introducedockerodewithout a good reason. One sanctioned native-dep exception:@homebridge/node-pty-prebuilt-multiarch(ships ABI-stable N-API prebuilds, no end-user compiler) is used only byagentbox dashboardfor the in-process terminal compositor. It is anoptionalDependenciesofapps/cliwith a guarded dynamic import — a missing prebuild degradesdashboardto a clear error, never breaks the rest of the CLI. - No emojis in code or output unless explicitly requested.
- Comments only when the WHY is non-obvious (a constraint, a workaround, a surprising invariant). Names should carry the WHAT.
@madarco/agentbox-provider-sdkis published separately — rebuild + republish it when you change its interface. The provider-plugin SDK (packages/provider-sdk) is a self-contained npm package that external plugins depend on; it inlines the internal@agentbox/*packages via tsupnoExternal. Its public surface = the re-export list inpackages/provider-sdk/src/index.tsplus the re-exported types/values from@agentbox/core(Provider/CloudBackend/ProviderModule),@agentbox/sandbox-cloud(createCloudProvider, attach/staging/checkpoint helpers), and@agentbox/sandbox-core(prepared-state, runtime-assets). If a change alters any of those, the shipped SDK is stale until you rebuild and republish it (bump its ownversion; a breaking change also bumpsSDK_API_VERSIONin that index + must be in the CLI'sSUPPORTED_SDK_API_VERSIONS). Gate + publish perdocs/provider-plugins.md→ "Publishing the SDK":pnpm --filter @madarco/agentbox-provider-sdk pack:testthencd packages/provider-sdk && npm publish. The/release-notesskill checks for this automatically.
A native macOS menu-bar app lives in the sibling repo ../agentbox-tray
(private GitHub madarco/agentbox-tray). It surfaces all boxes and gives one-click actions — open
the hub, open each box's Web/VNC, start/stop, per-box git ops (pull/push/push --host-only/
checkout/branch), restart services, and answer host-action approvals — without a terminal. It
updates live over the hub's SSE stream and falls back to polling.
It has no build-time coupling to this repo — it's a Swift Package Manager / AppKit app (Swift 5.10, no Xcode, no external deps) that drives the two public surfaces:
- Boxes + actions via the local Control Hub REST API at
127.0.0.1:8787:GET /api/v1/boxes(which carries the raw host-side fields —state,projectRoot, endpoint URLs, session titles — and the syntheticcreating/errorboxes for in-flight/failed creates) plus the lifecycle (start/pause/resume/stop/destroy), git, rename, and services routes. Approvals use/api/v1/approvals(+…/{id}/answer), live events the SSE/api/eventsstream. Auth split to remember when changing the hub:/api/v1/*usesAuthorization: Bearer <token>, but/api/eventsreads theagentbox_hub_tokencookie (Bearer there 401s). Token is~/.agentbox/hub/token. SSE events are refetch signals only (emptydata: {}). - Inherently-local actions via the installed CLI, shelled through a login shell
(
/bin/zsh -lc 'agentbox …', because a GUI app has no inherited PATH):open --in/open --targets(terminal attach in iTerm2/cmux/Herdr), andhub status/hub startto bootstrap the hub itself.
When you change the hub /api/v1 API (the Box payload especially), the SSE event/auth contract, or
the CLI commands the app still shells (open, hub), update the tray app too — its own
CLAUDE.md documents exactly which surfaces it depends on. The app's
data/action layer sits behind a BoxSource protocol (implemented by HubAPIBoxSource) so it can
target the hosted control-plane unchanged (aligns with docs/control-plane-roadmap.md).
Each topic has a dedicated file under docs/. Read the relevant one before changing that area.
Keep the public docs in sync — every change. The user-facing documentation site lives in
apps/web/content/docs/(Fumadocs, published at https://agent-box.sh/docs). Whenever you add or change a CLI command, flag, config key, default, or provider/lifecycle behavior, update the matching.mdxpage (andmeta.json/CLI reference) in the same change — stale public docs are a bug. When the UI a figure shows changes, recapture it perapps/web/images.md. Seeapps/web/CLAUDE.mdfor the site's structure, theming, and build.
docs/architecture.md— the design doc: why the box/worktree/checkpoint model is shaped the way it is, and what was rejected.docs/create-and-checkpoints.md— implementation reference foragentbox create(file/git handling) and the checkpoint capture/restore mechanics.docs/repo-layout.md— the package tree, build wiring, and box-identifier / per-project-index resolution rules.docs/state.md— where every piece of state lives:~/.agentbox/*, docker objects, volumes, worktrees, the box image.docs/in-box-supervisor.md—@agentbox/ctl: the DAG scheduler, tasks vs services,ready_when,expose/WebProxy, wire ops, config validation. Thecarry:block (host→box file copy with one-prompt host approval) is also declared at the top level ofagentbox.yamlbut is host-CLI-applied, not parsed by the supervisor — seedocs/features.mdfor the schema + flags (--carry-yes,--carry skip,AGENTBOX_CARRY_YES,AGENTBOX_CARRY).docs/host-relay.md—@agentbox/relay: the host process, per-box bearer token, endpoints, registration/rehydration, in-boxagentbox-ctl git/open.docs/terminal-integration.md— host-terminal attach placement (--attach-in/attach.openInacross tmux/cmux/Herdr/iTerm2), terminal-title handling, the cmux sidebar box-status integration (attach.cmuxStatusdrives the workspace colour/description, notset-statuspills) plus its gotchas (stored-but-hidden pills, socket session-auth, drive harness can't verify), and the Herdr integration (attach.herdrStatustransparently reports box agent state over Herdr's JSON-RPC socket so Herdr handles needs-input natively; an explicitnotification.showhighlights AgentBox's own host-relay approval prompts; detection runs before iTerm2) plus the Herdr plugin (aagentbox list --herdrboxes overlay,prefix+a/prefix+shift+ashortcuts, and anagentbox://Ctrl+click link handler →agentbox herdr link/new). Installable two ways from one source (install-herdr.ts):herdr plugin install madarco/agentbox(the committed repo-rootherdr-plugin.toml+build.sh— at the repo root, not a subdir, so the Herdr marketplace can index it;build.shrunsagentbox install herdr --plugin-keys) oragentbox install herdrdirectly. The static manifest routes through a generatedagentbox-shim.sh(absolute CLI path — Herdr has no reliable PATH); keybindings go in the user's~/.config/herdr/config.toml(Herdr ignores manifest keys); a test keeps the committedherdr-plugin.toml/build.shin sync with the builders.docs/features.md— what works today (the full CLI lifecycle) and what is not built yet.docs/development.md— build + verify commands, manual end-to-end runs, the image-rebuild checklist, and assumed host environment.docs/cloud-providers.md— Daytona + Hetzner provider docs: how each cloud differs from docker, the bridge relay model, agent-credential volumes, signed preview URLs, SSH ControlMaster + per-box firewall (hetzner), known caveats.docs/remote-docker-plan.md— remote-docker: how to finish the testing from the host. The provider was built + verified from inside a box against a nested engine; a real Linux server and a macOS/OrbStack remote have NOT been tested. Step-by-step commands, what to watch for, gotchas already found.docs/remote-docker-backlog.md— the remote-docker provider (docker on a machine you own, over SSH): what shipped, what was verified live, what's left. Design rationale is incloud-providers.md§3e.docs/provider-plugins.md— external / community providers: publishagentbox-provider-<name>on the public@madarco/agentbox-provider-sdkandagentbox plugin addit (registry at~/.agentbox/plugins.json, runtimeimport()seam, shared box-runtime viaresolveSharedRuntimeAsset, trust-on-add +SDK_API_VERSIONgate). Reference:examples/agentbox-provider-sample.docs/cloud-create-flow.md— step-by-step walk ofagentbox create --provider daytona: how.gitand workspace files get into the box (git bundle + stash + untracked tar), cloud checkpoints, the base snapshot vs project snapshot tiers, and the docker-auto-builds-but-daytona-doesn't asymmetry.docs/daytona-backlog.md— what's done vs still missing on the Daytona path. Quick index of where each cloud feature actually lives.docs/hertzner_backlog.md— Hetzner provider build-out status: phase-by-phase progress, the live e2e smoke results, deferred follow-ups (per-project snapshot tier,--pausecheckpoint flag,agentbox prune --provider hetzner, the install-script post-Chromium trace mystery). Filename uses the user-requested spelling.docs/vercel-backlog.md— Vercel provider build-out status: why Vercel's shape differs (no Dockerfile, no containers, no SSH, persistent snapshots), phase-by-phase progress, and the live-verify checklist (user mapping, attach latency / ttyd upgrade, snapshot-vs-delete cascade, VNC on AL2023, published-CLI asset staging).docs/e2b_backlog.md— E2B provider build-out status: how the shape maps ontoCloudBackend, why E2B is the only cloud that builds the base from a Dockerfile (Template.build()), task-by-task progress, and shipped/deferred items.docs/linux-host-backlog.md— Linux (Ubuntu) host support: what's done (agentbox doctoris Linux-aware), how to test on a persistent Hetzner Ubuntu VM (scripts/linux-dev-vm.sh—up/deploy/ssh/doctor/down), and the remaining macOS-only host assumptions (browseropen→xdg-open, iTerm2/AppleScript terminal spawning, OrbStack-only fast paths).docs/control-plane-roadmap.md— the Control Hub architecture + roadmap (the forward direction; "Control Hub" /agentbox hubis the new name for the control-plane, renamed in milestone M1). Covers the three shifts (in-box create/bake + poll to unify the local + serverless paths; hub-anywhere/PC-first with a SQLite local hub; custody + 3-way sync), the four deployment topologies (local host, mac-mini, server/container, serverless Vercel→Cloudflare), the two capability profiles (serverless "control plane" vs full-host "control-box"), the source-of-truth constraint, the CLI rename +hub install/update/uninstall, and milestones M0–M8. Pairs withdocs/control-plane-backlog.md(what shipped).docs/control-plane-guide.md— the feature guide for the hosted control plane (a.k.a. the legacy "control-box"): the one-relay-core/three-topologies model, how it works (Store seam, GitHub-App token leasing, block-vs-poll approvals, dual-mode server + bridge + CloudBoxPoller, the create-job worker, the in-box clone/relay-env/lease-and-push), and how to use it (agentbox control-plane setup|set-url|status|add|workerwith examples). Read this before the roadmap/backlog for the high-level "what + how to use".