AI agents
Use a box as the sandbox an AI coding agent works in, instead of your own machine.
An AI coding agent runs shell commands, installs packages and edits files on whatever machine it is given. A box is a good machine to give it: a real, fully isolated Linux machine with its own filesystem, that boots in seconds and that you can throw away afterwards. Whatever the agent does there, it does not do to your laptop.
There are four ways to use one, from the smallest to the largest.
| You want | Use |
|---|---|
| One command run somewhere safe, result back as JSON | boxctl exec |
| Claude Code on your machine, its shell commands in a box | The Claude Code plugin |
| The whole agent on a box, working while you are away | boxctl claude or boxctl kilo |
| A sandbox for an agent you built yourself | Your own agent |
All of them need the boxctl command line, logged in.
Everything on this page except boxctl exec is newer than the latest
tagged release, so install the
edge build to get it.
One command in a throwaway box
boxctl exec creates a new box, runs one command in it, prints the
result, and destroys the box, however the command went:
boxctl exec "python3 -c 'print(2**64)'"
{"exit_code":0,"stdout":"18446744073709551616\n","stderr":"","error":""}
Standard output is always exactly one JSON object, progress messages go
to standard error, and the process exits with the command's own exit
code. error is only set when the box could not be reached or the
command timed out; a command that simply fails is exit_code alone.
That makes it easy to hand to an agent as a tool: run the code the
model wrote, read the JSON.
Nothing carries over from one call to the next. Each one is a new box.
boxctl exec --size large --timeout 2m "make test"
boxctl exec --image ubuntu-26.04 "apt-get install -y jq && jq --version"
The command gets 30 seconds unless --timeout says otherwise.
Claude Code's shell in a box
The boxctl plugin for Claude Code keeps Claude on your machine and sends the Bash commands it runs to a box. Install it inside Claude Code:
/plugin marketplace add cdalar/boxctl
/plugin install boxctl@boxctl
It needs boxctl and jq on your PATH. Then:
/boxctl:on # from now on, Bash runs in a box
/boxctl:status # where Bash runs right now
/boxctl:off # back to your machine
--mode chooses which box:
| Mode | Box | /boxctl:off |
|---|---|---|
session (default) | One box for this Claude Code session, destroyed when the session ends | destroys it |
project | One box for this project directory, reused by every session there | leaves it running |
exec | A new, disposable box for every command; nothing persists |
--workspace chooses how your project gets there. It is always at the
same path it has on your machine:
| Workspace | The box has |
|---|---|
sync (default) | The project, copied there before every command and back after, so Claude's file edits and the box's commands see the same files. Costs about a second per command |
copy | The project as it was when the box was turned on |
git | A clone of the project's origin at your current commit |
none | An empty directory |
Files your .gitignore excludes are never copied in either direction,
so node_modules, build output and .env stay on the side that made
them.
Each command is limited to 5 minutes and has no standard input. A
command that starts with a # local line runs on your machine instead.
To have every new session start in a box without typing /boxctl:on,
set the plugin's Auto-on at session start option.
The whole agent on a box
boxctl claude runs Claude Code itself on a box, attached to your
terminal. Its file edits and its shell both happen there, on the box's
own copy of the project, so it can keep working after you close your
laptop.
cd ~/src/myproject
boxctl claude login # once: log every box's Claude in
boxctl claude # create the project's box, or reattach to it
boxctl claude --prompt "fix the flaky auth test"
boxctl claude --detach --prompt "..." # start it without attaching
The first run creates a box for the project, copies the project to it
at the same path (everything git counts as part of it, including
uncommitted changes, but nothing your .gitignore excludes), installs
Claude Code, and copies your own Claude settings, skills and commands.
Later runs reattach to the same box.
Claude keeps running when you detach (Ctrl-b d) or the connection
drops. Run boxctl claude again to return to it.
Several tasks at once. --task starts another Claude on the same
box, on its own branch and in its own copy of the files, so the two do
not get in each other's way:
boxctl claude --task auth # a second Claude, on branch auth
boxctl claude ls # every session for this project
Getting the work back. The box is a git remote. fetch brings back
every branch on it as box/<branch>, and whatever the agent has not
committed yet as box/wip:
boxctl claude fetch
git log box/fix-auth
git show --stat box/wip
boxctl claude push # send your local commits to the box
GitHub. While you are attached, Claude on the box can git push
and open pull requests as you. The box asks your machine for your gh
token each time it needs one and keeps nothing, and every use is
logged to ~/.boxctl/claude/github.log. Once you detach, the box has
no GitHub access. --github store keeps a token on the box for work
that has to reach GitHub while you are away; --github off gives it
none.
Repositories instead of a local copy. --repo owner/name (-R
for short, as in gh) needs nothing on your machine: it makes a new
box, clones the repository's default branch from GitHub onto a new
branch boxctl/<slug>-<id>, and starts Claude there. Private
repositories work too, through the GitHub access above.
boxctl claude -R owner/name
boxctl claude -R owner/name --goal "bump Go and fix what breaks" --github store
boxctl claude logs <box> -f # how an unattended run is going
With --goal, Claude does not wait for you. It works on the goal in
the background until it is done, opens a pull request, and the command
returns after printing the box, the branch and the logs command.
Because the run goes on after you close the terminal, it needs
--github store, with a token that covers the repositories involved,
and Claude logged in without you (boxctl claude login). Without
--repo, --goal uses the GitHub repository of the directory you are
in.
Several repositories. Repeat -R, or separate the names with
commas, to work across repositories in one box:
boxctl claude -R owner/api -R owner/web
boxctl claude -R owner/api,owner/web --goal "rename the /v1 route" --github store
They are cloned side by side under /root/workspace, each on the same
new branch, and Claude starts in that directory so it sees all of them.
With --goal it opens one pull request in each repository it changed,
and links them to each other. A repository it left alone gets none.
The repositories need different names, since each
is cloned to a directory of its own, and --repo cannot be combined
with --project, --task or --prompt.
Your Claude login. boxctl claude login saves a long-lived token
on your machine. Each session on a box receives it in memory when it
starts. It is never written to the box's disk.
Moving a session you already started. From a local Claude Code
session with the plugin installed, /boxctl:handoff moves the
conversation to the project's box and carries on there.
A box made this way is paused after 24 hours unused, rather than the
usual 6. Change that with --idle-ttl.
Kilo
boxctl kilo does the same for the Kilo CLI:
the box, the project copy, tasks, GitHub and fetch/push all work as
above.
boxctl kilo
boxctl kilo --task auth
boxctl kilo --env OPENAI_API_KEY # pass a provider key from this shell
Your Kilo configuration and login come along, with credentials passed in memory the same way.
Your own agent
If you are building an agent rather than using one, a box is a sandbox you can drive from code:
boxctl execfrom any language that can run a process and parse JSON, for one-shot commands.boxctl create, thenboxctl ssh my-box -- <command>as often as you need, thenboxctl rm, when state has to survive between commands.boxctl ls -o jsonlists your boxes for a script.- OpenAI Agents API on a box is a complete example: OpenAI runs the agent and a box is the environment its shell commands and file edits happen in. The box dials out to OpenAI, so it needs no inbound port.
The CLI authenticates with a personal API token. Give an automated agent a token of its own, so you can revoke it without logging yourself out.
What a box can reach
A box is private: nothing on the internet can connect to it unless you give a port a public URL. It can reach the internet itself, which is how an agent installs packages and clones repositories.
It has none of your credentials unless you put them there. The plugin
and boxctl claude pass what they need in ways described above; an
agent you run yourself has only what you copy in. A token an agent can
read is a token the code it runs can read too, so give a box
narrowly scoped ones.