Skip to main content
Most hosts need none of this page. outerlayer runner init names built-in hooks: builtin:vm or builtin:container where the machine can isolate a build, and builtin:process where it cannot and you allow it. The full rule is in outerlayer runner init. Write your own hooks to give builds an isolation of your own, such as a cloud VM or a sandbox your platform team runs. The runner runs the provision hook before the command, the cleanup hook after it, and an optional report hook at both ends. See What happens to one build. Name your scripts in the runner block of ~/.outerlayer/config.json:
Each value is the absolute path of an executable, with no arguments; see Hooks are executables. provision and cleanup are required. report is optional and is never a builtin: value. See the config file.

The build’s variables

Every hook and the command receive these, and so does everything they start. They also inherit the runner’s own environment, except any OUTERLAYER_* variable and the secrets a destination’s header names. A container build gets only the variables named in runner.build.variables. For anything else about the item, call the CLI:
Neither carries the issue’s body.

Hooks are executables, commands are shell lines

A hook path is run directly: no shell, no arguments, no variables expanded. "/opt/outerlayer/provision.sh" works. "$HOME/bin/provision.sh" and "/opt/outerlayer/provision.sh --fast" fail with an error naming the path. Put arguments and expansion inside the script. Command lines run through /bin/sh -c, so variables in them expand. A hook that goes quiet is ended. A hook silent in its log for idleLimitMinutes gets SIGTERM, then SIGKILL, logged as #9 provision idle for 30m, ended. No hook may outlast timeLimitMinutes. A provision hook ended this way fails the build. A cleanup or report hook ended this way leaves the build’s outcome as it was. Print progress.

The provision hook

Runs before the command, in $OUTERLAYER_JOB_DIR. It receives the build’s variables and these:

provision.out

The hook writes key=value lines to $OUTERLAYER_JOB_DIR/provision.out: A key the runner cannot honour, such as a relative path, fails the build at provision and names the key.

The tunnel and the proxy

The hook can mount $OUTERLAYER_BUILD_DIR into the isolation. With an exec, HTTPS_PROXY and its siblings point at the broker address, or at 127.0.0.1:3128 when there is none. Make that port reach the tunnel by running the relay inside the build’s network:
Each --forward <port>=<socket> relays one port to one destination; see Destinations.

Exit status

  • Exit 0 runs the command. A workdir that does not exist fails the job with outcome failed.
  • Exit 75 (EX_TEMPFAIL) declines the item: this host cannot take it now, for example with too little disk free. The runner releases the claim with no outcome, and the item stays queued for another host or a later poll.
  • Any other non-zero exit fails the job with outcome failed.
The command then runs in workdir. It must not wait for input.

The cleanup hook

Runs after the command, however it ends, and again on recovery for a job the runner lost. It receives the build’s variables plus:
  • OUTERLAYER_OUTCOME: ok, failed, timed_out, idle, lease_lost, interrupted or stopped. Empty when provision exited 75. Troubleshoot builds explains each. It is ok for any command that exited 0: the runner reads the item’s pull request and checks after cleanup, and only then can release the build incomplete.
  • OUTERLAYER_LOG: the path of the command’s output log.
  • OUTERLAYER_BUILD_DIR: so the hook can unmount what it mounted there.
It must remove everything the build created, and running it twice must change nothing. The cleanup and report hooks may write one line to $OUTERLAYER_JOB_DIR/reason. The runner sends it with the release, cut to 200 characters.

The report hook (optional)

Runs when a job starts, with OUTERLAYER_PHASE=start, and when it ends, with OUTERLAYER_PHASE=end, OUTERLAYER_OUTCOME and OUTERLAYER_LOG. Use it to comment on the issue. The log is always $OUTERLAYER_JOB_DIR/command.log, though the start phase is not given it. The end phase’s OUTERLAYER_OUTCOME is the outcome the release carries. So a command that exited 0 while the item has no pull request, or its checks still fail, is incomplete here. See When the command exits 0.

The process fallback

A machine that cannot isolate builds can still run them through the tunnel with both hooks set to builtin:process. runner init --allow-process-builds sets this on a machine without Docker Engine 26 or later; without the flag, runner init refuses there.
The runner clones the repository and runs the command as the runner’s user, with the same proxy variables a container build has. The build records shared-user. This is not a boundary. The build can read the runner user’s files, the runner’s environment (including destination secrets and Claude’s credential), the item key and the config file. It has the host’s network. npm, git, curl and Node 22.21 or later honour the proxy variables; a tool that ignores them bypasses the tunnel and shows nothing in the release. Use builtin:container where you can.