Run in CI
Run an agent's commands in boxer on a CI runner, with docker as the backend, a pinned version, exit codes a job can read, and cleanup.
A CI job is a host like any other, and boxer runs there the same way. The differences are what a runner lacks and what a job needs to read back.
Choose the backend
docker is installed on GitHub's Ubuntu runners, and needs nothing else. smolvm needs /dev/kvm,
and your user must be able to open it, which a job cannot arrange by logging in again. Check with
ls -l /dev/kvm on your runner before choosing smolvm.
docker refuses network.mode = "allowlist", so set the mode the job runs with. Commands in the
guest then have the network the runner has. See backends.
A GitHub Actions job
jobs:
agent:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
BOXER_VERSION: v1.4.2 # pin it; unpinned, the installer asks GitHub's API without a token
BOXER_BACKEND: docker
BOXER_NETWORK_MODE: "on"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # the guest can read .git/config
- run: |
curl -fsSL https://raw.githubusercontent.com/BarakChamo/boxer/main/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: boxer up
- run: boxer run --task test --fail-on-test-failures
- if: always()
run: boxer rm --all --volumes -yactions/checkout gives a main checkout rather than a linked worktree. boxer warns about that by
default (require_worktree); set
BOXER_REQUIRE_WORKTREE=off to silence it. An agent run headless in the job (claude -p, opencode run) reads the hooks you committed, as it does on your machine.
Read the outcome
boxer runexits with the command's own status. boxer's own failure (a sandbox that could not start) is 1, and a task that ran past itstimeoutis 137, which is also what an out-of-memory kill exits with.boxer doctorexits 1 only for an error. To fail a job on its warnings, read them:boxer doctor --json | jq -e '.warnings | length == 0'.--jsononls,status,doctorand the rest gives shapes listed in the JSON reference.
Clean up
A runner is thrown away, but a self-hosted one is not. boxer rm --all --volumes -y removes every
sandbox boxer made on the backend, and their volumes. Without -y, a filtered rm outside a
terminal is refused rather than run.
What is not protected
The agent itself runs on the runner, with the job's environment, its secrets and the runner's
network. git and gh run there with the job's token. Only the commands on
intercept run in the sandbox, and on docker the sandbox
shares the runner's kernel. See security.