- Shell 100%
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 |
||
|---|---|---|
| bin | ||
| config | ||
| docs | ||
| lib | ||
| scripts | ||
| template | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| install.sh | ||
| LICENSE | ||
| README.md | ||
| VERSION | ||
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,mktempon 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.