Cleanroom runs CI jobs and other untrusted code in self-hosted Linux microVMs on macOS and Linux, with sub-second VM creation on the fast path and deny-by-default network policy.
It is built for CI first: fast, isolated build and test environments with the same repo, services, caches, and network rules your pipeline expects. The point is to make microVMs cheap enough for normal build and test loops, not just rare high-risk jobs. Agents are the next natural use case. They get running code they can inspect and modify, without drifting away from CI or getting broad access to the host.
The short version:
- hardware VM isolation, not containers
- sub-second VM creation on warm paths
- deny-by-default egress from
cleanroom.yaml, including DNS resolution - host-side gateway backed by
content-cache, so Git, OCI images, Go modules, RubyGems, immutable downloads, and setup outputs can be cached without giving the guest your credentials - standard OCI images as sandbox bases
- repo-aware execution with explicit workspace copy-in and copy-out when needed
- deterministic dependency and service caches for setup declared from explicit inputs and outputs
curl -fsSL https://raw.githubusercontent.com/buildkite/cleanroom/main/scripts/install.sh | bashThe installer puts cleanroom in /usr/local/bin, installs the VM helper where needed, creates runtime config, and starts the daemon.
Need a pinned version, custom install directory, or binary-only install? Use --version, --install-dir, CLEANROOM_INSTALL_DIR, or --no-daemon.
Check the host, then start a repo-agnostic sandbox:
cleanroom doctor
cleanroom sandbox create --image ghcr.io/buildkite/cleanroom-base/alpine:latest
# 01kr7p9ksmfa7rmyypyqw2w8r0
cleanroom exec --in 01kr7p9ksmfa7rmyypyqw2w8r0 -- uname -a
# Linux (none) 6.1.155 #1 SMP Tue Nov 18 09:22:35 UTC 2025 aarch64 Linux
cleanroom sandbox rm 01kr7p9ksmfa7rmyypyqw2w8r0That path does not inspect your local Git checkout. It is the fastest way to prove the daemon, backend, image handling, and guest execution are working.
For a repo-aware run, use a real CI repo. The Buildkite Agent example keeps the policy in this repo so you can try it without changing buildkite/agent:
cd examples/buildkite-agent
cleanroom policy validate
cleanroom exec \
--repo-url https://github.com/buildkite/agent.git \
-- mise x -- go run . --versionThat checks out the latest agent revision at /workspace, resolves it to an exact commit before sandbox bootstrap, warms mise and Go module dependencies from the policy, then builds and runs the agent CLI. In your own repo, commit and usually push cleanroom.yaml so the sandbox sees the same revision CI sees. Use --copy-in or --sync for local edits you have not committed or pushed yet.
Run risky code without giving it your host:
cleanroom sandbox create --image ghcr.io/buildkite/cleanroom-base/alpine:latest
# 01kr7p9ksmfa7rmyypyqw2w8r0
cleanroom exec --in 01kr7p9ksmfa7rmyypyqw2w8r0 -- uname -a
cleanroom console --in 01kr7p9ksmfa7rmyypyqw2w8r0 -- shCopy local work into the sandbox, or bring generated changes back:
cleanroom exec --copy-in -- mise exec -- npm test
cleanroom exec --copy-out -- mise exec -- npm run fmt
cleanroom exec --sync -- mise exec -- npm run generate
cleanroom console --sync -- shFor the lower-level preview, diff, and copy commands, see Workspaces.
Expose a service from the sandbox while a client process is running:
# Raw TCP: host 127.0.0.1:15432 to sandbox port 5432
cleanroom exec --expose 15432:5432 -- postgres
# Local HTTPS: usually https://buildkite.cleanroom.localhost:8143
cleanroom exec --expose-https buildkite:3000 -- mise exec -- npm run devThe installer starts the Cleanroom daemon. It does not install the macOS
resolver or HTTPS trust for cleanroom.localhost, and it does not install a
persistent DNS server. On macOS, run this once before using --expose-https
hostnames:
sudo cleanroom dns installcleanroom dns install writes the resolver and trust material. The local DNS
listener is started by --expose-https and cleanroom expose while exposures
are active.
Run Docker inside the microVM when the workload needs it:
sandbox:
docker:
required: trueCache expensive setup at sandbox creation time:
sandbox:
dependencies:
- name: node
command: npm ci
inputs:
files: [package.json, package-lock.json]
outputs:
dirs: [node_modules]
services:
- name: database
command: |
docker compose up -d postgres valkey
bin/rails db:prepare
docker compose stop postgres valkey
inputs:
files: [docker-compose.yml, db/schema.rb]
outputs:
dirs: [/var/lib/docker]
run:
before: docker compose up -d postgres valkeyUse dependency blocks for deterministic repo-local setup, service blocks for on-disk service preparation, and sandbox.run.before for live startup before each execution.
Cleanroom checks cleanroom.yaml, then .buildkite/cleanroom.yaml.
The policy chooses the image, minimum resources, network rules, Docker requirement, and create-time setup:
version: 1
sandbox:
image:
ref: ghcr.io/buildkite/cleanroom-base/debian@sha256:...
resources:
vcpus: 4
memory: 8GiB
disk: 16GiB
network:
default: deny
allow:
- api.github.com:443
- registry.npmjs.org:443Use cleanroom image resolve <image:tag> to print a digest-pinned image ref, or cleanroom image bump-ref <image:tag> to update cleanroom.yaml.
Repository checkout is on by default for top-level commands:
repository:
remote: origin
path: /workspace
submodules: falseYou usually do not need to write that block. Add it only when you want to override the defaults or disable repo bootstrap:
repository:
enabled: falseCleanroom has a long-running server (cleanroom serve) and a CLI client. The default transport is a Unix socket. HTTP and HTTPS with server-auth TLS are also supported for remote control-plane access.
Each sandbox boots a Linux microVM:
| Host OS | Backend | Notes |
|---|---|---|
| Linux | firecracker |
Persistent sandboxes, per-sandbox TAP and guest IP identity, file copy, egress allowlist enforcement |
| macOS | darwin-vz |
Persistent sandboxes, file copy, filehandle networking with allowlist egress filtering, no Firecracker-style TAP parity |
Images are standard OCI images. Cleanroom materializes them into ext4 rootfs files for the selected VM backend. You can pull, list, remove, import, resolve, and bump image refs with cleanroom image.
Allowed Git and package traffic goes through the host gateway. The guest can fetch what policy allows, but upstream credentials stay on the host side.
- Hostname allow rules are currently enforced from observed DNS answers plus destination IP:port. They do not distinguish co-hosted services on the same IP and port.
darwin-vzdoes not expose Firecracker-style TAP devices or host-visible guest IP identity.- General UDP and IPv6 allowlist policy is not at parity with the TCP gateway path.
- Dirty local files are not copied into repo-aware runs unless you use
--copy-in,--sync, or the explicit workspace commands. - macOS hosts need the signed
cleanroom-darwin-vzhelper plusmkfs.ext4anddebugfsfrome2fsprogs.
cleanroom doctor
cleanroom doctor --json
cleanroom sandbox ls
cleanroom sandbox inspect <sandbox-id>
cleanroom sandbox inspect --last
cleanroom execution ls
cleanroom execution inspect --last
cleanroom status --last
cleanroom versioncleanroom exec and cleanroom console keep failure stderr focused on guest output. Use --print-sandbox-id, cleanroom status --last, or cleanroom execution inspect ... when you need retained control-plane details.
mise run build
mise exec -- go test ./...
mise run lint-shellBuild base images locally:
mise run build:imagesInstall the locally built binaries into /usr/local/bin:
mise run install:global- Policy -
cleanroom.yaml, resources, networking, Docker, dependencies, and services - Workspaces - repo-aware execution, local edits, copy-in, copy-out, and sync
- Networking - deny-by-default egress, DNS, the host gateway, and service exposure
- Caching - content-cache routes, dependency outputs, service outputs, and storage cleanup
- Backends - macOS and Linux support, backend capabilities, and host requirements
- Operations - daemon, runtime config, diagnostics, storage, and observability
- Snapshots - create, inspect, restore, and remove sandbox filesystem snapshots
- API - ConnectRPC control API for integrations