Introduction
aibox runs Claude Code in a small virtual machine on your computer.
Claude Code can read and change files, run commands and access the internet. That is what makes it useful, but it also means a bad prompt or a malicious file in a repository can make it do things you did not want. aibox limits the damage. Claude Code only sees the files you give it, and it can only connect to hosts you allow.
You can use aibox in two ways:
aibox runstarts Claude Code in your project folder, like you would on your machine. Changes show up in the folder right away. See Running Claude Code.aibox taskhands Claude Code a job to do on its own. It works on a clone of the last commit and you get its commits back to review. The VM is deleted afterwards. See Tasks.
You can also mount other folders into the VM, read-only.
With aibox run, the VM connects to your machine like this:
%%{init: {"flowchart": {"nodeSpacing": 60}}}%%
flowchart LR
subgraph vm["VM"]
claude["<br><br>Claude Code<br><br>"]
end
subgraph machine["Your machine"]
terminal["Your terminal"]
project["Project folder"]
mounts["Mounts and skills"]
subgraph aibox["aibox"]
proxy["Proxy"]
broker["Git broker"]
end
end
hosts["Hosts on the allow list"]
repos["Repositories on the git list"]
claude <-->|"SSH over vsock"| terminal
claude <-->|"virtio-fs, 9p on Windows"| project
claude ---|"the same, read-only"| mounts
claude <-->|"vsock"| proxy
proxy ---> hosts
proxy --> broker
broker -->|"your git login"| repos
The VM has no network card. vsock connects it to your terminal and to a proxy in aibox. On a Linux kernel without vsock namespaces, a serial port does that. Apart from that, it only shares folders with your machine. With git in the config, a broker adds your git login to the VM’s git requests, so the login never enters the VM.
aibox runs on Linux with QEMU, on macOS with Virtualization.framework and on Windows with Hyper-V. The VM boots in about a second.
Why a VM
There are other ways to fence in Claude Code. Here is why aibox uses a VM instead.
Claude Code has a sandbox of its own. It’s quick to turn on and good for cutting down on permission prompts. But it only covers shell commands. Claude Code’s file tools, MCP servers and hooks still run directly on your machine. Anthropic’s sandbox runtime wraps the whole process, but it still shares your kernel, and you configure the paths and hosts yourself.
A Docker container, like Anthropic’s dev container, puts the whole process in a box. But containers share the kernel with your machine, so a kernel bug is a way out. The dev container’s firewall runs inside the same container as Claude Code, and it allows the IP addresses it looked up when the container started. On a Mac, Docker Desktop runs all containers in one shared Linux VM.
A VM has its own kernel, so a kernel bug alone is not enough to get out. Something has to break the hypervisor or the programs that serve the VM. aibox keeps that VM small: no network card, no kernel modules, and a proxy on your machine as its only way to the network. It also locks down the programs that run the VM on your machine.
Apple’s container tool also runs each container in its own VM. But aibox can’t lock down its runtime, and with it aibox would have to reach the VM through a socket that other programs of yours could reach too. So on macOS aibox runs the VM with Virtualization.framework itself, like it runs QEMU on Linux.
Docker Sandboxes is the closest to aibox. It also runs agents in a microVM, with a proxy that only lets allowed hosts through and keeps API keys on your machine. You need to sign in to Docker to use it. aibox needs no account, and it has a git broker that only lets pushes through to the branches you name.
What aibox does not do
aibox keeps Claude Code away from the rest of your machine. It does not check
what Claude Code does with the things you give it. With aibox run the VM can
change anything in your project folder, .git included, and a task can use
every host and secret you allow it. Read
What the VM writes, Security and the
Threat model to see where the limits are.
Installing
Homebrew
On a Mac with Apple silicon or on Linux on amd64, install aibox with Homebrew:
brew install --cask the127/tap/aibox
On Linux, Homebrew installs just the binary. Install QEMU, virtiofsd and bubblewrap from your distribution, not from Homebrew. Ubuntu only lets its own bubblewrap make the namespaces that the sandbox needs.
sudo apt install qemu-system-x86 virtiofsd bubblewrap # Debian, Ubuntu
sudo dnf install qemu-system-x86-core virtiofsd bubblewrap # Fedora
aibox takes the first of these programs on your PATH, and Homebrew puts
its own folder first. If you installed any of them with Homebrew, uninstall
them there.
The rest of Linux applies too.
Linux
Each release has a deb and an
rpm for amd64. They pull in QEMU, virtiofsd and bubblewrap. There is also a
tar.gz with just the binary, and then you install those three yourself.
aibox needs:
- Linux 6.7 or newer. That’s the first Linux whose Landlock can limit aibox’s network connections.
- access to
/dev/kvm - virtiofsd on your
PATHor at/usr/libexec/virtiofsd, which is where Debian 13, Ubuntu 24.04 and Fedora put it
QEMU and virtiofsd from Nix work too.
On Linux 7.0 or newer with vsock (CONFIG_VSOCKETS and
CONFIG_VHOST_VSOCK), the VM reaches aibox over vsock. Most distributions
have vsock as a module. aibox opens /dev/vhost-vsock to make the kernel
load it. If that doesn’t work, load it with sudo modprobe vhost_vsock.
On an older kernel, or one without vsock, the VM reaches aibox over a
virtio serial port instead. The first line of console.log then says why.
When loading vsock would give the VM vsock, aibox also tells you in the
terminal.
See The network.
Windows
aibox runs on Windows 11 on amd64 with Hyper-V, which Windows 11 Pro,
Enterprise and Education have. Download aibox.exe from the
releases.
Hyper-V is off until you turn it on. Do that once, from a PowerShell you opened as administrator, and restart Windows:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
aibox service install tells you when Hyper-V is off, and so does aibox run when the service can’t make a VM because of it. If you turn Hyper-V
off later, the service no longer starts: run aibox service install
again to see why.
Hyper-V only lets administrators start VMs, so aibox comes with a Windows service that starts them for you. Install it once, from a terminal you opened as administrator:
aibox service install
That downloads the VM image, puts the service under C:\Program Files\aibox
and its kernel and root disk under C:\ProgramData\aibox, and starts it.
From then on you run aibox run and aibox task as yourself, without
administrator rights. Run aibox service install again after you update
aibox: aibox runs only with a service of its own version, since the
service decides how the VM is made. Each aibox run and aibox task
checks that before it makes a VM, and says so when they differ. Run
aibox service uninstall to remove it all.
Nix
The flake in this repository has a package of the latest release, for Linux on amd64 and macOS on Apple silicon. On Linux it brings QEMU, virtiofsd and bubblewrap with it.
nix profile install github:The127/aibox
You can also run it without installing:
nix run github:The127/aibox -- run
The VM image
Our packages don’t include the VM image. The first aibox run downloads the
image for its release, which is about 150 MB, and keeps it in
~/.aibox/image/<version>. aibox only takes the image whose SHA-256 was
built into the release. After an upgrade it downloads the new image and
removes the old one.
Packaging aibox
If you package aibox, you can ship the VM image with it. Then your users don’t download it, and your package manager updates it together with aibox.
Put vmlinuz and os.ext4 from the release’s aibox-image_<arch>.tar.gz
into a folder that all users can read. Our release binaries look in
/usr/lib/aibox on Linux and in /opt/homebrew/lib/aibox on macOS. If you
build aibox yourself, set the folder at build time:
go build -ldflags "-X github.com/the127/aibox/internal/image.systemDir=/usr/lib/aibox" ./cmd/aibox
When that folder has both files, aibox uses them and downloads nothing. If
the folder is there but a file is missing, aibox says so and downloads the
image instead. --image still takes precedence.
Ship the image of the same release as the binary. aibox checks the kernel against the SHA-256 built into the release and refuses to start with any other. If you build aibox yourself, build in the SHA-256 of each kernel too, or aibox boots any kernel:
go build -ldflags "-X github.com/the127/aibox/internal/image.kernelDigests=amd64:<sha256>,arm64:<sha256>" ./cmd/aibox
Note that aibox doesn’t check the root disk yet.
Checking a download
Each release has a checksums.txt with the checksums of all its files. It is
signed with cosign, without a key. To check a download, run this in the
folder you downloaded it to:
cosign verify-blob checksums.txt \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/The127/aibox/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum --ignore-missing -c checksums.txt
The first command checks that the release workflow of this repository signed
checksums.txt. The second checks your files against it.
Running Claude Code
Go to your project folder, let aibox suggest the hosts its tools need, and start it:
cd ~/projects/my-app
aibox config suggest
aibox run
The VM only reaches the hosts you allow. aibox config suggest looks at the
files in your project, like go.mod or package.json, and offers the
presets for them. It asks before it adds
them.
aibox run boots the VM, mounts the project at /project and starts Claude
Code there. Log in like you would on your machine. aibox keeps the login for the
next run in this project.
Each project has a VM home of its own, so you log in once for each project. aibox doesn’t copy the login from one project to another. That would mean aibox handles your login, and Anthropic’s terms don’t allow anyone but you to do that.
aibox won’t start in your home folder or above it, because the VM would see all your files.
| Flag | What it does |
|---|---|
--shell | Opens a shell in the VM instead of Claude Code. |
--resume <id> | Goes on with the Claude Code session that has this ID. When you quit Claude Code, aibox prints the command to go on with the session you were in. |
--memory | Sets the VM’s memory in MiB. The default is 2048, or what the config says. |
--cpus | Sets the VM’s number of CPUs. The default is 2, or what the config says. |
--image | Boots a folder with vmlinuz and os.ext4 instead of the image your package installed or aibox downloaded. aibox doesn’t check its kernel. |
--no-sandbox | Runs QEMU without its bubblewrap sandbox, for debugging. Linux only. |
What aibox keeps for a project
Each project gets a folder in ~/.aibox/projects, named after the project’s
path. Letters and digits stay, each / becomes -, and every other
character becomes _ and a code, so /work/my-app gets
-work-my_2dapp. A very long name is cut and ends in a hash of the path.
The folder holds:
| File | What’s in it |
|---|---|
config.yaml | The project config. |
home/ | The VM’s home folder, so Claude Code’s login and ~/.claude survive restarts. Other projects don’t see them. |
state.ext4 | A disk for the VM’s /usr/local and ~/.cache. |
console.log | The kernel and init messages of the last run. |
proxy.log | Every host and port the proxy connected to or refused, the first time in each run. When the run ends, a summary says how often and when last. A killed aibox writes no summary. |
tasks/ | The project’s tasks. |
path | The project’s path, for aibox projects list. |
run.lock, tasks.lock | Locked while the project runs or a task runs, so that a reset or a remove leaves it alone. |
Tools you install into /usr/local in the VM stay on the state disk. They
survive restarts and new images. Caches with lots of small files, like Go’s
build cache or pip’s, live there too. The shared home folder would be slow
for them.
The disk only takes up space on your machine as it fills, up to the size you
set with disk. The VM formats it on the first boot. It
won’t format a disk that has data on it but no ext4 file system, so a broken
disk never gets wiped.
Starting over
The VM’s home folder and state disk stay between runs. That’s how your
login and your tools survive. But it also means a session can leave
something behind for the next one. That might be a broken tool or a full
disk, or a line in ~/.claude/CLAUDE.md that something in the project
slipped in. To start the VM over, run this in the project folder:
aibox projects reset
This clears the VM’s home folder and deletes its state disk. The next run
makes a new disk, at the size disk says. Your config,
the logs and the tasks stay, and so does Claude Code’s login. Claude Code
asks for your color theme again, since its other settings are gone. Your
old sessions are gone too, so --resume can’t go back to them.
If you think a session was steered, add --logout. Then the login goes
too. The VM can write the login file, so a steered session could have put
someone else’s login there, and your next sessions would work in their
account.
aibox asks before it changes anything. Without a terminal, pass --yes.
It refuses while the project runs.
Removing a project
aibox never removes a project’s folder on its own. To see the projects it keeps a folder for, run:
aibox projects list
To remove everything aibox keeps for a project, run this in the project folder, or pass the project’s path:
aibox projects remove
aibox projects remove ~/projects/old-app
That removes the config, the VM’s home folder with the login, the state disk, the logs and the tasks. Your project folder itself stays as it is. This works for a project whose folder you already moved or deleted too. aibox asks first, and refuses while the project or one of its tasks runs.
In your IDE
Claude Code can work with your IDE, like VS Code or GoLand: it sees the code you select, shows its changes as a diff in the editor and reads the errors the IDE reports. On your machine the IDE’s Claude Code extension or plugin does this through a server that only listens on your machine. The VM can’t reach it, and it shouldn’t, because that server also runs code and opens files anywhere.
aibox can link the two for you. It’s off until you turn it on in the project config:
ide: true
Then open the project in your IDE and start aibox run in its terminal.
Claude Code in the VM connects to the IDE by itself. /ide shows it as
connected. aibox only passes on what the IDE integration needs, and only for
files in the project:
- the code you select in a project file
- diffs of project files in the editor
- the errors the IDE reports for project files
Everything else stays out. Claude Code can’t run code through the IDE, and it doesn’t see what you select in files outside the project.
Note that aibox looks for the IDE when the VM starts. If you open the IDE
later or restart it, start aibox run again. Also, the IDE talks to one
Claude Code at a time. Claude Code on your machine and the one in the VM
take the link from each other.
Approve Claude Code’s changes in the terminal, and don’t type into its diff in the IDE. When you accept or edit a diff in the IDE, the IDE writes the file itself, and the VM could have turned it into a link to a file outside the project. See What aibox does not protect.
aibox has been tested with VS Code and GoLand. Other JetBrains IDEs use the same plugin and should work too.
How it works
The IDE extension writes a file to ~/.claude/ide with its port and a
token. aibox looks there for an IDE that has the project open. It writes a
file like that into the VM’s ~/.claude/ide, with a token of its own and
port 3131, and sets CLAUDE_CODE_SSE_PORT so that Claude Code connects by
itself. The init forwards port 3131 to the proxy, and the proxy hands the
connection to aibox’s IDE bridge. The bridge connects to the IDE with the
IDE’s token, which never gets into the VM.
The bridge reads every message and passes on only the ones it knows, with
/project in paths turned into the project’s path on your machine and back.
It refuses paths outside the project, also through symlinks, and gives each
request an ID of its own. proxy.log shows what it held back.
Pasting images
Press Ctrl+V in Claude Code to paste the image on your clipboard, like a screenshot. Pasting text works like in any terminal, and so does copying.
On Windows, press Alt+V, as in Claude Code on Windows. Windows Terminal and the Windows console paste on Ctrl+V themselves, so the key never reaches aibox. aibox passes Alt+V on to the VM as Ctrl+V, so Alt+V in another program in the VM is Ctrl+V there too.
The VM can’t read your clipboard whenever it likes. If it could, it would
see every password you copy. aibox lets it read the clipboard only right
after you press Ctrl+V, for 5 seconds, and only one image. If you don’t want
that either, set clipboard: false in the
project config.
Note that aibox watches for the key, not for Claude Code. Ctrl+V in another program in the VM, like bash or vim, lets the VM read the clipboard too. If you bind image paste in Claude Code to another key, aibox doesn’t see it and pasting images stops working.
On Linux aibox reads the clipboard with wl-paste under Wayland and with
xclip under X. Install the one you need on your machine. On macOS it uses
osascript.
How it works
aibox reads what you type before it goes to the VM. It looks for Ctrl+V as the terminal sends it: as a control character, in the kitty keyboard protocol or with xterm’s modifyOtherKeys. Text you paste into the terminal doesn’t count. On Windows it looks for Alt+V the same ways, and sends the VM Ctrl+V in its place. It does that only when Alt+V comes in one read, as it does from a terminal: holding back the escape it starts with until the next read would hold back the Escape key too.
In the VM, xclip and wl-paste are aibox’s init under another name. When
Claude Code runs them for Ctrl+V, they ask aibox on port 3132 of the
loopback. The first asks which types the clipboard holds, the second asks
for the image. They answer nothing else, so Claude Code can’t read text from
your clipboard.
Once the VM runs, aibox can’t start programs any more. So before the VM starts, aibox starts a helper, a second aibox. The helper reads the clipboard when aibox asks and does nothing else. It ends with aibox.
On macOS
The VM runs arm64 Linux, so Mac programs don’t run in it. If you mount a
tool from your Mac, like a Go SDK or /nix/store, the VM can’t run it. Get
arm64 Linux builds of your tools instead, or install them into /usr/local
in the VM, where they stay.
You can only run one aibox run per project at a time on macOS.
Virtualization.framework locks the state disk while the VM runs.
On Windows
The VM runs on Hyper-V. The service of aibox
starts it for you, because Hyper-V only lets administrators do that; aibox run itself needs no administrator rights. Windows programs don’t run in
the VM, which runs amd64 Linux.
You can only run one aibox run per project at a time on Windows too.
Hyper-V keeps the state disk open while the VM runs.
The project’s folder in ~/.aibox/projects is named after the path as
Windows spells it, so cd c:\users\me\repo and cd C:\Users\me\repo are
the same project. A junction, a symlink, a subst drive or a mapped
drive leads to the project it points to; on a mapped drive Z:\repo that’s
\\server\share\repo. That’s unlike Linux and macOS, where a project you
reach through a symlink is a project of its own.
The state disk, state.ext4, is a dynamic VHD on Windows, the kind of
disk Hyper-V attaches, which takes up space as the VM fills it. The VM
sees the same disk as on Linux.
The VM sees your folders as Windows keeps them. Names are the same in any
case, so Foo and foo are one file. Files made on Windows are not
executable in the VM until you chmod +x them there, and a checkout of
git for Windows has CRLF line endings, which bash scripts in the VM
can’t run with. Tools on your machine are Windows programs, which the VM
can’t run, so a mount of them is no use. Install the tools in the VM
into /usr/local instead: it’s on the state disk and stays.
How it works
aibox asks the service over a named pipe to make a VM with the memory, the processors and the kernel command line it wants. The service checks them against what the machine has and boots its own copy of the kernel and the root disk. It never opens a folder or a file you name: it has Windows tell it who you are, and gives the VM the state disk you hold open, and nothing else of yours.
Your project, home and mounts are served to the VM by a helper, a copy of aibox, over 9p on Hyper-V sockets, as you: the VM can do with them what you can, and no more. The terminal and the proxy go over the same sockets. aibox serves the terminal itself, and the proxy from another helper in an AppContainer. That helper hands the requests for the services of the proxy, such as the Claude API forwarder and the git broker, to aibox, which holds their keys, over a port of the loopback. The service lets the helper reach only that port and those of the allow list. Both helpers end with aibox. The console comes over a pipe the service makes for the VM and hands to aibox open: Hyper-V connects the VM to it, and no one can open it by name. When aibox ends, or is killed, the service ends the VM.
How it works
The terminal is an SSH session over vsock, or over a serial port on a Linux kernel without vsock namespaces. Inside the VM, the init runs Claude Code on a pseudo terminal. aibox connects to it, puts your terminal into raw mode and attaches it. When you resize your terminal, the VM gets the new size. When Claude Code exits, aibox exits with the same code.
Tasks
A task is a job Claude Code does on its own while you do something else. It runs in a fresh VM on a clone of the last commit. When it’s done, you get its commits back as a git bundle to review, and the VM is deleted.
%%{init: {"sequence": {"mirrorActors": false}}}%%
sequenceDiagram
participant You
participant aibox
participant VM
You->>aibox: aibox task "prompt"
aibox->>aibox: bundle the start commit
aibox->>VM: prompt and bundle, read-only
VM->>VM: clone the bundle onto aibox/task
VM->>VM: Claude Code works and commits
VM->>aibox: changes.bundle and results
Note over VM: deleted
aibox->>aibox: check the bundle
aibox->>You: fetch command
You->>You: fetch into a branch and review
aibox task --model sonnet "fix the flaky test in internal/proxy"
What a task gets
The VM gets a clone of HEAD with its full history, not your project
folder. Use --from to start from another branch, tag or commit instead.
Note that whatever --from names goes in with its full history. That
includes a stash or a commit you reset away. A file you deleted in an
earlier commit is still in that history.
Changes you haven’t committed, files git ignores, and branches and stashes
that --from doesn’t name stay out of the task. aibox refuses a shallow
clone, because it lacks the history.
Claude Code works on the branch aibox/task. It has all permissions,
because nobody is there to approve them. Git uses your name and email from
your git config. If Claude Code leaves changes uncommitted, aibox commits
them in one more commit by aibox <aibox@localhost>. Files git ignores are
left out.
The VM starts with an empty home folder and an empty disk, and both are
deleted at the end. Your skills, agents and commands in ~/.claude are
mounted read-only, like with aibox run, unless the config says none for
them. Your CLAUDE.md and your settings don’t come along. Other tasks
and aibox run never see the home or the disk, so you can run several tasks
at once.
The project config applies like it does for aibox run: allow, mounts,
skills, agents, commands, path, env, git, memory, cpus and
disk. The task gets
its own proxy.log. It doesn’t write to the project’s.
Before you run a task
A task can use everything the config gives it, and nobody stops it. It can
reach every host on the allow list and every port of your machine the list
names. It can use every secret in env. With GITHUB_TOKEN and
preset:github, for example, it can push to your repositories.
Claude Code is told to push or open a pull request only when your prompt asks for it, only to a repository your prompt names by URL, and only to a new branch. Nothing enforces that. A pushed branch or pull request can start the repository’s CI, with its secrets.
Text Claude Code reads, in the project or on the internet, can steer it.
When it says what it published, that’s only its word. aibox lists the hosts
the proxy connected to, but not what went through those connections. A push
to github.com and a fetch from it look the same.
So before you run a task, remove the hosts, ports and secrets it doesn’t need from the config. To let a task fetch and push without a token in the VM, and only to branches you name, use git in the config.
Flags
| Flag | What it does |
|---|---|
--from | The branch, tag or commit to start from. The default is HEAD. |
--file, -f | Adds the text of a file to the prompt, after the arguments. - reads stdin. |
--model | The model Claude Code uses. Without it, Claude Code uses its default. |
--max-turns | The most turns Claude Code takes. |
--max-budget-usd | The most Claude Code may spend by its own estimate, in US dollars at API prices. See Cost. |
--timeout | How long Claude Code may work before it’s stopped. 1h by default, up to 30 days. |
--memory, --cpus, --image and --no-sandbox work like they do for
aibox run.
Prompt from a file
You can use a plan you wrote as the prompt. --file adds the text of a file
after the arguments, and --file - reads stdin. Without any arguments, aibox
reads the prompt from stdin, unless stdin is a terminal.
aibox task --file plan.md
aibox task --file plan.md "do only step 2"
cat plan.md | aibox task
A few rules apply:
- A relative path, and every link in it, has to stay inside the folder you run aibox in. That way a link in a repository can’t make a task read some other file of yours. For a plan somewhere else, give an absolute path. An absolute path must not end in a link.
- The file has to be a plain file. For a pipe, use
--file -. - The prompt can be up to 1 MiB of UTF-8 text, the arguments and the file
together. A file in UTF-8 or UTF-16 with a byte order mark, as Notepad
or
>in Windows PowerShell write them, is fine too. The 1 MiB counts the file as it is saved, so a file in UTF-16 holds about half as much.
A plan you haven’t committed only reaches the task as its prompt.
It’s not a file in /project.
Login
A task starts with an empty home folder, so Claude Code is not logged in.
You give it a login through an environment variable instead. Add one of
these to env in the project config:
ANTHROPIC_API_KEYfor an API key from the Claude Console. Usage is billed to the key.CLAUDE_CODE_OAUTH_TOKENfor your claude.ai subscription. Runclaude setup-tokento make one.
env:
- CLAUDE_CODE_OAUTH_TOKEN
aibox never stores the token or the key. It takes the value from your shell when the task starts.
API key
Your API key never enters the VM. Claude Code gets a placeholder key and an
ANTHROPIC_BASE_URL that points to a local port. aibox listens there, swaps
in the real key and forwards the request to api.anthropic.com. So nothing
in the VM can read the key, send it somewhere or put it into a commit.
But while the task runs, it can still spend the key through aibox, on any request to the Claude API. Give the key a spend limit in the Console.
This only works for programs that use ANTHROPIC_BASE_URL. Other
programs in the VM only get the placeholder. aibox refuses a config that
sets ANTHROPIC_BASE_URL itself next to the key.
An interactive Claude Code still connects to api.anthropic.com and
platform.claude.com directly when it starts, without the key. Keep both on
the allow list, like a new config has them.
Subscription token
A subscription token goes into the VM as it is, because Anthropic’s terms don’t allow anyone but you to handle it. In the VM, Claude Code and every program the task runs can read it. If something steers the task, it can send the token to a host on the allow list, or write it into a commit or the transcript. Use a separate token or key just for tasks, so you can revoke it on its own.
Whether running unattended tasks is fine on your subscription is your own risk. See Anthropic’s Legal and compliance page. For lots of tasks, or tasks for a team, use an API key.
How the key forwarding works
The port is 127.0.0.1:3129. The proxy answers there itself and doesn’t
connect anywhere for it. It forwards each request under /v1/, and the
check Claude Code makes at /api/hello, to https://api.anthropic.com, no
matter which host the request names. It refuses paths with dot segments or
escapes. It drops any credentials the VM sent and adds the key. proxy.log
shows these requests as localhost:3129.
aibox reads the root certificates before it locks itself down. After that, its sandbox stops it from reading them, and on macOS from asking the system to check a certificate.
Results
When the task ends, aibox prints the hosts the VM connected to, how the task went, Claude Code’s last message and the command that fetches the changes into a branch:
17:09:25 aibox[259e07]: the VM connected to api.anthropic.com:443
17:09:25 aibox[259e07]: Claude Code exited with 0
17:09:25 aibox[259e07]: 4 turns in 8.321s, about 0.05 USD at API prices, ended by completed
17:09:25 vm[259e07]: | I added a "Purpose" section to the README and committed it as f3572af.
17:09:25 aibox[259e07]: the changes end at f3572af8c6df, fetch them with
17:09:25 aibox[259e07]: git -c transfer.fsckObjects=true fetch .../changes.bundle aibox/task:aibox/task-20261007-170916-259e07
Each line starts with the time, where the line comes from and the end of the
task’s ID. That way you can tell tasks apart when they share a log. vm
lines come from the VM and are only what the task claims. aibox lines come
from aibox on your machine.
On Windows the fetch command names the bundle with forward slashes, and
quotes it only as PowerShell, cmd.exe and Git Bash all read it, so you can
paste it into any of them. A path with $, `, % or ! in it has
no quotes all three read, so then the command is quoted for PowerShell.
Reviewing the commits
Treat the commits like a pull request from a stranger until you’ve read them. The authors on them prove nothing, because the task can set any author.
- Fetch them with the command aibox prints. aibox only prints it after
checking that the bundle has
aibox/taskand nothing else, and needs no commit other than the one the task started from. The fetch only adds that one branch. - Read the whole change with
git diff <commit> aibox/task-…, no matter what the single commits look like. Take<commit>from the lineaibox[…]: task … starts from …that aibox prints. Don’t take it fromresult.json, because the VM writes that file. The printed commit is the one aibox checked the bundle against.
Checking out, merging or rebasing the branch puts its files into your project folder. From then on, everything in What the VM writes applies.
Cost
The cost aibox prints is Claude Code’s own estimate, from the tokens it used at the API’s list prices. With an API key it’s close to what you’re billed. With a subscription you aren’t billed this amount. The task counts against your plan’s limits instead.
--max-budget-usd caps the same estimate, for Claude Code alone. Programs
the task starts, like another claude, don’t count. The only limit on time
or spending that aibox enforces itself is --timeout. With an API key, give
the key a spend limit in the Console.
The task folder
Each task has a folder tasks/<start time>-<id> in the project’s folder in
~/.aibox/projects. aibox prints the path on stdout, for scripts.
| File | What’s in it |
|---|---|
changes.bundle | The commits. Empty when the task made none. |
result.json | What the VM reports: the commit the task started from and the one it ended at, Claude Code’s exit code, whether it ran out of time, whether aibox committed leftovers, the files that were cut, warnings, the error of a failed step, and Claude Code’s last line with the cost and the turns. |
transcript.jsonl | Everything Claude Code did, as stream-json, cut at 256 MiB. |
claude.log | What Claude Code wrote to standard error, cut at 1 MiB. |
console.log | The kernel and init messages of the VM. |
proxy.log | Every host and port the proxy connected to or refused for the task, once, with the time it first happened. When the task ends, a summary says how often and when last. A killed aibox writes no summary. aibox writes this file, not the VM. It shows the name the VM asked for, not what went through. |
share/ | The prompt, the settings and the bundle of the start commit with its full history, until aibox tasks clean removes it. |
Next to the folder, tasks/<start time>-<id>.lock is locked while the task
runs, so aibox tasks clean leaves the task alone. aibox tasks clean --all
removes it with the folder, so use that rather than removing a folder by
hand.
aibox exits with 1 when the task didn’t finish. That happens when Claude Code failed or ran out of time, a step in the VM failed, the VM was stopped, or the VM sent results aibox doesn’t accept. The folder then has whatever the task left behind.
Removing old tasks
Every task keeps its folder, and with it a bundle of the project’s history. aibox never removes anything on its own. To clean up, run:
aibox tasks clean
This removes the bundles of the tasks that ended and keeps their results. Running tasks are left alone.
aibox tasks clean --all --older-than 720h
--all removes the whole folders of ended tasks, results included.
--older-than only cleans tasks that started longer ago than that. The
command also removes what’s left of tasks that failed while starting. If it
can’t check or clean a folder, it names it and exits with 1.
A task counts as ended once its aibox process ends. The VM ends with aibox, even when aibox is killed.
How a task runs
aibox task boots the same VM as aibox run, but without a terminal. The
kernel command line tells the init to run a task.
First, aibox writes the prompt, the settings and a bundle of the start
commit into the task’s folder. The VM gets that folder as a read-only share.
git can only bundle refs, so aibox makes the bundle from an empty repository
that borrows the project’s objects and has the commit as its HEAD. Your
project gets no new ref.
Your project folder and the project’s home folder are not shared with the
task. The mounts from the config and your skills, agents and commands are,
read-only like with aibox run.
Each task gets a new state disk, which the init formats. The home folder and
the project are folders on that disk. aibox deletes the disk’s file as soon
as the VM has opened it, so the disk is gone when the VM is. Tasks share a
lock on the project, so they can run next to each other and next to
aibox run, but aibox projects remove leaves the project alone while one
runs.
Then the init clones the bundle, removes the remote and switches to the
branch aibox/task. It runs claude --print as the user, with
bypassPermissions, stream-json output and
image/task.md
as an extra system prompt, followed by the part of
image/host for
the system your machine runs. On Windows a task gets a part of its own,
which warns about the names a checkout on Windows can’t hold. Finally it
commits whatever is left and bundles the branch.
aibox runs its git commands with hooks and fsmonitor turned off, because the repository came from the task. Filters the task sets up still run, but only inside the VM.
Each step runs in its own cgroup. When the step ends, the init kills the cgroup, so nothing a step started outlives it. The git steps before Claude Code get 10 minutes together, and the ones after get another 10. Claude Code gets the task’s timeout. aibox stops the VM at the latest 25 minutes after the timeout.
The results come back over the SSH session, as a tar on its standard output.
The init sends each file once it’s complete, and result.json last. Progress
goes over standard error.
aibox checks what comes back:
- It only accepts files it knows by name. Each one has to be a plain file, sent once, and within a size limit. If aibox refuses the results, it stops the VM.
- It reads the bundle’s header without git. It checks that the bundle has
aibox/taskand nothing else, and needs no commit other than the start commit. - It strips control characters from text the VM sends before it reaches
your terminal. It adds the time, the source and the end of the ID to each
line from the VM itself, so a line from the VM can’t pretend to be from
aibox.
aibox runpasses the VM’s terminal straight through, so this only applies to tasks.
aibox opens the result files before the VM starts. Once the VM runs, aibox
locks itself down and can’t open files anymore. For the same reason a task
can’t remove its input bundle when it ends, so aibox tasks clean does that
later. Each task locks a file next to its folder while it runs, and makes
the folder only once it holds the lock. That’s how aibox tasks clean
knows to leave a running task alone. The lock is next to the folder, not
in it, because Windows can’t remove a folder that holds an open file.
aibox 1.6 and before kept the lock in the folder, and doesn’t see one next
to it, so don’t run aibox tasks clean of an older aibox while a task of a
newer one runs. A lock whose folder never got made, by a task that failed
to start, goes with the next aibox tasks clean a minute later.
Project config
Each project has its own config at
~/.aibox/projects/<escaped path>/config.yaml. It lives outside the project
on purpose, so the VM can’t change it. The first aibox run writes it, with
comments and the hosts Claude Code needs, or from
your own starting config.
To change it, run:
aibox config edit
This opens the config in the same editor git would use: $VISUAL, then
$EDITOR, then vi, or Notepad on Windows. Pass --editor to pick
another one. When you close the editor, aibox checks the file and tells
you about mistakes.
An editor that returns right away, like code without --wait,
gets checked before you’ve made your changes.
Your own starting config
If you use the same hosts, mounts or memory in most projects, you don’t have to add them to each one. Write them into a template once:
aibox config edit --template
This opens ~/.aibox/config.yaml, which starts as a copy of the default
config. From then on, every new project gets a copy of it, comments
included, instead of the default. Projects that have a config already keep
it, and a change to the template doesn’t reach them. So each project’s
config still says everything its VM gets.
If the template has a mistake, aibox says so and starts no new project from it.
Here is a full example:
allow:
- api.anthropic.com
- claude.ai
- preset:go
- preset:github
- registry.example.com:5000
- "*.example.org"
memory: 4096
cpus: 4
disk: 32
mounts:
- ~/sdk/go1.26.8:/opt/go
skills: all
settings: none
ide: true
clipboard: false
path:
- /opt/go/bin
env:
- GOFLAGS=-mod=mod
- GITHUB_TOKEN
git:
- remote: github.com/owner/repo
fetch: true
push:
- aibox/*
git has its own page, Git.
allow
The hosts the VM can connect to. The proxy refuses everything else.
example.comallows port 443. Writeexample.com:8443for another port.*.example.commatches every subdomain ofexample.com, but notexample.comitself.preset:NAMEstands for the hosts a tool needs. See the table below.127.0.0.1:8123or[::1]:8123is a port on your machine. See Ports on your machine.
aibox refuses a name that resolves to an address in your machine’s own networks, like loopback, link-local or private addresses. The only way to allow such an address is to write it into the list.
A new config only allows the hosts Claude Code needs:
| Host | Why |
|---|---|
api.anthropic.com | the Claude API |
claude.ai | logging in with a claude.ai account |
claude.com | the sign-in page redirects through here |
platform.claude.com | login tokens |
mcp-proxy.anthropic.com | MCP connectors of a claude.ai account |
code.claude.com | looking up documentation |
These are the presets:
| Preset | Hosts |
|---|---|
preset:go | proxy.golang.org, sum.golang.org, storage.googleapis.com, vuln.go.dev, dl.google.com, go.dev |
preset:npm | registry.npmjs.org, registry.yarnpkg.com, nodejs.org |
preset:pypi | pypi.org, files.pythonhosted.org |
preset:cargo | crates.io, static.crates.io, index.crates.io, static.rust-lang.org |
preset:github | github.com, api.github.com, codeload.github.com, *.githubusercontent.com |
preset:docker | registry-1.docker.io, auth.docker.io, index.docker.io, production.cloudfront.docker.com |
When the proxy refuses a host that a preset allows, its message names the preset.
Suggesting presets
A new config doesn’t know which tools your project uses. To find out, run this in the project folder:
aibox config suggest
aibox looks at the names of the files in the folder and suggests the
tools they point to, with the files that show each one and the hosts of
its preset. It asks for each tool before it adds its preset to allow. Start aibox again for them to apply. Without a
terminal to ask, or when allow isn’t a list with one entry per line, it
only prints the lines to add with aibox config edit.
| Preset | Files |
|---|---|
preset:go | go.mod, go.work |
preset:npm | package.json, package-lock.json, yarn.lock, pnpm-lock.yaml |
preset:pypi | pyproject.toml, requirements.txt, setup.py, setup.cfg, Pipfile, uv.lock, poetry.lock |
preset:cargo | Cargo.toml, Cargo.lock |
A project may come from a stranger, so it must not pick its own
permissions. That’s why aibox only reads the names of plain files, and only
in the top folder, and never what’s in them. It only ever suggests these
presets, never a host, a port, a mount or a variable from the project.
preset:github and preset:docker are never suggested. A git remote or a
Dockerfile doesn’t mean the VM needs them.
Note that anyone can upload files to package registries, so a program in the VM can send data there too. Only add a preset the project needs.
Ports on your machine
Some servers only listen on your machine’s loopback, like a local MCP server. Once you allow the port, the VM reaches it at the same address:
allow:
- 127.0.0.1:8123
claude mcp add -t http my-server http://127.0.0.1:8123/mcp
Write the address the server listens on, 127.0.0.1 or [::1]. The VM only
has IPv4, so in the VM the port is at 127.0.0.1 and localhost either
way. That also means you can allow a port on only one of the two addresses.
You can’t write localhost. It means both addresses, and any user on your
machine could listen on the one the server doesn’t use. 0.0.0.0 and ::
aren’t allowed either, because they lead to the loopback too.
This works for any protocol over TCP, also for programs that don’t use the
proxy. A program in the VM can’t listen on such a port itself. Port 3128
can’t be allowed, because the VM’s proxy is there. With ide
turned on, 3131 can’t be allowed either, because Claude Code reaches your
IDE there.
Keep in mind that the server runs as you, on your machine, outside the VM,
and does what the VM asks it to. An IDE’s MCP server, for example, often has
no login and can run commands and change files outside the project.
Allowing its port lets the VM out. Only allow a port for a server you’d trust
with whatever the VM asks of it. For your IDE, turn on ide
instead of allowing its port.
How it works
aibox passes the ports to the VM’s init on the kernel command line. The init
listens on 127.0.0.1 in the VM for each port. It sends each connection to
the proxy as localhost and the port. For localhost the proxy doesn’t ask
DNS. It connects to the one loopback address the allow list names for that
port. That’s why programs that ignore the proxy variables reach the port
too. It also means the server sees the Host it expects. Many local servers
refuse other names, to guard against DNS rebinding.
memory, cpus
The VM’s memory in MiB and its number of CPUs. Without them, the VM gets
2048 MiB and 2 CPUs. The flags --memory and --cpus override both.
disk
The size in GiB of the disk that keeps the VM’s /usr/local and ~/.cache
between runs. The default is 16. The disk only takes up space on your
machine as it fills.
The size is fixed when the first run creates the disk. Changing it
later has no effect, until you start the VM over with
aibox projects reset, which makes a new disk.
mounts
Folders on your machine that the VM can read, but not write. Write them as
host:guest.
guest is the path in the VM. A host path starting with ~/ starts in your
home folder.
- The guest path has to stay clear of the folders the VM needs:
/project,/dev,/proc,/sys,/run,/tmp,/etc,/bin,/sbin,/lib,/lib64,/usrand/root. It also can’t overlap with other mounts. - The host folder can’t be your home folder or a folder above it, because the VM would see your keys and shell files.
- The host folder can’t be
~/.aibox, a folder in it or a folder above it, because the VM would see every project’s config and Claude Code login. - aibox compares folders rather than paths for these two rules, so a symlink or another spelling doesn’t get around them.
- A mount inside
/home/useris fine. The VM’s home is a folder aibox manages, not your home folder. Leave/home/user/.claudeitself alone, because the VM’s login lives there. - The VM caches names and attributes for the whole run. If you change the folder on your machine, a running VM may not see it.
- On macOS the VM runs arm64 Linux, so Mac programs don’t run in it.
- On Windows the VM runs amd64 Linux, so Windows programs don’t run in it,
and files made on Windows aren’t executable there. Mount data, not tools,
and install tools in the VM into
/usr/local. - The host folder can’t be the root of a drive or share either.
- On Windows write the host folder with its drive, like
C:\Users\me\sdk:/opt/go, or with forward slashes,C:/Users/me/sdk:/opt/go.~\works like~/. Don’t put the entry in double quotes, because YAML reads backslashes in double quotes as escapes; single quotes are fine.
skills
If you have skills in ~/.claude/skills, aibox mounts them read-only at the
same place in the VM’s home, so Claude Code finds them. That’s the default,
skills: all. To keep your skills out of the VM, set:
skills: none
To give the VM only some skills, mount them inside
/home/user/.claude/skills. A mount there takes the place of the whole
folder:
mounts:
- ~/.claude/skills/pdf:/home/user/.claude/skills/pdf
A symlink in the skills folder that points outside it doesn’t work in the VM.
aibox run and tasks get the same skills.
agents, commands, claude-md, settings
The VM gets more of your Claude Code setup, so it works like on your machine:
agentsandcommands: your folders~/.claude/agentsand~/.claude/commands, read-only, like your skills. Tasks get them too.claude-md: your~/.claude/CLAUDE.md.aibox runcopies it into the VM’s home at each start.settings: a few settings of your~/.claude/settings.json, likemodelandtheme.aibox runputs them into the VM’s own settings at each start, and keeps the others the VM has.
All four are all by default. To keep one out, set it to none:
claude-md: none
settings: none
A mount inside /home/user/.claude/agents or /home/user/.claude/commands
takes the place of that folder, like with skills.
aibox doesn’t remove what it put into the VM’s home before. If you delete
your CLAUDE.md or a setting, or turn one of them off, the VM keeps the last
copy. aibox projects reset starts the VM’s home
over. When the VM left something there that keeps a file from going in,
aibox says so and starts the VM anyway.
Only these settings come along: model, theme, attribution,
includeCoAuthoredBy, outputStyle and alwaysThinkingEnabled. Settings
that start a program or hold a secret stay on your machine: hooks, env,
apiKeyHelper, statusLine and the permissions. So do your plugins and
your MCP servers. The VM starts with its own.
A task starts with an empty home folder, so it doesn’t get your
CLAUDE.md or your settings.
ide
Claude Code can use the IDE that has your project open, like VS Code or GoLand. It sees the code you select, shows its changes as a diff in the editor and reads the errors the IDE reports. This is off by default, because aibox then passes messages between the VM and a program on your machine. To turn it on, set:
ide: true
aibox only passes on what’s needed, and only for files in the project. Read In your IDE before you turn it on, especially the part about diffs.
With ide turned on, your env can’t set CLAUDE_CODE_SSE_PORT, because
aibox sets it itself. Tasks never get the IDE, only aibox run does.
clipboard
When you press Ctrl+V in Claude Code, or Alt+V on Windows, it pastes the image on your clipboard. The VM can read your clipboard only right after you press the key, and only once. This is on by default. To turn it off, set:
clipboard: false
The VM reaches the clipboard on port 3132 of its loopback, so with the
clipboard on, allow can’t name that port. Tasks never get the clipboard,
only aibox run does. See Pasting images.
path
Folders in the VM to put in front of its PATH, for Claude Code and the
shell. For example /opt/go/bin from the mount above.
An entry can also be the name of a variable, like PATH or DIRENV_PATH.
It then stands for the folders in that variable on your machine, without the
relative and empty ones. That way the VM finds the tools of a nix develop
or direnv shell, as long as /nix/store is mounted at the same path.
On Windows the folders of such a variable are Windows folders, which the VM
doesn’t have, so it adds none of them.
env
Environment variables for the command in the VM. NAME=value sets a value.
NAME alone passes on the value your shell has when the VM starts. That’s
how you get a secret in without writing it into the file.
aibox refuses the variables it sets itself: AIBOX, HOME, USER,
LOGNAME, SHELL, PATH, TERM, LANG, the proxy variables,
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and GOMODCACHE.
ANTHROPIC_API_KEY is the exception. Its value is never passed on. With
aibox run and with tasks, the key stays on your machine and the VM gets a
placeholder. See API key.
Git
Claude Code often needs to fetch from or push to a repository. You could put
a token into env, but then everything in the VM can read it and use it for
anything the token allows.
The git list in the project config is the safer way. It names
the repositories git in the VM can fetch from and push to, using the git
login on your machine. The login never enters the VM.
git:
- remote: github.com/owner/repo
fetch: true
push:
- aibox/*
- feature/login
- remote: github.com/owner/private-module
fetch: true
remoteis the host and path of the repository, withouthttps://and without.git.fetch: trueallows clone and fetch.pushlists the branches a push can create or update.*matches any part of a name without a/. Soaibox/*allowsaibox/fixbut notaibox/a/b, and*on its own allowsmaintoo.
aibox refuses pushes to other branches, deletes, tags, and pushes from a shallow clone. If one branch of a push is refused, the whole push is. aibox also refuses repositories the list doesn’t name.
Using it
In the VM, use git like you normally would. git clone, git fetch and
git push work with the addresses https://HOST/PATH, git@HOST:PATH and
ssh://git@HOST/PATH.
aibox sends these requests to a broker on your machine. The broker adds your
login and passes on what the list allows. When it refuses something, git in
the VM shows you why. The broker writes what it refused, and every push it
passed on, into proxy.log.
%%{init: {"sequence": {"mirrorActors": false}}}%%
sequenceDiagram
participant git as git in the VM
participant broker as Broker in aibox
participant server as Git server
git->>broker: clone, fetch or push
broker->>broker: check it against the git list
alt allowed
broker->>server: the request, with your login
server-->>broker: answer
broker-->>git: answer
else refused
broker-->>git: why it was refused
end
The host doesn’t need to be on the allow list for git.
To go get a private module, set GOPRIVATE in env. For hosts other than
GitHub, Go first asks the host over HTTPS where the module is, so that host
needs to be on the allow list too.
SSH
Before the VM starts, aibox asks git on your machine for the login of each
repository, the same way git push over HTTPS would. If git has none, the
broker talks to the server over SSH instead, as git@HOST. aibox tells you
when it does that.
aibox asks ssh -G how ssh would reach the host. So these settings from
your ~/.ssh/config apply: HostName, Port, IdentityFile,
IdentitiesOnly, IdentityAgent, UserKnownHostsFile,
GlobalKnownHostsFile and HostKeyAlias.
It offers the same keys ssh would, from your SSH agent and from key files
without a passphrase. A key with a passphrase works through the agent, which
has to keep running while aibox does. aibox refuses a host that your config
reaches through ProxyJump or ProxyCommand.
On Windows aibox uses the agent of Windows’ OpenSSH, the service “OpenSSH
Authentication Agent”, unless SSH_AUTH_SOCK names another one. Windows
turns that service off by default: start it with Start-Service ssh-agent, set it to start by itself, and add your key with ssh-add.
The agent of Git Bash sets SSH_AUTH_SOCK to a path only Git Bash can
reach, so unset it for aibox. It reads
the known hosts of everyone from %ProgramData%\ssh\ssh_known_hosts, as
that ssh does. It asks the ssh that comes first on your PATH. The one of
Git Bash writes its paths as /c/Users/..., which aibox can’t read, so
keep Windows’ own ssh first.
Your known hosts check the server. aibox refuses a server that isn’t in
them, so connect to it with ssh once to add it.
The broker logs in once and runs git’s requests from the VM over that connection.
If neither HTTPS nor SSH works, aibox stops. It reads the logins and keys only once, so restart aibox when one expires.
For SSH, aibox keeps a connection to your SSH agent open while it runs, and
can connect to the SSH server’s port. To limit what a bug in aibox could do
with that, give aibox its own key, or use an agent that asks before it
signs, like with ssh-add -c.
Your name and line endings
The VM has a home of its own, without your git config. So aibox gives git
in the VM the name and email git uses in your project. aibox run also
gives it your core.autocrlf. Git for Windows sets that to true when it
installs, and checks out files with CRLF line endings. Without the
setting, git in the VM would see every file as changed, and a commit would
change the line endings of every file. A task doesn’t need it, since the
VM clones the repository itself.
Settings of your own in env win, such as GIT_AUTHOR_NAME, or
core.autocrlf in a GIT_CONFIG_KEY_*.
What the broker can’t stop
The broker can’t stop a force push to a branch you allowed. It sees which branches a push updates, but not whether the push drops commits. Protect the branches that matter on the server, for example with branch protection on GitHub.
What the VM pushes is code you haven’t read. A pushed branch can start the repository’s CI, with changes to its workflows and with its secrets.
With push but no fetch, the VM still sees the names of all branches and
tags and the commits they point to, because git needs them to push. It
doesn’t get the files.
With git, aibox sets GIT_CONFIG_COUNT, GIT_CONFIG_KEY_* and
GIT_CONFIG_VALUE_* in the VM, so you can’t set them in env.
How the broker works
aibox asks git on your machine for each repository’s login before the VM starts. It runs git outside any repository and without a terminal. That way the project’s git config, which the VM can write, has no say.
git in the VM gets a config that sends the HTTPS and SSH addresses of each
repository to http://127.0.0.1:3130. The init forwards that port to the
proxy like the ports on the allow list, and the proxy serves the broker
there.
The broker speaks git’s smart HTTP protocol. It only accepts git’s requests,
only for the repositories on the list, and only without dot segments or
escapes in the path. It sends each request to https://HOST/PATH.git, where
the address comes from the config and not from the request. It adds the
host’s login and drops the VM’s credentials and cookies. When the server
refuses the login or redirects, the VM gets an error. That way git in the VM
never asks for a login of its own. The broker also refuses a fetch the
config doesn’t allow.
For a push, the broker reads the commands at the start. They name each ref with its old and new commit. The broker only passes the push on when every command updates or creates a branch on the list. Otherwise it refuses all of them, because the pack belongs to the whole push. It also refuses deletes, tags, signed pushes, push options and pushes from a shallow clone. It answers in git’s format, so git in the VM shows why. A push larger than git’s buffer starts with a probe that is just a flush, which the broker passes on.
Over SSH
Before the VM starts, aibox asks ssh -G for the host name, port, key
files, agent and known hosts ssh would use. It reads the known hosts and the
keys. The server’s port gets added to the ports aibox may connect to.
The broker only asks the server for the key types your known hosts have for it. It logs in once per repository and opens a session on that connection for each request. If the connection breaks, it logs in again. If the server doesn’t open a session in time, the broker logs in again too. It closes the old connection once the requests still on it are done. If the server refuses a session, for example because it has too many open, only that request fails. The other sessions on the connection go on.
HTTP is stateless and SSH isn’t. So for each request from git in the VM, the broker runs the service on the server once:
- For the advertisement, it passes on what the server advertises, with the line that names the service over HTTP, and then ends the service.
- For a request, it skips the advertisement, sends the body and passes on the answer. If git compressed the body for HTTP, the broker decompresses it first.
git in the VM sends Git-Protocol, which the broker passes on as
GIT_PROTOCOL, so a fetch runs in protocol version 2. The checks of a push
are the same as over HTTPS.
Logging in, opening and closing a session, the advertisement and every pause in an answer have a time limit. When git in the VM gives up on a request, the broker ends its session. What the server writes to standard error goes into the error and the log.
A path on the server that starts with - or ~ isn’t allowed in the
config, because the server would read it as an option or a home folder.
Containers and VMs
You can run containers and VMs inside the VM, without root. Bring podman or
QEMU from your machine like any other tool, with
mounts and path. On Windows
your machine has only Windows programs, which the VM can’t run, so
install them in the VM into /usr/local, which stays.
A container shares the VM’s network. It only has the loopback and the proxy,
and gets the proxy variables. To pull images from Docker Hub, add
preset:docker to the allow list.
podman keeps its storage on the state disk, in
~/.local/share/containers. overlayfs doesn’t work on the shared folders,
so it can’t live in the shared home.
podman’s limits, like --memory and --pids-limit, work. Setuid programs
in an image don’t, because the state disk is mounted with nosuid.
VMs
The VM has /dev/kvm for VMs of its own. On Linux this needs nested
virtualization on your machine. On macOS it needs a Mac where
Virtualization.framework offers nested virtualization. Apple documents that
for M3 and newer, with macOS 15 or newer. On Windows the VM never has
/dev/kvm: Hyper-V gives the VM of aibox no virtualization of its own.
Everywhere else, those VMs run in software.
How it works
The VM’s kernel has user namespaces, cgroups, overlayfs and KVM. The init
opens /dev/kvm and /dev/fuse to everyone and mounts cgroup2. The image
has newuidmap and newgidmap, with a range of subordinate IDs for the
user. The init gives the user a cgroup with the cpuset, cpu, io, memory and
pids controllers, and starts the command in a cgroup below it.
What the VM writes
With aibox run, the VM can write to your project folder. That’s on purpose,
it’s how you see Claude Code’s changes right away. But it also means that
anything in the project folder can come from the VM. Treat it like a pull
request from a stranger until you’ve read it.
This matters most for files that tools on your machine run without asking:
- lefthook runs the commands in
lefthook.yml - direnv runs
.envrc - just reads the
justfile - npm runs the scripts in
package.json - your IDE starts the run configurations in
.idea/
All of them run straight from the working tree.
.git
The same goes for .git. A setting in .git/config, like core.fsmonitor
or core.hooksPath, or a hook in .git/hooks, runs the next time you use
git on your machine. Neither git status nor git diff shows it.
aibox does not protect these files. Mounting them read-only in the VM
doesn’t help. git and IDEs replace .git/config every time they write it,
for example on git push -u, and the mount stays with the old file.
What to do
- Only trust a project in your IDE when it’s your own. GoLand, for example, asks about this when you first open a project.
- Read the diff of what the VM changed before you run a build, a test, a script or a run configuration of the project on your machine. Do the same before you commit with hooks the project defines.
- Check
.git/configand.git/hooksfor changes you didn’t make.
Security
A prompt or a file in a repository can steer Claude Code. So aibox doesn’t trust anything in the VM: not Claude Code, not the programs it runs, and not what they write.
Threat model says who aibox defends you against and where the trust boundaries are. This page has the details.
To report a vulnerability, see SECURITY.md.
What aibox protects
- The VM can’t see your other files. With
aibox runit sees your project folder. A task only sees a clone of a commit. Both also see the folders you list inmounts, and your skills, agents and commands from~/.claude, read-only.aibox runalso copies your~/.claude/CLAUDE.mdand a few settings that start no program and hold no secret into the VM’s home. The project config can keep each of them out. See agents, commands, claude-md, settings. aibox won’t start in your home folder or above it. - The VM can only reach the hosts on the allow list. It has no network card, only a proxy on your machine. aibox refuses a name that resolves to your machine’s own networks, unless the list names that address itself. A port on your machine’s loopback is only reachable when the list names it. See allow.
- Your API key stays on your machine. The VM gets a placeholder, and aibox adds the real key to the requests that go to the Claude API. See API key.
- Your git login stays on your machine too. A broker adds it to git’s requests from the VM, but only for the repositories and branches the config names. See Git.
- The VM can’t read your clipboard whenever it likes. It gets one image, and only right after you press Ctrl+V, or Alt+V on Windows. See Pasting images.
- The programs that run the VM are locked down. On Linux QEMU runs in a bubblewrap sandbox, and aibox restricts itself with Landlock and seccomp. On macOS aibox restricts itself with Seatbelt. See The host side.
- aibox only takes a downloaded image if its SHA-256 was built into its
release. Before each start a release checks the kernel against the
SHA-256 built into it, whether it downloaded the image or a package
installed it. An image you pass with
--imageisn’t checked, and neither is the image of a build from a checkout. On Linux QEMU boots the copy aibox checked. On macOS Virtualization.framework reads the kernel again from its file, so a change right after the check isn’t caught. See Packaging aibox.
What aibox does not protect
- What the VM writes into the project. That includes
.git, and files that tools on your machine run without asking. Read What the VM writes. - What you allow. The VM can reach every host on the allow list and every
port of your machine the list names. It can use every secret in
envand push to every branch thegitlist names. A subscription token goes into the VM as it is, so the VM can read it. A task can use all of this, with nobody there to stop it. See Tasks. - What goes through an allowed connection.
proxy.logshows the host and port the VM asked for. It doesn’t show the address behind the host, or what was sent. A push togithub.comand a fetch from it look the same. - What a task says about itself. Claude Code’s last message,
result.jsonand the authors of the commits all come from the VM. - Force pushes and CI. The git broker can’t tell a force push to an allowed branch from a normal push. A pushed branch can start the repository’s CI, with its secrets. See What the broker can’t stop.
- Spending. The only limit on time or spending that aibox enforces itself
is a task’s
--timeout. Give an API key a spend limit in the Console. - Diffs you accept or edit in your IDE. aibox checks that a diff is for a file in the project, but the IDE opens the file later. In between, the VM can replace the file with a symlink to a file outside the project. If you then accept or edit the diff in the IDE, the IDE writes through the symlink. Approve changes in the terminal instead. See In your IDE.
- Your clipboard right after Ctrl+V, or Alt+V on Windows. aibox can’t tell which program in the VM you pressed the key in. After a Ctrl+V in bash or vim, the VM can read the image on your clipboard too. aibox also counts on your terminal not to echo text a program chose back as input, as current terminals don’t. A terminal that does lets any program type into it, with or without aibox.
- Gatekeeper on macOS. The VM can set and remove extended attributes on
files in the project, including
com.apple.quarantine, and files it creates have none. So Gatekeeper never checks a program that comes out of the VM. Virtualization.framework has no option to prevent that. Like everything the VM writes, such a program is untrusted until you’ve read it.
How it works
The VM
On Linux the VM runs on QEMU’s microvm machine type. On macOS it runs on Virtualization.framework.
The project folder of aibox run, the mounts, the skills, the agents and
the commands are shared into the VM with virtio-fs, or with 9p on Windows.
Your user on your machine is the user in the VM, so the files it creates
belong to you. Your machine enforces the read-only shares, not the VM.
The root disk is read-only, for QEMU and for the VM. The init puts an
overlay in RAM over it, only to create the mount points, and then makes the
root read-only again. In the VM, only /tmp, /run, /dev/shm, the home,
the project and the state disk can be written to.
The kernel has no modules, no network drivers, no bpf() syscall, no ftrace
and no kprobes. The kernel QEMU boots on amd64 has no PCI and no ACPI either.
The arm64 kernel has them. The amd64 image also carries vmlinuz-hyperv, a
kernel for Hyper-V on Windows, which needs both; Linux never boots it. On
Windows the shares are 9p, served by aibox itself, which enforces the
read-only ones as virtio-fs does elsewhere.
The network
All of the VM’s traffic goes over vsock to the proxy on your machine. The terminal is an SSH session over the same vsock. On a Linux kernel without vsock namespaces, a serial port takes the place of vsock, see below.
On Linux each VM gets its own vsock namespace. aibox creates it in an unprivileged user namespace, so it doesn’t need root. Every VM uses the same address, CID 3, and no other process on your machine can connect to it.
Linux has vsock namespaces since 7.0. On an older kernel, or one without vsock, the VM has no vsock at all. It reaches aibox over a virtio serial port instead. QEMU connects the port to a socket pair, and aibox holds the other end. No file on your machine leads to it, so no other process can reach the VM. The proxy, the terminal and everything the proxy carries share the port, as streams that yamux multiplexes on it.
On macOS aibox owns the VM, and only the process that owns a VM can reach its vsock. So it doesn’t need a namespace.
On Windows the VM’s sockets are Hyper-V sockets, and the service that makes the VM lets only your user and the helper that serves its proxy listen on them. Connecting into the VM stays with administrators, and nothing does it. The helper runs in an AppContainer of its own, and the service puts filters of the Windows Filtering Platform on that container: it connects only to the TCP ports of the allow list, to any host on them, and, on your machine’s loopback, to the ports of the allow list and the one aibox serves the services of the proxy on. Windows keeps an AppContainer off the loopback, so the service lets this one on, in a list of the whole machine that it records, and takes it off again once aibox has ended the helper, or at its next start after a crash, or when it is uninstalled. The filters don’t cover names: Windows looks them up for the helper outside its container, so a VM that took the helper over could look up any name, and send data out in the names it asks for. The helper holds no secret of aibox’s to send.
The Windows service
Hyper-V serves only administrators, so on Windows a service that runs as SYSTEM makes the VMs. It is the one part of aibox with more rights than you, and it keeps them to itself: it boots its own copy of the kernel and the root disk, from a folder only administrators can change, and takes no path from you. How it works says what it takes instead. The VM gets at one file of yours, the state disk, only after the service checked that you may write it, and only until the VM is gone: the entry on the file is for an account of that one VM, which goes with it. A VM may have as much memory and as many processors as the machine has, and all VMs together no more memory than that. What the service refuses, it says; what fails inside it goes to the Application event log, not to you.
The host side
On Linux QEMU runs in a bubblewrap sandbox with no network and no writable
file system. Its only environment variable is LD_LIBRARY_PATH, which
names the folders its libraries are in. It only sees its own program,
libraries and firmware and a copy of the kernel. Before QEMU starts, aibox
asks your system’s loader which libraries QEMU loads and binds only those
files. Devices, disks and sockets come in as open files from aibox. QEMU
also filters its own syscalls. aibox run --no-sandbox runs it without
bubblewrap, for debugging.
Once QEMU runs, aibox locks itself down too. It gives up the right to gain
new privileges. Landlock rules only let it read /etc for DNS and connect
to the ports on the allow list. A seccomp filter blocks ptrace, mount,
namespace and similar syscalls. This needs Linux 6.7 or newer.
On macOS aibox locks itself down with Seatbelt, the macOS sandbox, once the VM runs. From then on it reads the contents of no files except the resolver’s, though it can still see the metadata of all files. It only connects over TCP to the ports on the allow list, and to the system’s name service. It can’t start programs or read or signal other processes. Files it opened before, like the logs, and the VM keep working.
Before the VM starts, aibox works in folders an earlier VM could write. git
reads the project’s .git for your identity and for the bundle of a task.
With ide: true aibox writes the IDE’s lock file into the VM’s home. A
symlink the VM left there could lead a write to a file outside. So aibox
does that work in a helper, a short-lived copy of itself that the kernel
lets create, change and delete files in one folder only. On Linux that’s
Landlock, on macOS Seatbelt. The kernel checks where a path really leads, so
a symlink out of the folder can’t be written through, by the helper or by a
program it starts. When git reads .git it may write no file. For the
bundle of a task it may write only in the task’s folder in ~/.aibox. The
lock file goes into the VM’s home. The helper can still read any file you
can and reach the network. On Linux it can also still change the
permissions, owner, times and extended attributes of a file outside, since
Landlock doesn’t restrict those.
On Windows aibox serves the VM from two helpers, copies of itself. One
serves the shares, with your rights, as it must to read and write them,
and does nothing else. The other serves the proxy, in an AppContainer: it
parses what the VM sends through the proxy, reads none of your files,
holds no secret, has no console and reaches only the network the service
lets it. The Claude API forwarder, the git broker, the IDE bridge and the
clipboard stay in aibox, with the API key, your SSH keys and git logins
and the token of the IDE: the helper hands their requests to aibox over a
port of the loopback, which answers only requests with a token the
helper got from aibox. The terminal stays in aibox as well. aibox reads
what the helper says as it would what the VM says. Both helpers are in a
job, which Windows ends with aibox, however aibox ends. The AppContainer
has a profile under your account, which aibox removes when the helper
ends. A profile an aibox that was killed left behind goes with the next
run without --no-sandbox. aibox run --no-sandbox runs the helper of the proxy outside its container, with
no filters.
With the clipboard on, aibox starts a helper before
it locks itself down. The helper isn’t locked down, because it has to start
osascript, wl-paste or xclip. It only reads the clipboard when aibox
asks, and ends when aibox does.
On macOS the VM runs in a Virtualization.framework process. aibox can’t put
that process in a sandbox of its own, like it does with QEMU and bubblewrap.
That’s why --no-sandbox changes nothing on macOS.
The VM ends with aibox, even when aibox is killed or crashes. On macOS the Virtualization.framework process does, and it also serves the shares, so nothing is left behind. On Linux aibox starts QEMU and virtiofsd with a parent death signal, so the kernel kills them when aibox dies. bubblewrap does the same for QEMU. The kernel sends that signal when the thread that started the program ends, so aibox starts all of them from one thread that lives as long as aibox. virtiofsd forks the process that serves the share, and that process ends by a signal of its own.
aibox won’t start in your home folder, above it, or above ~/.aibox. It
compares folders rather than paths, so a symlink, a different spelling on a
file system that ignores case, or a bind mount doesn’t get around it. On a
Mac, /users/YOU and /System/Volumes/Data/Users/you are your home folder
too, and /System/Volumes/Data contains it. aibox refuses those as well.
aibox checks the host folder of a mount the same way. It also refuses a
mount inside ~/.aibox, because each project’s config and login live there.
Threat model
This page says who aibox defends you against, what it keeps safe, and where its limits are. Security has the details of each defense.
Who aibox defends you against
aibox defends you against anything that can steer Claude Code: a prompt, a file in a repository, a web page Claude Code reads or a package it installs. aibox assumes the worst. Whatever runs in the VM is under an attacker’s control, root in the VM included, and it may try bugs in the VM’s kernel.
Other programs on your machine can’t reach the VM either. On Linux each VM has its own vsock namespace, so no other process can connect to it. On a Linux kernel without vsock namespaces, the VM talks to aibox over a serial port that only aibox holds. On macOS only aibox can reach its VM’s vsock.
Other programs also can’t pose as a server you allowed. A server on your
machine’s loopback usually listens on only one address, 127.0.0.1 or
[::1]. That’s why the allow list takes the exact address and not
localhost, which means both. Otherwise another user could listen on the
other address and get the VM’s connections.
What aibox keeps safe
- Your files outside the project folder and the mounts.
- Your API key, your git login and SSH keys, and your IDE’s token. Note that
a subscription token and the secrets in
envdo go into the VM. - Your clipboard, apart from one image right after you press Ctrl+V. Set
clipboard: falsein the config to keep that out too. - Your machine’s own networks and the servers that listen on its loopback.
- The rest of the internet. Data leaves only through what you allowed, like a host, a port or a branch.
- The branches, tags and repositories the
gitlist doesn’t name. - Your machine while the VM runs. The VM can’t run code on it directly. Code it writes into the project can run later. See below.
- Your aibox config. It decides everything else on this list.
Why the config isn’t in the project
The project config decides what the VM gets: the hosts, the ports, the
secrets, the mounts and the branches it can push to. So it lives on your
machine, in ~/.aibox/projects/<escaped path>/config.yaml, and never in the
project or its git repository. aibox doesn’t read a config from the project
at all.
People ask for a config file in the repository, like a dev container’s
devcontainer.json. aibox can’t have one, for two reasons:
- A repository would pick its own permissions. Whoever wrote it, or slipped
a change into it, would decide which hosts and secrets the VM gets. Running
aibox runin a stranger’s repository would hand them your secrets. - The VM would rewrite its own config. With
aibox runthe VM can change anything in the project,.gitincluded. A config there could grant more in one session and take effect at the next start. A task’s commits could carry one too, and you’d merge it with the rest of the change.
Your starting config for new
projects, ~/.aibox/config.yaml, is out of reach of the VM too.
aibox reads the config once, when it starts. Nothing the VM does while it
runs changes what it gets. To change the config, run aibox config edit on
your machine.
Trust boundaries
Everything the VM can reach on your machine goes through one of these.
| Boundary | What crosses it | What aibox does |
|---|---|---|
| The hypervisor | The VM’s CPU, memory and devices | The VM has its own kernel. On Linux it runs on QEMU in a bubblewrap sandbox. On macOS it runs on Virtualization.framework, which aibox can’t sandbox. On both, aibox locks itself down once the VM runs. See The host side. |
| The shares | The project, the mounts, your skills, agents and commands, and the VM’s home | Your machine enforces the read-only shares. aibox won’t start in your home folder or above it, and won’t mount your home folder, a folder above it, or a folder in or above ~/.aibox. See mounts. |
| The proxy | All network traffic, over vsock or a serial port | The VM has no network card. The proxy only connects to the allow list and refuses names that lead to your own networks. See allow. |
| The git broker | git’s requests | The broker adds your login on your machine, only for the repositories and branches the config names. See Git. |
| The key forwarder | Requests to the Claude API | The VM gets a placeholder key. aibox adds the real one. See API key. |
| The IDE bridge | Selections, diffs and errors | With ide: true, the bridge passes on only those messages, and only for files in the project. See In your IDE. |
| The clipboard | One image after Ctrl+V | aibox lets the VM read the clipboard only right after you press Ctrl+V, once, and only an image. Tasks never get it. See Pasting images. |
| The terminal | What Claude Code shows you | aibox run passes the VM’s terminal straight through. |
| A task’s results | The bundle, the logs and result.json | For aibox task, aibox checks each file and the bundle, and strips control characters before text reaches your terminal. See How a task runs. |
| Your project folder | What the VM writes | aibox doesn’t check it. Treat it like a pull request from a stranger. See What the VM writes. |
Attacks and what stops them
| The VM tries to | What stops it |
|---|---|
| read a file outside the project | The VM only sees its shares. |
| get aibox to write a file outside through a symlink in the project or its home | aibox works in those folders in a helper that the kernel lets create, change and delete files only there. On Linux it can still change the permissions and owner of a file outside. See The host side. |
| give itself more hosts, secrets or branches | The config isn’t in the project, and aibox reads it once when it starts. |
| write to a read-only mount | Your machine enforces read-only, not the VM. |
| break out through a bug in its kernel | The VM has its own kernel, so a bug in it gives the attacker nothing on your machine. The kernel has no modules, no network drivers and no bpf(). The attacker then has to break the hypervisor too. |
| break out through a bug in the hypervisor | On Linux QEMU runs in bubblewrap with no network, only LD_LIBRARY_PATH in its environment and no writable file system, and filters its own syscalls. On macOS the VM runs in a Virtualization.framework process, which aibox can’t put in a sandbox of its own. |
| take over aibox through a bug in the proxy or the broker | aibox locks itself down once the VM runs. On Linux 6.7 or newer that’s Landlock and seccomp, on macOS Seatbelt. |
| send your data to a host of its own | The proxy only connects to the allow list. |
| reach your router or a server on your machine | The proxy refuses a name that resolves to your own networks. A loopback port needs its address on the allow list. |
| read your API key | It only has a placeholder. |
| read your git login | It never enters the VM. |
push to main, delete a branch or push a tag | The broker refuses what the git list doesn’t allow. |
| read the passwords you copy | It can only ask for the clipboard right after you press Ctrl+V, and only gets an image. |
| run code through your IDE | The IDE bridge only passes on selections, diffs and errors. |
| sneak extra branches into a task’s bundle | aibox reads the bundle’s header and only prints the fetch command for aibox/task. |
| fake a line from aibox in a task’s output | aibox strips control characters and marks every line from the VM. |
| get a changed kernel or image booted | A release checks the SHA-256 of a downloaded image and of the kernel before each start. On macOS Virtualization.framework reads the kernel again after the check, so a change right after it isn’t caught. |
| keep running after aibox ends | The VM ends with aibox, even when aibox is killed. |
What aibox doesn’t stop
These are on you. What aibox does not protect has the details.
- What the VM writes into the project,
.gitincluded. A hook,.envrcorpackage.jsoncan run on your machine later. - Code the VM wrote, once you run it on your machine. That includes tests your IDE runs by itself, like on save, and the program you’re working on when you build or start it. A test or a line in the program can do anything you can.
- What you allowed. The VM can use every host, port, secret in
envand branch you give it. A mount gives it every file in that folder. A subscription token goes into the VM as it is, so the VM can read it. - The servers on your loopback that you allow. They run as you, often without a login, and do what the VM asks. See Ports on your machine.
- What goes through an allowed connection.
proxy.logshows the host, not what was sent. - What a task says about itself. Claude Code’s last message,
result.jsonand the authors of the commits come from the VM. - What the VM leaves for the next
aibox run. The VM’s home folder and state disk stay between runs, so a steered session can leave something behind for the next one, like a line in~/.claude/CLAUDE.md.aibox projects reset --logoutclears both. See Starting over. Tasks get a new home and state disk each time. - What the VM shows in your terminal. With
aibox runits output reaches your terminal as it is, escape sequences included. - Force pushes to an allowed branch, and the CI a pushed branch starts.
- Diffs you accept or edit in your IDE.
- Your clipboard right after a Ctrl+V, or Alt+V on Windows, in any program in the VM. aibox watches for the key, not for Claude Code, so a Ctrl+V in bash or vim lets the VM read the image too.
- Gatekeeper on macOS, for programs that come out of the VM.
- Spending. Only a task’s
--timeoutis enforced by aibox.
Tasks
aibox task runs in the same VM, behind the same proxy, broker and key
forwarder. What changes is what the VM gets to touch and who is watching.
aibox run | aibox task | |
|---|---|---|
| What the VM sees | your project folder, writable | a clone of one commit |
| Who approves each step | you | nobody, because Claude Code has all permissions |
| What stays for the next run | the home and the state disk | nothing, because both are new each time |
| The clipboard | after Ctrl+V, unless clipboard: false | never |
| What reaches your terminal | the VM’s output as it is | text with control characters stripped, each line marked |
| What reaches your project | every write, right away | only the bundle, after aibox checks it, and only once you fetch it |
So a task can’t touch your working tree or .git, and it leaves nothing
behind in the VM. But nobody stops it from using everything the config
gives it: every host, port, secret and branch. And what it hands back is
code you haven’t read. Your review is the boundary of a task. Fetch the
branch with the command aibox prints and read the whole diff before you
check it out or merge it. See
Reviewing the commits.
What aibox assumes
- KVM, Virtualization.framework or Hyper-V holds. aibox makes a break-out
harder, but on macOS it can’t sandbox the Virtualization.framework
process. On Linux,
--no-sandboxtakes QEMU out of bubblewrap. On Windows it takes the helper that serves the proxy out of its AppContainer. - Your machine isn’t already taken over. A program that can change aibox,
its config in
~/.aiboxor the programs it starts is out of scope. - The release is the one we built. Check a download with cosign. See
Installing. An image you pass with
--image, and a build from a checkout, don’t check the kernel. - You read what comes out of the VM before you run it, commit it or merge it.