Skip to main content
This page takes one Linux machine from nothing to a finished build. On the way, outerlayer runner init installs the service that keeps the runner running.

Before you begin

You need:
  • Node.js 22 or later, and the CLI: npm install -g @outerlayer/cli. A service needs a fixed path to start, so use the global install rather than npx.
  • A way to create an API key on the factory. The owner, admin and write roles can. If you cannot, a member who can creates the runner key and gives it to you; see If you cannot create the key.
  • The factory’s name, as <org>/<factory>. It is in the factory’s dashboard address, app.outerlayer.ai/orgs/<org>/factories/<factory>. If your login can use only one factory, runner init uses it and you need no name.
  • The GitHub App installed on the repository. Otherwise every build fails at provision with repository_not_in_installation. See Connect a repository and GitHub App permissions and build tokens.
  • Docker Engine 26 or later, on Linux or WSL2, usable by the runner’s account. Docker Desktop alone is not enough for a runner installed on the machine. On a Mac, follow Run builds on macOS. On Windows, follow Run builds on Windows. To run the runner from a container image with no CLI installed, follow Run the runner in a container.
  • On a Mac, Docker Desktop and the runner image. The image runs the runner on the Docker Desktop that the Mac already has, with no CLI to install. Follow Run the runner in a container.
  • On a Mac without Docker, Lima. outerlayer runner init --vm uses it to run the Linux VM that the runner lives in. Install it with brew install lima; see Lima’s install page.
  • Docker’s Buildx plugin, 0.18 or later, and access to Docker Hub. The runner builds each repository’s image in a builder of its own. It is docker-buildx-plugin in Docker’s apt repository. Without it the runner claims nothing. See Image builds.
  • For microVM builds: x86_64 Linux where the runner’s account can open /dev/kvm. Without it, builds run in containers.

Give the runner its key

The runner needs a key of its own: an API key with the Runner preset. The preset holds work.claim and hosts.ingest, among others. A login never creates a key, so runner init asks you for one. When the runner’s config file has no apiKey, runner init prints the link to the factory’s API keys page and asks you to paste the key. Nothing is shown as you paste; press Enter when you are done. It checks that the gateway accepts the key, and writes it into the config file. Each runner gets its own key, because a runner key works only with the first host key that signs for it. A factory key outranks an account login. If runner init wrote the key into ~/.outerlayer/config.json, it would replace your own login there, and your sessions would switch factory. Give the runner a config file of its own instead: the commands below pass --config ~/.outerlayer-runner/config.json to each outerlayer runner command. runner init creates that file, owner only, and keeps its host key, install layout, claude.env and runner.env beside it. Log in on the machine first, and approve the link it prints. The login tells runner init which factory you can use. On a machine with no browser, add --no-browser and open the link on another machine.
Then create the key. In the factory, open Settings, then the API keys tab (app.outerlayer.ai/orgs/<org>/factories/<factory>/settings/api-keys), choose Create API key, and pick the Runner preset. The key is shown once, so keep the page open until runner init has accepted it.

If you cannot create the key

Ask a member who can create keys to make one with the Runner preset. runner init also stops, and prints the same link, when it has no terminal to ask on. That happens in a script, or in a session over a connection with no terminal. In either case, put the key in the config file yourself. Run these lines one at a time. Each read waits for you to paste a value and press Enter. read -rs shows nothing as you paste, so the key stays off the screen. The echo after it prints only the key’s length, so you can tell the paste worked. The Factory Id is on the factory’s Settings → General tab.
If the echo prints 0 characters, the key is empty. Run the read line again before you write the file. runner init below then uses this key and asks for none. Leave --factory out of the commands below, because a config file that holds a key already names its factory. On a machine that only runs the runner, you can save the key with outerlayer login instead. The key is read from stdin, so it stays out of your shell history. The dialog that shows the new key also shows this command, filled in. If you save it this way, leave --config out of every command below.

Set it up

The runner needs a Claude credential. A build never sees the credential itself. Create a token; on a host with no browser, claude setup-token prints a link to open on another machine:
Then give the token to runner init. read -rs keeps it off the screen and out of your shell history:
runner init writes the runner’s block of the config file: ~/.outerlayer/config.json, or the file you gave with --config. When that file has no apiKey, it first asks you to paste the runner key for the factory --factory names. It checks the key before it changes anything else. With no --factory, it uses the only factory your login can use and says which. If your login can use several, it lists each as a --factory value and stops. --app-id takes the Factory Id from the factory’s Settings → General tab instead. It names the built-in hooks that fit the machine: builtin:vm on x86_64 Linux whose account can open /dev/kvm, else builtin:container where Docker Engine 26 or later runs. The full rule is in outerlayer runner init.
  • On macOS, --vm runs the runner in a Linux VM and --no-vm declines it. With neither, a terminal gets the question, and a piped command such as the one above refuses and names the runner image and --vm. See Run builds on macOS.
  • On any other machine that cannot isolate a build, it refuses and writes nothing. Run it again with --allow-process-builds to name builtin:process, where builds run as your account with no isolation.
builtin:process has no isolation. A build runs as your account, can read your files and the runner’s own environment, and can reach the network directly. See Security and limits before you use it.
With --agent claude, runner init also:
  • writes a command for each queue that starts Claude Code: claude -p "/build" --permission-mode bypassPermissions for implement, and claude -p "/outerlayer-amend" --permission-mode bypassPermissions for amend. /build and /outerlayer-amend are skills that outerlayer init installs in the repository.
  • stores the token in claude.env, beside the config file, readable only by your account. An Anthropic API key works too: pipe it the same way, and runner init tells the two apart by how they start. It removes any Claude credential line from runner.env. Any secret a destination reads goes in runner.env, and runner init keeps those lines.
It then installs and starts the service that keeps the runner running, with the credential loaded; see Keep it running and What the service sets.
By default a build can reach every public host, and can send anything it can read to any of them. List the hosts your builds need in build.allowHosts; see Limiting the hosts a build reaches.
To use your own isolation, see Write your own hooks.

Using another agent

Without --agent, runner init writes a placeholder command for each queue, and stores no credential. The runner claims nothing while a queue’s command is still the placeholder. Edit runner.commands in the config file to start your agent on item $OUTERLAYER_WORK. The running service reads the edited file on its next poll, with no restart. Put the credentials your agent needs in runner.env, readable only by the runner’s account, and restart the service, which reads that file only when it starts. Claude’s credential is the exception: it lives in claude.env.

Check the host before the runner takes work

check validates the config, confirms the key works, and prints the settings the runner would use. It takes the Claude credential from your shell, and from the service’s credentials directory. When the shell has none, it reads the claude.env beside the config, then runner.env. It names the file when it used it. It exits 1 and says what to fix when:
  • a queue’s command is still the placeholder.
  • a claude command has a --permission-mode Claude Code does not accept, such as bypassPermissons. The message names the command and the value.
  • built-in hooks find the credential neither in your shell nor in claude.env or runner.env.
  • the key does not hold work.claim. Create it with the Runner preset.
If it prints credentials unverified, the gateway did not answer normally. Run it again later. outerlayer doctor reports what isolation a build gets on this machine. It reads the credential from the service’s claude.env, then runner.env.

Keep it running

outerlayer runner init already set up what keeps the runner running, with the same command on every system. You write no service file. It restarts the runner after a crash, a reboot and a self-update. It asks for sudo where it needs root, and names each command first.
  • Linux: the runner runs from boot, as a systemd service.
  • macOS: the runner runs from login, as the same service inside the Linux VM that --vm makes.
  • Windows: the runner runs from login, as the same service inside WSL2.
To set it up again after the config changes, or to remove it, see Run the runner as a service.

Run the runner in a terminal instead

The service and a terminal runner cannot run on the same config. To run the runner in a terminal, remove the service first:
The runner then needs the credential in that shell’s environment. It refuses to start, naming the problem, when the config is wrong, a hook is not an executable file, or the key is refused. Its first claim binds the runner key to this machine; see The host key.

Build your first item

Ask for a build as yourself, not with the runner key. A build requested with a runner key gets no repository tokens, so it fails. Either:
  • choose Build beside the issue on the Work page, or
  • run this from your own machine:
The first build of a repository also builds its image, so it takes longer. The first microVM build also downloads Firecracker and a guest kernel from github.com.

Watch it run

From a second terminal on the host:
status lists running builds and their stage. logs -f follows one item’s build log. outerlayer runner status --recent shows finished builds. The session and the pull request land on the item’s page. If something goes wrong, see Troubleshoot builds. Every key is in The config file, and every command in Runner commands.