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 wantUse
One command run somewhere safe, result back as JSONboxctl exec
Claude Code on your machine, its shell commands in a boxThe Claude Code plugin
The whole agent on a box, working while you are awayboxctl claude or boxctl kilo
A sandbox for an agent you built yourselfYour 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:

ModeBox/boxctl:off
session (default)One box for this Claude Code session, destroyed when the session endsdestroys it
projectOne box for this project directory, reused by every session thereleaves it running
execA 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:

WorkspaceThe 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
copyThe project as it was when the box was turned on
gitA clone of the project's origin at your current commit
noneAn 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 exec from any language that can run a process and parse JSON, for one-shot commands.
  • boxctl create, then boxctl ssh my-box -- <command> as often as you need, then boxctl rm, when state has to survive between commands. boxctl ls -o json lists 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.