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.