Skip to main content
This reference lists every command and flag of the preloop binary. Run preloop <command> --help for the same text from the installed version. Environment variables that these commands read are listed in Environment variables. preloop --version and preloop version print the build version. Commands that talk to the engine use PRELOOP_URL (default http://127.0.0.1:9090) and the native API token. Only preloop serve and preloop init --mode foreground start an engine (preloop engine is serve’s former name, kept for existing supervisor units). Every command that talks to an engine — run, rerun, status, logs, cancel, shell, debug, dap, push, webhooks — requires one that is already running: it probes PRELOOP_URL and, when nothing answers, exits non-zero naming the probed endpoint and how to start one — preloop serve for the foreground, preloop server install for a supervised service. preloop run is included: it submits work to an engine, it does not boot one (issue #658). Local and administrative commands (plan, store, setup, doctor, secret, update, version, build-golden, golden-path, and init outside --mode foreground) neither probe nor start an engine, so preloop store migrate still prepares a database with no engine running. A loopback endpoint is probed with a tight budget (500 ms) so a silent local engine refuses at once; an endpoint that crosses a network gets 5 s, so a healthy but slow remote engine is not reported absent.

Run and inspect workflows

preloop run

Submit a workflow, stream its events, and wait for the run to finish. Requires a running engine (preloop serve); it does not start one.
Local workspace. A run snapshots your working tree, including uncommitted and untracked files that git does not ignore. The run never depends on what you pushed. Interactive debugging. In a terminal, a non-detached run on the default SmolVM backend pauses a failed job for preloop debug. Pass --debug to opt in for detached or non-interactive runs; the AgentENV backend (PRELOOP_VM_BACKEND=agentenv) never pauses implicitly, so it needs the flag as well. pull_request runs. Like GitHub, a local pull_request run tests the merge of the base branch’s current tip into your head; the base is fetched from origin. A conflicted pull request fails the submission with the conflicted files listed, and an unfetchable base fails loudly. --no-merge tests the branch alone in either case. push events are unaffected: they test the commit itself. Derived event data. A local run fills in what git can answer: changed files (for paths: filters, from git diff against the base), the target branch for branches: filters, and the current branch or tag. It does not invent a pull request number, actor, or labels. Pass --payload when a workflow depends on those. Secret preflight. Before submitting, preloop run warns about the secrets.* names the workflow reads that the engine has not stored, and prints the preloop secret set commands to fix them. --strict-secrets turns that warning into a failure. The check reads the engine’s own store — global, repository, and each environment: tier the workflow declares — never GitHub’s, whose values are write-only. A job that passes secrets: inherit to a reusable workflow is called out, because its callee’s names cannot be listed statically. GITHUB_TOKEN and the ACTIONS_* runtime tokens are ignored: the engine mints those per job. --push. A dirty tree is committed as a snapshot of exactly the files that were tested, and that commit is pushed only after the run succeeds. Publishing needs a GitHub App with checks: write to report check runs.

preloop plan

Show the expanded job graph, including matrix fan-out and needs:, without running anything.

preloop status

Show active and recent runs, or print one run’s status word for scripts.

preloop logs

Show run logs. Without flags, every job’s log is merged in dispatch order.
Steps that the runner adds itself, such as Set up job and Complete job, appear in the whole-job log but do not take a --step number.

preloop cancel

Cancels the run. RUN_ID defaults to the most recent active run.

preloop rerun

With no flag, every job runs again as a new attempt on the same run. github.run_attempt increments, and the previous attempt stays in history. A run that the archiver has already moved to history can only be re-run in full, as a new run.

preloop push

Publishes a completed run’s result to GitHub: pushes the tested commit, creates or updates the pull request, and reports check runs. RUN_ID defaults to the most recent run. Re-running the command is safe.

Debug and inspect

preloop debug

Attach to a job paused at a failed step, inspect it, and issue a verdict.

preloop shell

Opens a shell in a VM that was preserved after a failure. Use --preserve-on-failure on run to keep the VM.

preloop dap

Attach an interactive DAP client to a run that was started with debugging enabled.

Configure

preloop init

Configure GitHub credentials, the golden image that every job forks from, workflow requirements, and how the engine runs. In a terminal, init asks each of its five steps. Any flag answers its step without a prompt, so a fully specified command runs without prompting.
The steps are credentials, golden, workflows, run mode, and preflight. The workflow step reads local .github/workflows/*.yml|yaml and .github/actions/**/action.y*ml; a --repo that is not the checkout is read through GitHub when its credential allows it. It never fails init because a repository cannot be read or a secret is missing. Its report lists every secrets.NAME / secrets['NAME'] reference (excluding the engine-minted GITHUB_TOKEN), its file locations, and whether the name is configured globally, for the repository, or for an environment. In a terminal each missing UPPER_SNAKE name can be entered through a hidden prompt; otherwise the report prints the exact preloop secret set NAME --repo OWNER/NAME command. The runs-on table uses the engine’s actual matching rules: base labels are self-hosted, Linux, and the host architecture; ubuntu-latest/ubuntu-24.04 use the official golden, other ubuntu-* labels use the configured golden, and macOS/Windows labels require a matching external runner because the Linux pool cannot host them. Undeclared custom labels need a matching registered runner or an explicit PRELOOP_RUNNER_LABELS value. For --auth app, the stable URL must be an HTTPS, non-loopback URL that GitHub can reach. Answer the prompt with the URL directly; when tailscale is on the host and reports a running tailnet, its own name for the machine (https://<machine>.<tailnet>.ts.net) is offered: Enter accepts it when tailscale funnel status --json shows the webhook path as the only route this node publishes, and it is shown as a hint (blank still skips) when it is not Funnel-published or the Funnel also has other mounts. Type - to skip. The prompt also prints the way there — a Tailscale Funnel on a hostname nothing else serves (tailscale funnel --set-path=/api/v1/github/webhooks --bg http://127.0.0.1:9090/api/v1/github/webhooks; Funnel applies to the host and port, so a root mount beside it would publish the runner-registration surface too — check with tailscale funnel status), a Cloudflare named Tunnel (not a quick Tunnel, whose URL changes on restart) behind a path-filtering proxy, or a domain/reverse proxy; the full guide is in Deploy. Skipping means the App is created with webhooks off: pushes and pull requests will not start runs and PR checks will not report. An existing --public-url or PRELOOP_PUBLIC_URL is used without asking. The URL resolved here is not written to the config file: it is handed to whatever run mode init starts — the foreground serve args, or a PRELOOP_PUBLIC_URL line in the installed service’s environment file — so Check Run detail links and the webhook health check use the address GitHub was given. (A kept credential — the “kept” path — resolves it from --public-url or PRELOOP_PUBLIC_URL, since nothing is reconfigured there.) Without a terminal on both stdin and stdout, or with --json, init never prompts. It then needs --auth, --golden, and --mode. Exit codes: 2 a required flag is missing or invalid. 3 preflight failed (disk, architecture, or hypervisor). 4 resolving, pulling, or building the base image failed. 1 any other failure.

preloop setup github

Configure GitHub credentials directly, without the other init steps. With no App credentials and a public URL, the engine hosts the App-creation form; over SSH the command listens on 127.0.0.1:8765 on the server and you reach it with ssh -L 8765:127.0.0.1:8765 <host>; otherwise it is a single-use local listener. GitHub App walks through all three.

preloop doctor

Reports which GitHub credentials are configured and how many global secrets are stored. With --repo, it also verifies the credential against GitHub, and it exits non-zero if a check fails. It needs a configuration file.

preloop secret

Manage stored secrets. Values are never printed.
The configuration guide explains the scopes and when changes apply.

Serve and operate

preloop serve

Run the control plane and the microVM pool in the foreground. This is the self-hosting entry point. It serves the GitHub webhook and Checks endpoints and starts microVMs for queued jobs. The only foreground entry point that starts an engine (preloop init --mode foreground runs this same mode): every command that talks to an engine requires one that is already running, while local and administrative commands never contact one. On a fresh PRELOOP_HOME it initializes the brand-new control database — the one --store / PRELOOP_STORE_URL selects, which is also the one the boot opens. An existing store is never migrated here; run preloop store migrate for that, and provision a Postgres store yourself.
serve initializes only a brand-new local store and never migrates an existing database. See Configuration and Storage.

preloop server

Install or remove the engine as a supervised service. The install guide explains the scopes.
server install: server uninstall removes the units and keeps the state directory unless you pass --purge-data. It accepts --home, --user, and --dry-run, with the same meanings. server uninstall flags:

preloop store

Manage the control database. These commands are offline. They never start the engine. migrate initializes or upgrades the schema. preloop serve initializes only a brand-new local store and otherwise never applies migrations; run this command explicitly to upgrade an existing store. SQLite migrations take a pre-migration backup by default when the database exists and has pending migrations.

preloop webhooks

Inspect, replay, and check the durable webhook queue. Preloop keeps each delivery’s payload for 30 days, so a delivery can be replayed after GitHub stops offering it. See Webhook resilience.

preloop volumes

Read-only volumes the engine mounts. Each holds one host tree — today the runner bundle, including the Node externals it carries — named preloop-runner-<digest> after the digest of the tree, so a new bundle is a new volume. The engine collects superseded volumes on its own (gc, and the PRELOOP_VOLUMES_* variables); these commands run that plan by hand, or reclaim volumes it deliberately leaves alone. They are offline: they talk to AgentENV (PRELOOP_AENV_BINARY, default aenv) and never bootstrap the engine.

preloop volumes list

Lists the volumes AgentENV holds, with kind, mode, size, status, and the runner/Node versions each one was populated from. AgentENV cannot be asked what is inside a volume, so the versions come from the index the engine writes when it populates one: a volume populated before that index existed, or by another host, reports unknown (pre-manifest or created elsewhere).

preloop volumes prune

Deletes every volume that no sandbox mounts, except the tree the engine would mount. Without --keep that tree is the same selection the pool makes — PRELOOP_RUNNER_BUNDLE when it names a usable Linux runner bundle, otherwise the installed or development bundle beside the binary, including the engine’s own mirror — plus the standalone Node externals (PRELOOP_RUNNER_EXTERNALS, else $PRELOOP_HOME/externals) when one is installed. AgentENV refuses to delete a mounted volume, and that is reported as in use, not as a failure. A dry run cannot tell which volumes a sandbox holds (the server only reveals a reservation when a delete is attempted), so it reports candidate instead of claiming a deletion. Pruning is safe while an engine is running: the provider re-checks that a remembered volume still exists before reusing it, and populates it again if it was deleted. It is not safe while a volume is being populated: one deleted between a populate’s create and its first mount fails that populate.

preloop volumes gc

Collects the volumes nothing needs any more. It keeps the trees named with --keep (without that flag, the same tree selection prune keeps), every volume a sandbox mounts, every volume this engine published and remembers, every volume younger than the grace period or with no recorded creation time, the newest superseded volume per kind for a rollback (PRELOOP_VOLUMES_KEEP_PREVIOUS, default 1), and any staging volume a populate is writing. Remembering a volume keeps it only while it can be mounted: a volume AgentENV reports as failed or uploading is kept until its grace period is over and then reclaimed as unusable, because this engine refuses to mount such a volume — keeping it would leave every start on that tree stuck. It deletes the rest and prints one row per volume — size, kind, age and the reason it is kept or deleted — so --dry-run shows exactly what a real run does. The reasons are live, mounted, populating, young, unknown-age, previous and unmanaged for kept volumes, and superseded, abandoned (a staging volume whose populate is gone) and unusable for deleted ones. A volume whose age nothing recorded — populated by an older build, or by another host — is kept, which is what prune is for. A planned deletion that something claimed between the plan and the deletion — a populate that started while the listing ran, or a volume this engine published for a start that is about to mount it — is printed as left and left for the next pass. A deletion the server refused for any reason other than a mounted volume exits nonzero, as prune does, so a caller can tell that the cleanup it asked for did not happen. The engine runs the same plan in the background after it populates a volume and every PRELOOP_VOLUMES_GC_INTERVAL_SECS (default 21600 seconds), so a bundle change reclaims the bundle it replaced once the grace period is over. The timer starts with the engine, so an engine that only resumes sandboxes it already has still collects. PRELOOP_VOLUMES_GC=off stops those automatic passes and leaves this command.

preloop update

Check for a newer release, or install one. Installs are atomic.
On macOS, update also refreshes the Linux guest runner. On Unix it ensures the SmolVM runtime is present.

Companion client: preloop-runner-client

preloop-runner-client is a separate binary that calls the native REST API directly. Release archives do not include it. Build it from a checkout with cargo build --release -p preloop-runner-client.
The client uses PRELOOP_SYSTEM_TOKEN when it is set. For a loopback server, it can read the engine token from the credential store. Its HTTP timeout is PRELOOP_CLIENT_TIMEOUT_SECONDS, default 30.

Hidden operator commands

These commands are omitted from --help. Release automation and operators who build their own images use them.