> ## Documentation Index
> Fetch the complete documentation index at: https://docs-stg.outerlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Run builds on macOS

> A Mac with Docker Desktop runs the runner image. A Mac with no Docker runs the runner in a small Linux VM that Lima runs.

A Mac cannot give a build a container of its own, but a Linux VM can.
`outerlayer runner init` offers to run the runner in a small Linux VM with
Docker Engine, which Lima runs. Builds run in containers in the VM and record
`container`, as on a Linux host. This works on Apple silicon and on Intel
Macs.

Use Lima only when the Mac has no Docker. A Mac with Docker Desktop runs the
runner image instead; see
[Run the runner in a container](/run-the-runner-in-a-container). Every route
is in [Choose how to run the runner](/run-work-on-your-machines#choose-how-to-run-the-runner).
A runner installed directly on the Mac does not use Docker Desktop: there,
`runner init` offers only the Lima VM or process builds.

## Set up the VM

Before you start with Lima:

* [Lima](https://lima-vm.io/docs/installation/), installed with
  `brew install lima`.
* A login: run `outerlayer login` on the Mac. It tells setup which
  factories you can use.
* A runner key. Setup prints the link to the factory's API keys page and
  asks you to paste a key made with the **Runner** preset. It checks the
  key and hands it to the VM. The key is never written on the Mac, and your
  login stays as it is. If you cannot create the key, put one a member made
  in [a config file of its own](/set-up-a-host#if-you-cannot-create-the-key)
  with `url`, `apiKey` and `appId`; setup moves them into the VM instead.

If you have never run `outerlayer runner init` on this Mac, nothing else
applies: go to the commands below. Otherwise, setup checks what is already
there:

* A runner block with `builtin:process` hooks moves into the VM, without
  its hooks. Setup refuses a block whose
  hooks are anything else. Remove that block and run setup again.
* Setup refuses when the runner already lives in a VM: the config has the
  `runnerVm` key that a finished `runner init --vm` writes, or a Lima
  instance named `outerlayer-runner` exists. The refusal says how to start
  or delete that VM.

```bash theme={"system"}
brew install lima
read -rs CLAUDE_CODE_OAUTH_TOKEN   # paste the token from `claude setup-token`, then press Enter; nothing is shown
printf '%s' "$CLAUDE_CODE_OAUTH_TOKEN" | outerlayer runner init --vm --agent claude --factory <org>/<factory> --config ~/.outerlayer-runner/config.json
```

`<org>/<factory>` is the factory's name in its dashboard address,
`app.outerlayer.ai/orgs/<org>/factories/<factory>`. Leave `--factory` out
if your login can use only one factory: setup uses it and says which. Setup then asks for the runner key. Paste it when asked; nothing is shown as you paste.

`--agent claude` writes the commands that start Claude Code in the VM's
runner block, and stores the token in the VM. You do not open a shell in
the VM. Leave `--agent` out to use another agent; see
[Using another agent](/set-up-a-host#using-another-agent).

`--vm` answers yes without asking, and `--no-vm` answers no. With neither, a
terminal gets the question and a script gets a refusal that names the runner image and `--vm`.

## What the VM needs

* **Lima.** `outerlayer runner init --vm` runs `limactl`. It refuses with
  `brew install lima` when `limactl` is not on your `PATH`. If a step in the
  VM fails, the Mac's config is unchanged; delete the half-built VM with
  `limactl delete --force outerlayer-runner`.
* **Room for the builds.** With the default limits the VM gets 5 CPUs,
  11 GiB of memory and 90 GiB of disk. The VM is sized from the runner's
  `concurrency` and `build` limits, and never gets more CPUs or memory than
  the Mac has.

  * CPUs: each build is pinned to `build.cpus` cores of its own, rounded
    up. The VM gets that count times `concurrency`, plus one.
  * Disk: the sum of `build.disk` for every build but the last, the larger
    of `build.disk` and `housekeeping.minFreeDisk`, the Docker build cache's
    cap, and 30 GiB for the OS and images. Lima allocates it as it is used.
  * Memory: `concurrency` times `build.memory`, plus 2 GiB for the runner,
    divided by 0.95 and rounded up. A Linux guest reports less memory than
    it is given.
  * Setup makes no VM when the builds do not fit in the memory the VM will
    report. On a Mac with too little memory, lower `runner.concurrency` or
    `runner.build.memory`.

  To change the size later, edit `~/.lima/outerlayer-runner/lima.yaml`, then
  run `limactl stop outerlayer-runner` and `limactl start outerlayer-runner`.
* **A network.** The first run downloads an Ubuntu image, Node and the CLI.
* **Your Claude credential, in the VM.** It is not copied from the Mac's
  environment or files. Setup hands the token you pipe in to the VM the same
  way it hands over the runner key: on the stdin of the VM's own
  `runner init`. That stores `CLAUDE_CODE_OAUTH_TOKEN=<token>` in `~/.outerlayer/claude.env`
  in the VM, readable only by its owner, before the service starts. With
  `--agent claude` and nothing piped, setup refuses before it makes the VM.

## What lives where

The VM is a Lima instance named `outerlayer-runner`, with no directory of the
Mac mounted, so a build cannot read your home directory. It holds the CLI,
the runner and host keys, and a systemd service, `outerlayer-runner`.

The Mac's config gains a `runnerVm` note. A runner key created from your
login was never on the Mac. A key that was in the config loses its place
there: the config loses `apiKey` and `runner`. If you used that key for
capture on the Mac, run `outerlayer login` there again. A runner key bound to an old Mac host key is
refused until a member uses **Clear host key** on the API keys page.

A launch agent, `ai.outerlayer.runner-vm`
(`~/Library/LaunchAgents/ai.outerlayer.runner-vm.plist`), runs
`limactl start outerlayer-runner` when you log in.

## Enable the service

`outerlayer runner init --vm` enables and starts the service for you. It
runs `outerlayer runner install --service` inside the VM, which writes the
same unit as on Linux. You write no unit. The launch agent starts the VM at
login, and the VM starts the service. Before login, the runner does not
run.

With `--agent claude`, the service starts with Claude's commands and
credential, and takes work. Check it from the Mac with
`limactl shell outerlayer-runner outerlayer runner check`.

Without `--agent`, the runner block has placeholder commands, and the
service runs but claims nothing. The runner's entry in the factory says why.
Edit the commands, put your agent's credential in `runner.env`, and restart
the service:

```bash theme={"system"}
limactl shell outerlayer-runner
$EDITOR ~/.outerlayer/config.json
sudo systemctl restart outerlayer-runner
```

To update the service after the config changes, run
`outerlayer runner install --service` on the Mac. It runs the same command
inside the VM and rewrites the launch agent. To remove the service and the
launch agent, run `outerlayer runner uninstall --service` on the Mac. See
[Run the runner as a service](/run-a-host-as-a-service).

## Upgrade and operate the VM

The runner in the VM updates itself to the CLI version your factory names.
It does not follow upgrades of the CLI on the Mac. Setup leaves npm 9.5 or
later in the VM, which self-update needs to verify a release's provenance.
See [Keep the runner up to date](/run-a-host-as-a-service#keep-the-runner-up-to-date).

To upgrade by hand, install the new CLI in the VM, then put it in the
runner's install layout:

```bash theme={"system"}
limactl shell outerlayer-runner
sudo npm install --global @outerlayer/cli@<version>
"$(npm prefix -g)/bin/outerlayer" runner install --service
```

`npm install --global` alone does not upgrade the runner. The service
starts the CLI under `~/.outerlayer/runner/current`, and only
`runner install --service` moves that link. Do not restart the service
yourself. When the runner restarts on the new version is in
[When a build carries on through an update](/run-a-host-as-a-service#when-a-build-carries-on-through-an-update).
See [Upgrade by hand](/run-a-host-as-a-service#upgrade-by-hand).

To try a CLI build that is not published, pack it with `npm pack` and give
the tarball to `outerlayer runner init --vm --vm-cli-package <file>`.

Run runner commands through `limactl shell outerlayer-runner`, for example
`outerlayer runner status` or `outerlayer runner logs <item>`. In the VM, the
runner's config file is `~/.outerlayer/config.json`, so leave out
`--config`. `outerlayer doctor` on the Mac reports the VM,
and says to run `limactl start outerlayer-runner` when it is stopped. It reads
only `~/.outerlayer/config.json` and has no `--config` option. With the runner
key in a config file of its own, `doctor` does not see the VM, so use
`limactl list` to check that it is running.

## A second factory on the same Mac

One Mac runs one VM, and `outerlayer runner init --vm` refuses to make a
second. The one VM can still run a runner for a second factory. Each runner
needs its own config file in a directory of its own, and its own service.
Do this inside the VM:

```bash theme={"system"}
limactl shell outerlayer-runner
mkdir -p ~/factory-b
( umask 077; printf '{"url":"https://api.outerlayer.ai","apiKey":"%s","appId":"<second-factory-id>"}\n' "$RUNNER_KEY_B" > ~/factory-b/config.json )
read -rs CLAUDE_CODE_OAUTH_TOKEN   # paste the token, then press Enter
printf '%s' "$CLAUDE_CODE_OAUTH_TOKEN" | outerlayer runner init --config ~/factory-b/config.json --unit-name outerlayer-runner-b --agent claude
```

That writes Claude's commands and stores the credential in
`~/factory-b/claude.env`. Give it a
runner key of its own: a runner key works only with the first host key that
signs for it. The second runner keeps its host key and its install layout under
`~/factory-b/runner/`, so it updates itself apart from the first.

The VM was sized for one runner's builds. Both runners draw on its CPUs,
memory and disk, so raise them as
[What the VM needs](#what-the-vm-needs) shows. The Mac's launch agent starts
the VM, and so both runners, at login. `outerlayer doctor` on the Mac
reports only the first.

## What process hooks give up

Answer no, and the runner uses `builtin:process`. Builds run on the Mac as
the runner's user, record `shared-user`, and are **not isolated**. See
[The process fallback](/write-your-own-hooks#the-process-fallback).
`outerlayer doctor` names two fixes, in this order: run the runner image on
Docker Desktop, or run `outerlayer runner init --vm` to move the runner
block into a VM.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.