> ## 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.

# The config file

> Every key in ~/.outerlayer/config.json.

`outerlayer login` creates `~/.outerlayer/config.json` with owner-only permissions. Every write merges into it, so a key you add by hand survives a later `login`. Remove a key by editing the file.

If the file is not valid JSON, `login`, `logout` and every command that signs in to the gateway stop with an error that names the file. They do not write over it. Fix the file or move it aside, then run the command again.

A `login` that is waiting for approval keeps its one-time key in `~/.outerlayer/login-pending/<id>.json`, readable by you only. `login` and `login --check` remove it when the login ends, and remove any that has passed its expiry.

```json theme={"system"}
{
  "url": "https://gateway.example.com",
  "apiKey": "<key>",
  "appId": "<factory-id>",
  "autoSync": true,
  "tier": "full",
  "repos": { "include": ["github.com/acme/*"], "exclude": ["github.com/acme/secrets"] },
  "scrub": { "literals": ["hunter2"], "patterns": [{ "label": "ticket", "pattern": "ACME-[0-9]{6}" }] },
  "runner": {
    "name": "ci-3",
    "queues": ["implement", "amend"],
    "concurrency": 1,
    "pollSeconds": 60,
    "timeLimitMinutes": 480,
    "idleLimitMinutes": 30,
    "commands": { "implement": "claude -p \"/build\" --permission-mode bypassPermissions", "amend": "claude -p \"/outerlayer-amend\" --permission-mode bypassPermissions" },
    "hooks": { "provision": "/opt/outerlayer/provision.sh", "cleanup": "/opt/outerlayer/cleanup.sh", "report": "/opt/outerlayer/report.sh" }
  }
}
```

| Key | Default | Meaning |
| - | - | - |
| `url` | none | Gateway base URL. Set by `login`, or by `login --url` with a piped key. With no `--url` and no `OUTERLAYER_URL`, a cloud command refuses without it. |
| `apiKey` | none | The factory key, set by a piped `login`. It wins over `accountToken`: see [Credential resolution](/reference/cli#credential-resolution). Never printed back. |
| `appId` | none | The factory id of a factory key, set by `login --app-id`. With an account login instead of a factory key, commands ignore it and use the factory the repository is connected to (`outerlayer connect`). |
| `accountToken` | none | Your account's login token, used only when no factory key is set. Never printed back. `logout` removes it. |
| `dashboard` | none | The dashboard `login` signed in to, so `logout` can revoke the token there. |
| `autoSync` | `true` once the file exists | Whether the hooks run `sync --quiet` in the background and the daemon streams launched sessions. `false` leaves every upload to you. |
| `tier` | `"full"` | Capture tier for a sync without `--tier`: `metrics`, `redacted` or `full`. |
| `repos` | none | Which repositories may sync, and which the runner takes work in. Patterns match the host-qualified remote, such as `github.com/acme/app`. `*` matches anything, and matching ignores case. `exclude` wins over `include`. With `include` set, a session with no remote is skipped. A Linear or Jira item matches by the repository it was added with (`work build --repo`). One added without a repository matches no pattern. An excluded session is skipped for good; loosen the filter, then run `sync --all` to pick it up. |
| `scrub` | none | Extra scrubbers on top of the built-in secret patterns. `literals` are exact strings. `patterns` are regular expressions, each with an optional `label` shown in the redaction marker. |
| `runnerVm` | none | Written by `outerlayer runner init --vm` on a Mac. `instance` is the Lima VM the runner lives in; `since` is when it was made. While set, `doctor` reports the VM and `runner init --vm` refuses to make another. See [Running on macOS](/run-builds-on-macos). |
| `runner.name` | this machine's hostname | The runner's name, which claims are recorded under. One machine can run several runners, so two config files on one machine need distinct names, or both runners can claim the same item. The API reports the value as `host`. `runner.host` is the earlier name of this key. It still works, and `outerlayer doctor` warns about it. Setting both to different values is refused. |
| `runner.queues` | both | Which of `implement` and `amend` this runner takes work from. |
| `runner.concurrency` | `1` | Jobs running at once. A whole number of at least 1. |
| `runner.pollSeconds` | `60` | How often the runner looks for new work. From 5 to 86400. |
| `runner.timeLimitMinutes` | `480` | A job past this age ends `timed_out`. Also the ceiling on any one hook. At most 1320. |
| `runner.idleLimitMinutes` | `30` | A job whose log stops growing for this long, using almost no CPU, ends `idle`. It is also how long a hook may write nothing to its own log before the runner ends it. |
| `runner.commands.implement` / `.amend` | none | The command line that starts the agent for that queue. Runs through `/bin/sh -c`, so `$OUTERLAYER_WORK` expands. Required for every queue in `runner.queues`. |
| `runner.hooks.provision` / `.cleanup` | none | Run before and after the command. Both required. Either an absolute path to an executable, with no arguments and no `$HOME` or `~`, or the same built-in for both: `builtin:container` ([What a build gets](/container-builds)), `builtin:vm` ([MicroVM builds](/container-builds#microvm-builds)) or `builtin:process` ([The process fallback](/write-your-own-hooks#the-process-fallback)). |
| `runner.hooks.report` | none | Optional executable, same path rule, run when a job starts and ends. It cannot be a built-in. |
| `runner.autoUpdate` | `true` when both hooks are built-in, `false` when you supply your own | Whether the runner updates itself to the CLI version its gateway names. It installs each version under `<config dir>/runner/versions/<version>/`. A version that fails its checks is rolled back to the previous one. A runner with no install layout cannot update itself until you run `outerlayer runner install`. See [Keep the runner up to date](/run-a-host-as-a-service#keep-the-runner-up-to-date). |
| `runner.update.maxWaitHours` | `6` | How long an update may wait for builds that do not carry on through a restart while the runner keeps taking work. After that the runner claims nothing until it has updated. `0` holds work as soon as an update is pending. A negative or non-numeric value is refused. A service runner whose builds all carry on through a restart does not wait. See [When the runner holds work for an update](/run-a-host-as-a-service#when-the-runner-holds-work-for-an-update). |
| `runner.build.memory` | `8g` | Container memory limit, with no swap. A microVM build gets this much memory. A number with an optional `k`, `m` or `g`, such as `8g` or `1.5g`. Also the provision hook's `OUTERLAYER_MEMORY`, and the limit of the runner's own image builder. |
| `runner.build.cpus` | `4` | Container CPU limit, above 0. A container build also sees only this many cores, rounded up. A microVM build gets this many virtual CPUs, so it needs a whole number. Also `OUTERLAYER_CPUS`. |
| `runner.build.pids` | `4096` | Container process limit, at least 1. A microVM build has no process limit. Also `OUTERLAYER_PIDS`. |
| `runner.build.disk` | `40g` | The most a container build's volume may hold. A container build past it is released `failed` with `disk_limit_exceeded`. A microVM build's disk is this size, and a write past it fails. Same form as `memory`. Also `OUTERLAYER_DISK`. |
| `runner.build.variables` | `[]` | Container and microVM builds only. Up to 100 names of host variables passed into the build, the only way one enters it. A name the runner sets itself (`OUTERLAYER_*`, `PATH`, `ANTHROPIC_BASE_URL` and the like) is refused. `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN` and `GITHUB_ENTERPRISE_TOKEN` are skipped and logged. `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` are accepted, but the build gets a placeholder, never the value. |
| `runner.build.allowHosts` | none (every public host) | The public hosts a build may reach through the tunnel. Each entry is a host name, an IP address, or `*.` and at least two labels, which matches names under that domain but not the domain itself. At most 500. See [Limiting the hosts a build reaches](/container-builds#limiting-the-hosts-a-build-reaches). |
| `runner.housekeeping.jobsKeepDays` | `30` | Whole days a finished build's job directory is kept after it last changed, at least 1. An unfinished build's directory is never removed. See [Housekeeping](/run-a-host-as-a-service#housekeeping). |
| `runner.housekeeping.buildCacheMax` | `"20g"` | The most the runner's own image builder keeps in its cache, in the same form as `runner.build.memory`, or `"off"` to leave the cache alone. |
| `runner.housekeeping.minFreeDisk` | `runner.build.disk` | Free space the runner's file system must hold for the runner to claim work, in the same form as `runner.build.memory`. Below it the runner claims nothing until there is room. |
| `runner.build.destinations` | `[]` | Credentialed services the runner forwards a build's requests to, with the secret kept on the host. At most 18. `gateway` and `claude` are built in and refused as names. See [Destinations](/destinations-and-secrets). |
| `runner.build.destinations[].name` | none | 1 to 63 lower-case letters, digits and hyphens, unique. Required. |
| `runner.build.destinations[].upstream` | none | The service's `https://` address, with no login, query or fragment. Required. |
| `runner.build.destinations[].header` | none | `Name: value`, set on every forwarded request. The value may read the runner's environment as `${NAME}`. The runner refuses to start while one is unset. A name may not also be in `runner.build.variables`. Required. |
| `runner.build.destinations[].env` | `{}` | Variables set in the build to point its tools at the destination. `{local}` in a value is the destination's local address. A name the runner sets, or another destination sets, is refused. |
| `runner.checks.timeLimitMinutes` | `10` | From 1 to 240. The longest the host checks' environment may take to set up: the container or microVM start, the clone and the lifecycle commands, together. Also the longest one check command or the judge may run. A command past it is ended with its container or microVM and recorded as a fail. A setup past it records every check as a fail. See [Host checks](/reference/build-reference#host-checks). |

The runner needs `url`, `apiKey` and `appId` from a factory key, never a
person's login. `outerlayer runner init` asks for a runner key and writes all
three; see [Give the runner its key](/set-up-a-host#give-the-runner-its-key).
Every `outerlayer runner` subcommand except `container-start` takes
`--config <path>` to use a file other than `~/.outerlayer/config.json`. On
your own account, give the runner a file of its own, such as
`~/.outerlayer-runner/config.json`, so its key does not replace your login.

Every key inside `runner`, `runner.build`, `runner.housekeeping` and
`runner.checks` is checked when the runner
starts and whenever the file changes. An unknown key is refused, naming the
key it is most likely a misspelling of. `outerlayer runner check` reports
all of this without taking any work.

See [Write your own hooks](/write-your-own-hooks) for what each hook receives
and is expected to do.

Environment variables override `url`, `apiKey`, `appId` and `tier`. The runner ignores them and reads only its own config file. See [Environment variables](/reference/environment-variables).

## Other files under \~/.outerlayer

| Path | What it holds |
| - | - |
| `spool/` | Launch records, artifact spool, hook errors. `spool/hook-errors.log` explains an issue that did not reach the Work page. |
| `statusline.json` | The cross-agent figures the status line reads. Kept fresh by the daemon. |
| `cli/` | The installed copy of the CLI that the hooks execute. On a machine with a source checkout of OuterLayer, `doctor` warns when the copy is older than the checkout's build. |
| `login-pending/` | A login waiting for approval. See the top of this page. |
| `hooks-manifest.json` | The hooks `init` installed and where. `doctor` compares it with the settings files to spot hooks that went missing. |
| `last-sync.json` | When the last sync ran, how it ended, and when one last succeeded. The session hooks warn when sync has been failing. |
| `watermarks.json` | For each gateway and factory, the newest transcript already synced, so a sync reads only what is newer. |
| `reported-cost/` | The session cost Claude Code reports to the status line, one file per session, for the uploads to read. |
| `control-planes/` | Bare git caches of the [control planes](/share-instructions-across-repositories) that `outerlayer` fetches. |
| `context/` | The composed copy of each control plane at each commit, as `context/<owner>/<repo>/<commit>/`, that [`outerlayer claude`](/reference/cli-context#outerlayer-claude) attaches. `context/own/` links each checkout's own `.agents/skills/` skills. In a build, the runner writes the control plane here in the build user's home. |
| `bin/` | In a container or microVM build's home only: the `claude` program the runner puts first on the command's `PATH`. It runs `outerlayer claude`. |
| `runner/` | The runner's files. See [Paths](/reference/build-reference#paths). |
| `claude.env` | The runner service's Claude credential, one `NAME=value` line, readable only by the runner's account. The service loads it when it starts. |
| `runner.env` | Secrets that destinations read, values that `build.variables` passes in, and settings such as `OUTERLAYER_HELPER_SOCKET=`. The service reads it only when it starts. |


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