A failed step, a fix in another pane, and a re-run, all local, with no push required.
preloop is a local, self-hosted equivalent of GitHub Actions. The engine (preloop serve) accepts workflows the same way GitHub does: ${{ }} expressions, matrix builds, reusable workflows, concurrency groups, OIDC etc. It executes them on hardware-isolated microvms that work on Windows/MacOS or Linux, and resume in 300ms. Your .github/workflows run unmodified, and you can run CI against your uncomitted changes(respects .gitignored/untracked ones).
It speaks the official actions/runner protocol, so you can use the official runner to register, poll, execute, and report against it without GitHub-hosted minutes. You can also use our Rust-equivalent runner — 36× smaller install, 19× lower peak memory (see benchmarks below).
Preloop doesnt rely only on Github Webhooks to update the status of the CI. We run a Webhook watchdog that detects drift, and reconciles your commits with received webhooks against the Gtihub API. You can run your committed changes and run workflows locally and pass --push or --create-pr to open a PR with the checks correctly updated (new PRs open as drafts by default, see --pr-draft). This can be useful if/when Github's webhook service is down.
# Install (macOS/Linux), any one of:
brew install preloopdev/tap/preloop # Homebrew — formula published by the release workflow
npm install -g @preloop-dev/cli # npm — same release binaries, fetched at install time
# Or fetch the installer at a release tag, read it, then run it. It downloads
# the release binaries and verifies every artifact's sha256.
ver=v0.33.6 # any tag from https://github.com/preloopdev/preloop/releases
curl -fsSL "https://raw.githubusercontent.com/preloopdev/preloop/$ver/install.sh" -o install.sh
less install.sh
sh install.shpreloop init # wizard: credentials, golden image, how to run
preloop serve # engine on 127.0.0.1:9090
cd my-repo
preloop run -f .github/workflows/ci.yml --event pull_request
preloop init walks through four steps (GitHub credential, the golden image
every job forks from, run mode, preflight) and writes the result to
~/.preloop/config.toml. Without a terminal it takes the same answers as
flags — preloop init --probe --json reports what this host supports without
changing anything, and preloop init --help documents every flag. It is the
one-command front door to what preloop setup github and a hand-picked
PRELOOP_RUNNER_BASE_IMAGE used to be.
This starts the server in the foreground, but you can detach it too(add a -d). First run can take a few minutes as we need to download a packed vm artifact of the SBOM-attested Official Github Runner OCI image (runner-image-blobs) and create a "golden" vm locally. This golden vm will be forked per job in 300 ms. The official Github image is around 9GB compressed, and unpacks to almost 50 GB so atleast 80GB disk is recommended. You can alternatively define your own golden vm from an OCI image — preloop init records it for you. docs/vm-images.md for more detailed info. Each vm's memory is elastic so it only consumes what's actually being used in the job. The control plane idle rss is around 25MB.
Prefer not to run a script: take preloop-cli-<target>.tar.gz and its .sha256
from the latest release,
verify the checksum, put preloop on your PATH, and run preloop update to
install the pinned SmolVM runtime. Every release asset also carries keyless
build provenance (gh attestation verify <asset> --repo preloopdev/preloop).
MicroVM jobs need the Linux guest runner at
<prefix>/lib/preloop/runner/<triple>/preloop-runner; install.sh places it,
and docs/self-hosting.md §3 has the manual commands.
On Apple Silicon, x86_64 goldens also run through Rosetta 2 translation (enabled automatically for every VM). Performance is slightly slower than arm64 native, so prefer arm64 goldens on Apple Silicon when you can. Docker actions are not supported yet on this path: amd64 images inside the VM's Docker lack the Rosetta mount, so they fail with a cryptic rosetta-wrapper error. Fixing soon.
You can simulate most if not all Github events locally. For some events, you might need to add a payload. See docs/cli_reference.md for more flags you can pass.
To continue with the setup, see GitHub App and PAT credentials, secrets, config file, and the troubleshooting guide: docs/setup.md
Run it as a server, service install, every runtime knob, and how to expose it (tailnet only, Tailscale Funnel, Cloudflare Tunnel, or your own domain): docs/self-hosting.md
preloop-runner is a drop-in Rust replacement for actions/runner — it
registers against GitHub (or a preloop server) the same way, at 36× smaller
install and 19× lower peak memory:
curl -fsSL https://raw.githubusercontent.com/preloopdev/preloop/main/install.sh | sh -s -- --runner
export PATH="$HOME/.local/bin:$PATH"
preloop-runner configure --url https://github.com/owner/repo --token <registration-token>
preloop-runner runEvery release also publishes standalone binaries for Linux (x86_64/aarch64),
macOS (x86_64/arm64), and Windows (x86_64), each with a CycloneDX SBOM
(preloop-runner_<triple>.cdx.json) and a sha256 checksum:
curl -fsSLO https://github.com/preloopdev/preloop/releases/latest/download/preloop-runner-<triple>
chmod +x preloop-runner-<triple>
./preloop-runner-<triple> configure --url https://github.com/owner/repo --token <registration-token>
./preloop-runner-<triple> runOr use the container image with a persistent volume:
docker run --rm -v preloop-runner-data:/runner \
ghcr.io/preloopdev/preloop-runner:latest \
--runner-root /runner configure --url … --token … --unattended
docker run --rm -v preloop-runner-data:/runner \
ghcr.io/preloopdev/preloop-runner:latest \
--runner-root /runner runDetails: docs/setup.md.
- We use the real runner protocol, not a behavior approximation: the official runner binary works against it unchanged.
- Hardware-isolated microvms for each job that spin up in 300ms, elastically scale up and down, and cross-platform.
- GitHub App and fine-grained PAT support with per-job token minting so your checks get updated.
- DAP-powered job inspection: attach at entry, inspect live GitHub/job context, pause, and continue, and allow for step-level rerties.
- Heavily tested with property tests, differential tests, and formal verification. Conformance is table-stakes.
- NDJSON event output for agents and developer tooling.
For a more detailed comparison of how we compare, please see: docs/preloop_vs_others.md
When a run is submitted with the DAP debugger enabled, preloop holds the job at entry until a debugger attaches. An agent or compatible DAP client can then inspect the live github, env, runner, job, steps, and secrets
scopes before continuing the job. This is useful when the workflow YAML looks
right but the runtime event payload, matrix values, or generated context is
wrong.
preloop dap <run-id>preloop-runner is a from-scratch Rust reimplementation of actions/runner
that speaks the same protocol — the official runner works against preloop
unchanged, and preloop-runner registers against GitHub the same way. Same
server, same job, measured on the same machine (M1 Pro, macOS, median of 3):
| Metric | actions/runner 2.336.0 | preloop-runner | |
|---|---|---|---|
| Install footprint | 434 MB · 9,301 files | 11.9 MB · 4 files | 36× |
| Runner code only | 85 MB | 11.9 MB | 7× |
| Cold start | 97 ms | 29 ms | 3.3× |
| Time to listening | 456 ms | 32 ms | 14× |
| Idle RSS | 67 MB | 9 MB | 7.5× |
| Warm pool ×10 idle | 672 MB | 90 MB | 7.5× |
| Job pickup | 268 ms | 100 ms | 2.7× |
| Peak RSS during a job | 206 MB | 11 MB | 19× |
The official tarball bundles node20 + node24 (~350 MB) into every install; preloop-runner ships as a single binary and mounts one shared read-only copy of the externals into every VM (or bakes them into the golden image). A warm pool of 10 idle runners costs ~90 MB instead of ~670 MB.
Reproduce: scripts/bench-runner-compare.sh (set BENCH_RUNS=N; needs
target/release/preloop-runner and the official runner in
~/.cache/actions-runner/current). Interactive chart:
benchmarks/runner-compare.html.
| Topic | Doc |
|---|---|
| Setup, credentials, secrets, config | docs/setup.md |
| CLI reference | docs/cli_reference.md |
| Hosting it yourself: service install, knobs, exposure | docs/self-hosting.md |
| VM images, version pins, and custom goldens | docs/vm-images.md |
| GitHub App webhooks and check runs | docs/github-app-webhook.md |
| Job tokens, minting, OIDC | docs/github-tokens.md |
| Debug sessions (pause, inspect, retry) | docs/debug-sessions.md |
| Architecture and crate map | docs/architecture.md |
| Protocol conformance | docs/conformance.md |
| Fidelity gaps and roadmap | docs/fidelity-gap.md |
| Contributing and CI requirements | CONTRIBUTING.md |
Two licenses, one project:
- Everything except the control plane is MIT — the CLI, the Rust runner, the parser/expression/protocol crates, the VM orchestrator, and the docs.
- The control plane (
preloop serve/preloop-runner-server) is FSL-1.1-MIT so source-available. You may use, modify, and redistribute it for any non-competing purpose (internal CI, commercial products, forks), and it converts to MIT on the second anniversary of each release. What "non-competing" means: you can't offer it as a hosted CI service that competes with preloop's own offering.
Full terms: crates/preloop-runner-server/LICENSE (FSL-1.1-MIT) and MIT for
the rest.
This project wouldn't especially be possible without:
- smolvm — the microVM runtime every job executes in
- runner.server — the protocol reverse-engineering this project builds on



