AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Self-hosted GitHub Actions runners on the Linux box you already pay for. Every job gets a fresh container that is gone when the job ends, and your GitHub App key never touches the box.
Developer toolsGo · TypeScriptGitHub @dominikzabcikOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Ephemeral GitHub Actions runners on the Linux box you already pay for
Taken from the repository README
Loading the full README.
Read the full README ↗GitHub Actions runners on the Linux box you already pay for. Every job gets a fresh container that is gone when the job ends. Your GitHub App key stays in a Cloudflare Worker and never touches the box. Your main CI job keeps the box's full speed while lighter jobs use only the cores it leaves idle.
You need a Linux box with systemd and Linux 5.15 or newer (Ubuntu 22.04 or newer, Debian 12 or newer; amd64 or arm64) and a free Cloudflare account. Debian 11 works with a newer kernel than its own 5.10.
Deploy the Worker. With one click, from the latest release, where Cloudflare asks for a setup secret (keep it, the setup page asks for it):
Or from a clone of this repository:
cd worker && bun install
bunx wrangler deploy
secret=$(openssl rand -base64 36) && echo "$secret"
echo "$secret" | bunx wrangler secret put SETUP_SECRET
Keep the printed secret: the setup page asks for it.
Create the GitHub App. Open https://<your worker>/setup, enter the setup secret and confirm on GitHub. Install the App on the private repositories whose jobs should run on your box.
The App gets the repository permission Administration (read and write), which registering runners for a repository needs, and Metadata (read). Setup creates it under your account, and a private App installs only on its owner, so for repositories an organization owns, set the Worker's APP_OWNER to the organization and ADMINS to the logins that may open the dashboard first (repositories of an organization). To serve a whole organization through one scale set per class, set ORG, RUNNER_GROUP and ADMINS: the App then belongs to the organization and gets the organization permission Self-hosted runners (read and write) instead, and a runner group limited to selected private repositories decides which repositories reach the box (organizations, not yet tried on a real organization).
Install the agent. The setup page shows the command, which looks like this:
curl -fsSL https://boxrunner.zabcik.me/install.sh \
| sudo sh -s -- --worker https://<your worker> --token <box token>
Use it. In a workflow, runs-on: boxrunner, or runs-on: boxrunner-light for jobs that should only use idle cores.
To run jobs on GitHub's runners while the box is down, turn on fallback and pick the label from a variable the Worker sets: runs-on: ${{ vars.BOXRUNNER_DOWN && 'ubuntu-latest' || 'boxrunner' }}. To hear when the box goes silent, give the Worker an alert webhook for ntfy, Discord or Slack. To see jobs that wait for a label or repository no box here serves, turn on unserved jobs.
Install and doctor on a fresh Ubuntu 24.04 VM, with the Worker and token passed as BOXRUNNER_WORKER and BOXRUNNER_TOKEN. Doctor's last line, the Worker check, is cut because it names our own Worker. Pauses longer than 1.5 s are shortened; building the job image took about seven minutes.
For services:, container: jobs and docker build, add --docker to the install command, which installs Sysbox and gives each job its own Docker daemon; the configuration explains the limits. With --docker or --sysbox, jobs run under Sysbox, where a job's root is an unprivileged user on the box (isolation).
To run an AI coding agent on the box, one job at a time behind an egress allowlist, see running coding agents.
sudo boxrunner doctor checks the box, including that a job cannot reach the host, your private network or the cloud metadata service. sudo boxrunner status shows what runs (--json prints the same data as JSON for scripts and monitoring), and sudo boxrunner shell opens a shell in a running job, or in a failed one a class keeps for you. When a new box takes over from one that died, replacing a box says how to recover the jobs the old one had acquired.
A personal website ran Lighthouse, Playwright and CodeQL on every pull request and used all 2,000 of GitHub Free's private minutes in twelve days. A 4-core VPS that was already paid for now runs every job:
| GitHub's runners | boxrunner | |
|---|---|---|
| Test suite with Lighthouse | about 15 min | 11.6 to 12.6 min |
| Queue to start | 6 to 12 s | |
| Cost | $0.006 a minute after the free 2,000 | the box you have |
sequenceDiagram
participant A as Agent on your box
participant W as Your Worker (App key)
participant G as GitHub
participant C as Job container
A->>W: box token: open a session for each repository and class
W->>G: App key: create the scale set and session
W-->>A: session token
A->>G: long-poll the queue
G-->>A: a job is waiting
A->>W: box token: one runner config
W-->>A: one-job runner config
A->>C: start a fresh container
C->>G: run exactly one job
A->>C: remove the container
The architecture explains each part and what it holds, and the decisions record the measurements behind them. The configuration covers classes, limits, custom job images and Prometheus metrics.
| boxrunner | Zoomies | runscaler | GARM | ARC | |
|---|---|---|---|---|---|
| Runs on | one Linux box you own | your hosts: a controller and agents | a Docker host or Apple Silicon Mac | many clouds, LXD, Incus, Kubernetes | Kubernetes |
| Where the GitHub credential lives | App key in a Cloudflare Worker; the box holds only a scoped token | App key in the controller | personal access token on the host | App key or token in the GARM server | App key or token in the cluster |
| One environment per job | container | container | container or macOS VM | per provider | pod |
| Priority between jobs | strict idle class (SCHED_IDLE) | guaranteed shares, idle CPU lent out | none | none | none |
| Docker inside jobs | own daemon under Sysbox, no privileges, opt-in | Docker-in-Docker, opt-in | Docker-in-Docker or the host's socket, opt-in | per provider | privileged sidecar |
| License | Apache-2.0 | AGPL-3.0 | MIT | Apache-2.0 | Apache-2.0 |
Checked against each project's README on October 14, 2026. Corrections are welcome.
sudo boxrunner upgrade
It installs the latest release on the box, checked like the first install, and restarts the agent. The Worker is upgraded separately; upgrading says how for a Worker deployed with the button or with Wrangler. The agent also upgrades itself to newer releases of its major version, six hours after it first sees them, unless the config sets auto_upgrade = false. sudo boxrunner status and the dashboard say when a newer release is out.
sudo boxrunner uninstall
It closes the box's sessions and removes the service, containers, images, network, firewall rules and files. Docker stays, and so does Sysbox if install added it.
Issues, fixes and docs are welcome; see CONTRIBUTING.md, and the good first issues are a place to start. Questions and ideas go to Discussions. Report security problems privately as described in SECURITY.md.