No description
Find a file
Jan-135 922248d443 Use generic paths in the documentation examples
Two examples added recently were pasted verbatim from the machine they were
written on, so the published documentation carried a real home directory and
real project names. Every other example in this repository uses my-project and
a placeholder home; these now do too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ73eU49FsML72CJdVDwwE
2026-09-11 23:11:43 +02:00
bin Add an end-to-end suite that runs against a real sandbox 2026-09-11 23:07:38 +02:00
config Cut the builder allowlist to the three hosts the installers reach 2026-09-11 22:05:23 +02:00
docs Use generic paths in the documentation examples 2026-09-11 23:11:43 +02:00
lib Cut the builder allowlist to the three hosts the installers reach 2026-09-11 22:05:23 +02:00
scripts Locate the clone without depending on the current directory 2026-09-11 22:18:38 +02:00
template Inline the briefing; Claude Code never loaded the import 2026-09-11 22:21:09 +02:00
tests Add an end-to-end suite that runs against a real sandbox 2026-09-11 23:07:38 +02:00
.gitignore Add an end-to-end suite that runs against a real sandbox 2026-09-11 23:07:38 +02:00
CHANGELOG.md Add an end-to-end suite that runs against a real sandbox 2026-09-11 23:07:38 +02:00
install.sh Let the builder's allowlist be set per build 2026-09-11 21:58:22 +02:00
LICENSE Initial Agentbox setup 2026-08-06 15:29:16 +02:00
README.md Use generic paths in the documentation examples 2026-09-11 23:11:43 +02:00
VERSION Read the policy verdict from its prefix, not from the whole output 2026-08-06 18:19:45 +02:00

Agentbox

Agentbox creates one isolated Docker Sandbox microVM per Git project and adds a small workflow around Claude Code and OpenAI Codex.

The goal is not to make prompt injection impossible. The goal is to keep a compromised coding agent away from the host, other projects, SSH keys and the real Git remote.

Security model

Host repository
├── origin                         → Forgejo/GitHub, controlled by the user
└── sandbox-<project>-agent        → private clone inside the microVM
                                     (created by sbx in clone mode)

Project microVM
├── working clone                  → agent can edit and commit
├── host remote                    → read-only host repository
├── no origin                      → removed on purpose
├── Claude Code
├── Codex CLI
└── network                        → default deny plus explicit allow rules

In clone mode, the host repository is mounted read-only at /run/sandbox/source. The agent works in a separate clone whose only remote is host, pointing at that read-only mount. Agentbox removes origin inside the VM whatever it points at, so there is no configured path from the agent to the real Git server.

sbx restores an origin pointing at the real upstream every time a sandbox starts, so removing it once at create time is not enough. agentbox open and agentbox shell re-assert the layout before handing you the session, and claude-agent and codex-agent do it again immediately before the agent runs. host-fetch does the same from inside the VM at any time.

A sandbox started with sbx run directly, bypassing agentbox, has origin configured until something re-asserts the layout.

Commits move in one direction at a time:

Host → VM    git fetch host          (inside the VM, or: agentbox sync)
VM → Host    agentbox diff / merge   (on the host, via sandbox-<name>)
Host → real upstream                 git push origin, by you, after review

Requirements

  • Linux on x86_64
  • CPU virtualization enabled
  • readable and writable /dev/kvm
  • Git
  • Docker Sandboxes (sbx)
  • a global Locked Down network policy
  • for building the template: curl, install, mktemp on the host

This repository was tested on Omarchy/Arch Linux with:

sbx v0.39.0
Claude Code 2.1.236
Codex CLI 0.146.1

Docker Sandboxes is Early Access. Agentbox uses a large part of the sbx command surface, so a different version may behave differently. agentbox doctor warns when the installed version is not the tested one.

Quick start

Install the CLI and build the clean agent template:

git clone <this-repository>
cd agentbox
./install.sh --build-template

Make sure ~/.local/bin is in PATH, then verify:

agentbox doctor

In an existing, clean Git repository:

cd ~/dev/projects/my-project
agentbox create

Agentbox stops before creation when it finds common secret filenames such as .env, private keys or credentials.json. Review them rather than blindly overriding the check.

Inside the microVM:

claude-agent
# or
codex-agent

On the first start, authenticate each agent inside the project VM. Never save a logged-in project sandbox as the shared template.

If a login fails with 403, the network allowlist is missing a host. See Troubleshooting.

Daily workflow

The agent edits and commits inside the microVM:

git add .
git commit -m "Implement feature"

Review on the host:

agentbox diff

Accept with a fast-forward merge:

agentbox merge
git push origin

Make new host commits visible inside the VM:

agentbox sync

Then inside the VM, on whatever branch you are on:

git merge --ff-only "host/$(git branch --show-current)"

main, master and project-specific branch names all work; nothing in agentbox assumes a particular default branch.

Agent instructions

The VM has a deliberately unusual posture: a read-only host remote, no origin, no way to push, and a default-deny network. An agent that has not been told any of that will try to push, invent workarounds for a blocked package registry, and call the work done while it is still uncommitted.

Agentbox tells it instead, in two layers.

The sandbox briefing is the same for every project and ships in the template as /etc/agentbox/agent-briefing.md. claude-agent and codex-agent render it into ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md inside the VM before they start, together with the current state of the clone: workspace, branch, remotes, whether host commits are waiting, whether anything is uncommitted. Both agents load those files automatically.

It is never written into your project. It would otherwise show up in every agentbox diff and end up pushed to the real remote.

Read it yourself at any time inside the VM:

agentbox-orient

The project instructions are yours, one file per repository:

agentbox init

That writes agentbox/README.md with the sections worth filling in: how to build and test, which hosts the network allowlist needs, conventions, what counts as done, known traps. The briefing tells every agent to read it first.

Commit it. The agent works in a clone, so only committed files reach it. Until then it is visible only through the read-only host mount, which is a fallback rather than the intent.

Agentbox never edits or overwrites the file after creating it.

Updating a sandbox that already exists

A sandbox keeps the template it was built from, so an edit to the briefing does not reach it on its own. It does not need a rebuild either:

agentbox update           # this project
agentbox update --all     # every sandbox you own

That copies the current briefing, agentbox-orient, agent-workspace-init and both agent wrappers into the VM, then re-runs the workspace init so the remotes are re-asserted and the memory files rewritten. Files move host → VM only, the same direction as the read-only source mount; it opens no path out of the sandbox.

A stopped sandbox is started detached, updated, and stopped again — stopped is the normal resting state, so an update that skipped those would reach almost nothing. One that is already running is left running, because stopping it would kill whatever session is using it. Either way the sandbox ends as it was found.

What it cannot move is what the builder installs from the network:

in place, no rebuild    the briefing, agentbox-orient,
                        agent-workspace-init, claude-agent, codex-agent
rebuild and recreate    Claude Code, Codex, the base image

For those:

./scripts/build-template.sh --force
agentbox remove && agentbox create

Rebuilding the template is still worth doing so new sandboxes carry the briefing from the start. agentbox create warns when a sandbox comes up without one, and points at agentbox update.

Sandbox names

The sandbox name is derived from the project directory and stays readable:

~/dev/projects/my-project  →  my-project-agent

It never contains a hash, id or random suffix. That means two projects whose directories have the same name would map to the same sandbox. Agentbox records which project a sandbox was created for and refuses to act when the name belongs to a different project, instead of silently opening the wrong VM.

Give the second project its own readable name:

AGENTBOX_SANDBOX_NAME=my-other-project-agent agentbox create

See what is on the host and which rows agentbox manages:

agentbox status --all
SANDBOX                    STATE     AGENTBOX
my-project-agent           stopped   /home/you/dev/projects/my-project
other-agent                stopped   not registered
old-agent                  stopped   registered path is gone: /home/you/dev/old

A sandbox that is not registered predates the registry or was created with sbx directly. Every command that walks the registry skips it, agentbox update --all included; run agentbox update from its project directory to adopt it.

A record whose project directory is gone is stale. Drop it without touching the sandbox:

agentbox forget old-agent

forget refuses while the project directory still exists, because that record is the only thing pointing at a live sandbox; agentbox remove is what deletes a sandbox you no longer want.

Later commands find that sandbox again automatically, because the mapping is stored. Set the variable through direnv or a shell alias if you prefer to be explicit every time.

Commands

agentbox init [path]             Write agentbox/README.md for this project
agentbox create [path]           Create and open a sandbox
agentbox open [path]             Open its default shell
agentbox shell [path]            Open an interactive Bash shell
agentbox fetch [path]            Fetch agent commits to the host
agentbox diff [path] [branch]    Review commits and changes
agentbox merge [path] [branch]   Fast-forward the host branch
agentbox sync [path]             Fetch the read-only host remote in the VM
agentbox status [path] [--all]   Show this project's sandbox and Git state
agentbox policy [path]           Show effective network rules
agentbox allowlist [path]        List this sandbox's network allowlist
agentbox allow <url> [path]      Allow a host for this sandbox only
agentbox deny <url> [path]       Remove a host from this sandbox's allowlist
agentbox stop [path]             Stop without deleting state
agentbox remove [path] [--force] [--yes]
                                 Delete the project VM
agentbox update [path] [--all]   Replace the agentbox files in a live sandbox
agentbox forget <name> [--force] Drop an ownership record, keep the sandbox
agentbox doctor                  Check prerequisites
agentbox version                 Print the agentbox version

Commands that need the VM to be up (fetch, diff, merge, sync) check first and say so plainly when it is stopped:

There is no active Agentbox session. Start one with: agentbox open

agentbox remove fetches from the sandbox first and refuses to delete while commits exist that the host does not have. Use --force to discard them anyway, and --yes to skip the interactive confirmation in scripts. After the checks pass it stops the sandbox the regular way before deleting it, and it prefers plain sbx rm so you keep sbx's own confirmation: that command's --force would also override an in-use sandbox, which is a different decision from discarding commits.

sbx refuses to delete when there is no terminal to confirm at, so in a script agentbox falls back to sbx rm --force — after it has asked its own question and stopped the sandbox itself, which is what that flag would otherwise have overridden.

Inside the VM:

claude-agent                     Claude Code, started without a host API key
codex-agent                      Codex, started without a host API key
host-fetch                       Re-point and refresh the read-only host remote
agentbox-orient                  Print the briefing and the current Git state

host-fetch and agent-workspace-init are the same program. Running it makes new host commits visible as host/<branch> and re-asserts the remote layout: one remote named host, no origin.

Templates

Templates are addressed by the fully qualified name that sbx template ls prints:

docker.io/library/jan-agent-shell:v1

A short name is normalised to that form, so AGENTBOX_TEMPLATE=jan-agent-shell:v1 and the full spelling mean the same template. A custom registry is kept as written:

registry.example.com/team/agent-shell:v2

Detection compares the canonical names exactly. :v1 never matches :v10, and agent-shell never matches other-agent-shell.

Configuration

~/.config/agentbox/config is created by the installer from config/agentbox.conf.example.

The file is parsed, not sourced: only KEY=VALUE lines and comments are accepted, only known keys are allowed, and a typo is an error rather than a silently ignored line. Nothing in it is executed.

bin/agentbox and scripts/build-template.sh read the same file, so AGENTBOX_TEMPLATE means the same template everywhere and the export filename is derived from it (team-shell:v2 exports as team-shell-v2.tar).

Network

No rule agentbox writes is ever global. Every sbx policy allow it runs is scoped with --sandbox, to one project sandbox or to the throwaway template builder. Nothing it does widens egress for the rest of your sandboxes.

The builder's allowlist exists only while the template is being built, because the installers have to be downloaded. It is gone with the builder. Set it per build, without editing the config file:

./scripts/build-template.sh --network "claude.ai,downloads.claude.ai,registry.npmjs.org"
./install.sh --build-template --builder-network "claude.ai,registry.npmjs.org"

Project sandboxes get a narrow allowlist covering the AI providers only. Package registries such as registry.npmjs.org, github.com and pypi.org are not allowed by default, so npm install inside a fresh sandbox will fail until you allow the host you need.

Find out what was blocked and allow exactly that, for one sandbox:

sbx policy log
agentbox allow https://registry.npmjs.org/express/-/express-4.18.2.tgz

allow takes what you actually have in front of you. A blocked request is reported as a URL, so the scheme, userinfo, port, path and query are stripped down to the host sbx wants:

https://registry.npmjs.org/express/-/express.tgz  ->  registry.npmjs.org
https://user@example.com:8443/path?q=1           ->  example.com:8443
*.npmjs.org                                      ->  unchanged

It always writes a rule scoped to this sandbox. sbx policy allow on its own defaults to the global scope, which would widen every other project too.

Review and undo:

agentbox allowlist                  # just the network rules, and where each comes from
agentbox deny registry.npmjs.org

deny removes the allow rule rather than adding a deny rule: under default-deny, a host nothing permits is blocked. It then checks what is actually true and says so, because a rule from a kit or another policy source cannot be removed this way and the host may still be reachable. In that case it prints the explicit sbx policy deny that would override it.

A bare * or ** is refused.

Every allowed host is also a possible exfiltration channel. That includes the AI provider domains: an agent that has been talked into it can encode data into a request to a domain you deliberately allowed. A narrow allowlist reduces the number of such channels; it does not remove them.

Template portability

scripts/build-template.sh creates:

~/.local/share/agentbox/exports/jan-agent-shell-v1.tar

Copy that file privately to another machine and import it with a full path:

sbx template load ~/.local/share/agentbox/exports/jan-agent-shell-v1.tar

The export is a full filesystem image built for x86_64. It is not portable to another architecture; build the template locally there instead.

The safer default for friends is to build the template locally from this repository. Do not commit template exports to Git. A saved template captures the complete filesystem, so a mistakenly logged-in builder could leak tokens. The builder is audited for credential files before it is saved, and the build fails if any are found.

The build is automated and repeatable, not fully reproducible: Codex is pinned exactly, but the base image tag and the Claude Code installer channel can change between runs. See Architecture.

Tests

Two suites, for two different questions.

./tests/run-tests.sh

Runs entirely offline against a fake sbx and throwaway Git repositories in a temporary directory. It never touches a real sandbox, template or your configuration, and it finishes in seconds. This is the one to run while you work.

./tests/e2e.sh

Runs against a real sbx, a real microVM and a real Git repository: it scaffolds a throwaway project in .e2e-workspace/, creates a sandbox, and checks the whole chain — the briefing reaching the agent, the remote layout, the allowlist commands, work moving back to the host, update, and removal refusing to discard unmerged commits. It takes a few minutes and needs KVM.

The offline suite cannot see sbx changing its behaviour underneath agentbox. That is how the origin remote came back on every sandbox start without a single test failing; the fake sbx did not restore it, so nothing noticed. The end-to-end suite exists for that class of bug.

It only ever touches a sandbox named agentbox-e2e-agent, refuses to start if one already exists, uses its own ownership registry under .e2e-workspace/, and refuses to act on a path outside that directory. Your sandboxes are not reachable from it.

./tests/e2e.sh --keep     # leave the sandbox and workspace behind to inspect

.e2e-workspace/ is gitignored and recreated from scratch on every run. A failed run keeps it so you can look at what it built.

Documentation

License

Agentbox itself is MIT licensed. Docker Sandboxes, Claude Code and Codex are separate products with their own licenses and terms.