Skip to main content
A container build runs each item in a locked-down container of its own. The runner starts it, serves its only network exit, and holds every credential the build uses. Set both the provision and the cleanup hook to builtin:container:
Name both hooks or neither. The host needs Docker Engine 26 or later. If it is older or Docker is not running, the build fails at provision and says which.

MicroVM builds

With both hooks set to builtin:vm, each build runs in a Firecracker microVM with its own kernel. The host must be x86_64 Linux, with Docker Engine 26 or later and Docker Buildx 0.18 or later. The runner’s account must be able to open /dev/kvm. runner init names builtin:vm on such a host; the full rule is in outerlayer runner init. outerlayer doctor fails builtin:vm hooks on any other host, and says why. The microVM is made from the recipe’s image, with the same tunnel, tokens and limits a container build has. The tunnel, the destinations, the branch rule and the failure reasons below apply to it too. It has no network device: its connections leave over vsock to the runner’s tunnel. A recipe that adds the docker-in-docker feature gets its own Docker daemon inside the microVM. That is how a build that needs Docker runs. The first microVM build downloads a pinned Firecracker release, a guest kernel and a small guest helper from github.com. The runner checks each file against a sha256 pinned in the CLI, and fails the build at provision if one differs.

What a build gets

The runner builds the repository’s image, as outerlayer runner image does, and starts one container from it. The container has no network but loopback, a read-only root filesystem, and one volume for the clone. Your home directory, other builds and the Docker socket are out of reach. Commands run as the recipe’s remoteUser, else as the image’s own user. When that is root, or neither names one, they run as ol-agent. A lifecycle command that exits non-zero fails the build at provision, with a reason naming the command.

The build block

variables is the only way a host variable enters a build. Claude’s credential reaches it only as a placeholder. Repository tokens (GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN) are never passed; the runner logs each name it skipped. A name the runner sets itself, such as HOME, PATH or a proxy variable, is refused. To size the host for these limits, see Memory limits.

The gateway and Claude

Every build the runner provisions with a built-in hook (builtin:vm, builtin:container or builtin:process) gets two built-in destinations. The build holds neither the item key nor Claude’s credential. The runner reads CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token), or else ANTHROPIC_API_KEY, from its own environment; restart it after a change. A terminal claude login is not forwarded. With neither set, each build fails at provision as agent_credential_missing. See Give the runner Claude’s credential. A build your own hooks run gets no built-ins. It holds the item key as OUTERLAYER_API_KEY; see The build’s variables.

The tunnel

The container’s only way out is a tunnel the runner serves for that build. The runner sets the standard proxy variables to it, http://127.0.0.1:3128. A tool that ignores them cannot connect; set pip’s --proxy or Maven’s settings.xml to that address in the recipe. A build cannot reach the host’s own addresses, private or loopback addresses, link-local addresses including the cloud metadata address 169.254.169.254, or Tailscale addresses. A refusal is a 403 naming the range. Listing such a host in allowHosts does not change that. To reach a private service, make it a destination.

Limiting the hosts a build reaches

By default a build can reach every public host, and can send anything it reads, such as a build.variables value or its read token, to any of them. Set build.allowHosts to close that.
An entry is a host name, an IP address, or a pattern such as *.githubusercontent.com, which matches names under the domain but not the domain itself. An empty list refuses every public host. An unlisted name gets a 403 with the range not-allowed. The list does not limit the gateway, Claude, your destinations or the git remote. To find the hosts your builds need, run a few without the list, then run outerlayer doctor on the host. While the list is unset, its “Build hosts” check warns and lists every host past builds reached, ready to paste into allowHosts. Review it first: a host a build should never have reached is in it too. Each build’s own list is hosts in its environment (see What a build records).

The git remote and the build’s tokens

A container build holds no credential that can write to its repository. Git and gh reach GitHub through the runner, which adds short-lived read and push tokens the gateway issues (GitHub App permissions and build tokens). gh can read, such as gh pr view, but not write. The branch rule. A push may update only the claim’s branch. On an outerlayer/… branch any update goes through, force-pushes included; elsewhere only creating the branch and fast-forwards. A refused push updates no ref, the runner logs push refused: <refs>, and git prints:
A gateway with no GitHub App configured refuses every read token with repository_tokens_unavailable, so every container build fails at provision. Configure the App first.

A governed repository’s context

When a control-plane repository governs the item’s repository, a container build starts with its context (AGENTS.md, .claude/, .outerlayer/) in the checkout. The runner fetches it on the host; the build never commits it. The host’s git login must read the control plane. outerlayer doctor checks this as Control plane access.

Failure reasons

A read token that fails for a temporary reason, such as github_unavailable, releases the claim with no outcome; the runner retries on its next poll. A refused push token fails the push, and the build fails at its next health check.

Stopping a build

Stopping a build stops its container. Cleanup removes the container and volume; a runner that crashed finishes it on restart. If the runner logs cleanup failed, its next housekeeping pass removes what the build left, within the hour. To free the space sooner, remove what it names with docker rm --force and docker volume rm.

What a build records

outerlayer work status --json shows each build’s environment under claims: the isolation, image, hosts reached and refused, variable and destination names, and CLI versions. It never holds a secret.