---
title: "Config reference"
url: https://devlanes.run/docs/reference/config/
description: "Every field of devlanes.yaml and of the per-user config.yaml, generated from the JSON Schemas devlanes publishes."
updated: 2026-11-02T00:53:29.000Z
---

# Config reference

<!-- Generated from crates/config/schema.json and crates/config/user-schema.json by crates/config/tests/it/reference_docs.rs. Edit the doc comments in crates/config, then run DEVLANES_BLESS=1 cargo nextest run -p devlanes-config. -->

Every field of `devlanes.yaml` and of the per-user `config.yaml`, generated from the JSON Schemas devlanes publishes. In a path, `<name>` stands for a key you choose (a service, task or profile name) and `[]` for an item of a list. A type marked *templated* also accepts a `${{ ... }}` expression, evaluated for each lane. A *duration* is a number with `ms`, `s`, `m`, `h` or `d`, and a *scalar* is a string, number or boolean. [examples/devlanes.yaml](https://github.com/devlanes-run/devlanes/blob/3afa327903b6275d80d44ea599d8b71b274abc95/docs/examples/devlanes.yaml) shows the fields in context.

## Project config

`devlanes.yaml` at the repository root or in a subdirectory; devlanes uses the nearest one above the current directory, up to the repository root. Another project file in the same directory, such as `devlanes.prod.yaml`, is named once with `--file`. Each project file can have local overrides in a `.local.yaml` next to it. Schema: `https://devlanes.run/v1/schema.json`.

### extends

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `extends` | string or list of string | Other project files this one builds on: one path or a list, relative to this file, such as `devlanes.yaml` for a `devlanes.prod.yaml` that runs the same apps another way. They are merged in order, each with its own `.local.yaml`, then this file and its `.local.yaml` go last, so later files win. Mappings merge by key and lists and single values replace, as with local overrides. An extended file may be partial and its `project` is ignored; paths in it mean what they would in this file. | `devlanes.yaml` |

### global_env

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `global_env` | map of scalar (templated) | Variables for every service and task of the lane, above devlanes' own variables (`PORT`, `DEVLANES_*`) and below each unit's `env` and the profile's `env`. Use it for values many units share, such as a database URL built from `${{ infra.postgres.port }}`. Never set `PORT` here: it would replace every service's own port. | `{"API_URL":"${{ services.api.url }}"}` |

### infra

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `infra` | object | Containers the lanes depend on (databases, brokers, object stores), started and waited on before setup, tasks and services, plus the commands that create and remove each lane's data in them. | `{"compose":"docker-compose.yml","hidden":["postgres"]}` |
| <code>infra.<wbr>compose</code> | string or list of string | Compose file, or a list of files merged like repeated `-f`, relative to the project. Its services are shared by every lane unless listed in `per_lane`; the shared project runs from the primary worktree with that worktree's `.env` as `--env-file`. | `docker-compose.yml` |
| <code>infra.<wbr>containers</code> | map of object | Docker containers run without a Compose file, keyed by name and reached as `infra.<name>` in expressions. | `{"cerbos":{"image":"ghcr.io/cerbos/cerbos:0.40.0","per_lane":true,"ports":{"http":3592}}}` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>command</code> | string (templated) or list of string (templated) | Command replacing the image's default: a list is used as is, a string runs through `/bin/sh -c` inside the container. | `["server","--config=/policies/config.yaml"]` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Environment variables set inside the container. | `{"LOG_LEVEL":"debug"}` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>image</code> | string (templated) | Image to run. Required. A running container with the same image, command, env, mounts and ports is reused; any change recreates it. | `ghcr.io/cerbos/cerbos:0.40.0` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>per_lane</code> | boolean (templated) | Run one container per lane instead of one shared by every lane. Defaults to false. Per-lane containers and their named volumes are removed by `devlanes lane clean`. | `true` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ports</code> | map of integer (templated) | Container ports by name. devlanes publishes each on a host port on 127.0.0.1, read as `${{ infra.<name>.ports.<port> }}`; `infra.<name>.port` is the first one. | `{"grpc":3593,"http":3592}` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready</code> | `started` or object | Extra check the start waits for once the container is up (the image's healthcheck, else its first port accepting TCP), before setup, tasks and services run. Write `started` or one of `tcp`, `http`, `log` (matched against the container's logs) or `cmd` (run on the host). If it does not pass within its timeout (default 60s), the start fails. Values: `started`: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe. | `started` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>cmd</code> | string (templated) or list of string (templated) | Ready once this command exits 0. It runs on the host through `/bin/sh` (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service's environment and working directory. | `curl -fsS ${{ self.url }}/healthz` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>http</code> | object | Ready once a GET request to `path` on the unit's main port answers with `status`, or with a 2xx status when `status` is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends `User-Agent: devlanes-ready`, so an app can leave the checks out of its request logs. | `{"path":"/healthz","status":200,"timeout":"60s"}` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>http.<wbr>host</code> | string (templated) | Host header the check sends, for apps that answer only on their own hostname, such as `${{ self.hosts[0] }}`. The request still goes to 127.0.0.1; only the header changes. Defaults to `127.0.0.1:<port>`. Spaces and control characters are refused. | `${{ self.hosts[0] }}` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>http.<wbr>path</code> | string (templated) | Path requested with GET, such as `/healthz`; defaults to `/`. | `/healthz` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>http.<wbr>status</code> | integer (templated) | Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as `302` for a route that redirects to a login. | `200` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>http.<wbr>timeout</code> | duration (templated) | How long the HTTP check may take to pass, replacing the check's `timeout`; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails. | `15s` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>log</code> | string (templated) | Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected. | `Connected to Redis` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>tcp</code> | boolean or string | Ready once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. `true` uses the unit's main port; a port name such as `inspector` uses that named port. It only proves the socket is open, not that the app works. | `true` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>ready.<wbr>timeout</code> | duration (templated) | How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and `after: services` tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen. | `15s` |
| <code>infra.<wbr>containers.<wbr><name>.<wbr>volumes</code> | list of string (templated) | Mounts as `source:destination[:mode]`. `./relative` and `/absolute` sources are bind mounts, relative ones from the owning worktree; a bare name is a named volume kept per project or per lane. | `["./infra/cerbos:/policies:ro"]` |
| <code>infra.<wbr>env</code> | map of scalar (templated) | Variables for `${VAR}` interpolation in the Compose files only; processes do not see them (use `global_env` for those). | `{"ACME_DB_PORT":15432}` |
| <code>infra.<wbr>exclude</code> | list of string | Compose services devlanes never starts, such as a local reverse proxy the devlanes proxy replaces. | `["caddy"]` |
| <code>infra.<wbr>external</code> | map of object | Servers you run yourself on a loopback port, such as pgAdmin, keyed by name. Every lane lists them, the proxy routes their hostnames, and machines connected with `devlanes remote connect` mirror them while a lane of the project runs. devlanes never starts or checks them. Put personal ones in `devlanes.local.yaml`. | `{"pgadmin":{"address":"127.0.0.1:5050","hosts":"pgadmin.${{ lane.domain }}"}}` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>address</code> | string | Where the server listens, `127.0.0.1:<port>` or `[::1]:<port>`. Only loopback addresses work. A port that devlanes or a lane already uses is an error. Required. | `127.0.0.1:5050` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hidden</code> | boolean | Keep the entry away from machines connected with `devlanes remote connect`. They never see, mirror or reach it. This machine still routes its hostnames. Defaults to false. | `false` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts</code> | string (templated) or object or list of string (templated) or object | Hostnames the devlanes HTTPS proxy routes to the port. One entry or a list. An entry is a hostname, or a mapping with `host` and optional `fallback` and `host_header`. Expressions can use `lane`, `project`, `infra` and `env`. Put the names under `${{ lane.domain }}` so each lane gets its own. The first host without a wildcard becomes the unit's URL. | `pgadmin.${{ lane.domain }}` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts.<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Defaults to false. | `true` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts.<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself. Quote it in YAML. Labels hold letters, digits and inner hyphens. Case and a trailing dot are ignored. | `pgadmin.${{ lane.domain }}` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts.<wbr>host_header</code> | `preserve` or `upstream` | Host header the server receives. Defaults to `preserve`, the hostname the browser asked for. `upstream` sends the address devlanes connects to, such as `127.0.0.1:5050`, for servers that only accept their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts[].<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Defaults to false. | `true` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts[].<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself. Quote it in YAML. Labels hold letters, digits and inner hyphens. Case and a trailing dot are ignored. | `pgadmin.${{ lane.domain }}` |
| <code>infra.<wbr>external.<wbr><name>.<wbr>hosts[].<wbr>host_header</code> | `preserve` or `upstream` | Host header the server receives. Defaults to `preserve`, the hostname the browser asked for. `upstream` sends the address devlanes connects to, such as `127.0.0.1:5050`, for servers that only accept their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>infra.<wbr>hidden</code> | list of string | Containers that machines connected with `devlanes remote connect` never see, mirror or reach. | `["postgres"]` |
| <code>infra.<wbr>hosts</code> | map of string (templated) or object or list of string (templated) or object | HTTPS hostnames for containers, keyed by Compose service or plain container name, so a browser can open a container's web port: a MinIO console or presigned S3 URLs, Mailpit, a mock OIDC provider. A hostname routes to the container's first published port unless its route sets `port`; each key takes one hostname or route, or a list. | `{"s3mock":"s3.${{ lane.domain }}"}` |
| <code>infra.<wbr>hosts.<wbr><name>.<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>infra.<wbr>hosts.<wbr><name>.<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>infra.<wbr>hosts.<wbr><name>.<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>infra.<wbr>hosts.<wbr><name>.<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>infra.<wbr>hosts.<wbr><name>[].<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>infra.<wbr>hosts.<wbr><name>[].<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>infra.<wbr>hosts.<wbr><name>[].<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>infra.<wbr>hosts.<wbr><name>[].<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>infra.<wbr>per_lane</code> | list of string | Compose services run once per lane instead of shared, each in its own Compose project with ports from the lane's block and its own named volumes, removed by `devlanes lane clean`. Needs Compose 2.24.4 or newer. | `["mailpit"]` |
| <code>infra.<wbr>profiles</code> | list of string | Compose profiles to enable when resolving and starting the Compose services. | `["development"]` |
| <code>infra.<wbr>project_name</code> | string (templated) | Name of the shared Compose project, fixed so `docker compose -p <name>` reaches the same containers outside devlanes. Defaults to the name Compose picks itself. | `acme` |
| <code>infra.<wbr>setup</code> | map of object or string (templated) or list of string (templated) | Named commands that prepare each lane's data, such as creating its database in a shared Postgres. They run once per lane after the containers are ready, all at the same time, and are recorded as done so later starts skip them; a failure stops the start unless the entry sets `ignore_errors`. | `{"database":"docker compose exec -T postgres createdb acme_${{ lane.name }}"}` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>container</code> | string | Run this setup entry again whenever the named container has restarted since it last ran, for data that lives in the container and may be gone, such as a RabbitMQ vhost. | `rabbitmq` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>cwd</code> | string (templated) | Working directory, relative to the project file's directory in the lane's worktree; defaults to that directory. | `infra` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Variables for this command only. | `{"AWS_DEFAULT_REGION":"us-east-1"}` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>ignore_errors</code> | boolean (templated) | Count a failed or timed-out command as done instead of failing the start, for commands whose "already exists" error is fine. Defaults to false. | `true` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>run</code> | string (templated) or list of string (templated) | Command to run: a string runs through `/bin/sh -c`, a list runs the program directly. Write the command you mean, such as `docker compose exec -T postgres ...`; devlanes runs it as written. Required. | `pnpm dev` |
| <code>infra.<wbr>setup.<wbr><name>.<wbr>timeout</code> | duration (templated) | Longest the command may run. When it runs out, the command is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed, so a setup entry fails the start unless `ignore_errors` is set. No limit by default. | `15s` |
| <code>infra.<wbr>teardown</code> | map of object or string (templated) or list of string (templated) | Named commands that remove a lane's data when the lane is cleaned (`devlanes lane clean`), one after another; never on stop. A failure is reported and the cleanup goes on. | `{"database":"docker compose exec -T postgres dropdb --if-exists acme_${{ lane.name }}"}` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>container</code> | string | Run this setup entry again whenever the named container has restarted since it last ran, for data that lives in the container and may be gone, such as a RabbitMQ vhost. | `rabbitmq` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>cwd</code> | string (templated) | Working directory, relative to the project file's directory in the lane's worktree; defaults to that directory. | `infra` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Variables for this command only. | `{"AWS_DEFAULT_REGION":"us-east-1"}` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>ignore_errors</code> | boolean (templated) | Count a failed or timed-out command as done instead of failing the start, for commands whose "already exists" error is fine. Defaults to false. | `true` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>run</code> | string (templated) or list of string (templated) | Command to run: a string runs through `/bin/sh -c`, a list runs the program directly. Write the command you mean, such as `docker compose exec -T postgres ...`; devlanes runs it as written. Required. | `pnpm dev` |
| <code>infra.<wbr>teardown.<wbr><name>.<wbr>timeout</code> | duration (templated) | Longest the command may run. When it runs out, the command is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed, so a setup entry fails the start unless `ignore_errors` is set. No limit by default. | `15s` |

### lanes

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `lanes` | object | Which worktrees get a lane, how many ports each lane reserves, and values computed per lane (`lane.<name>`). | `{"hosts_file":"always","primary":"main","vars":{"db_name":"acme_${{ lane.name }}"}}` |
| <code>lanes.<wbr>block_size</code> | integer (templated) | Number of ports reserved for each lane. By default devlanes counts the ports the config needs and adds half again, rounded up to ten. A fixed size smaller than what the config needs is an error. | `80` |
| <code>lanes.<wbr>hosts_file</code> | `auto` or `never` or `always` | Whether this project's hostnames are also written to `/etc/hosts`, for programs that cannot resolve `*.localhost` by themselves (Wrangler's workerd, musl or pure-Go binaries). Overrides the user's `hostnames.hosts_file` (default `auto`), except that a user `never` always wins. Writing needs the helper `devlanes setup` installs, and only exact names are written, never wildcards. Values: `auto`: Write the exact hostnames only when the daemon's startup check finds that the system resolver does not answer `*.localhost` with a loopback address. `never`: Never write hostnames to `/etc/hosts`. Set in the user config, it applies to every project whatever the project sets. `always`: Always write the exact hostnames, even when the system resolver handles `*.localhost`, for programs that send `*.localhost` to DNS themselves. | `auto` |
| <code>lanes.<wbr>matrix</code> | list of map of scalar (templated) | Fixed values per lane instead of computed `vars`: entry N holds lane N's values, read as `lane.<name>`. A lane whose index has no entry fails to resolve. Cannot be combined with `vars`. | `[{"domain":"acme.localhost","port":20000},{"domain":"acme1.localhost","port":20100}]` |
| <code>lanes.<wbr>max</code> | integer (templated) | Most lanes the project may hold at once; further worktrees stay unassigned, without ports. Lowering it keeps existing lanes. Unset, the number of lanes is limited only by free port blocks. | `6` |
| <code>lanes.<wbr>new_worktrees</code> | `register` or `setup` or `start` | What devlanes does when a worktree gets its lane: only reserve it, also set it up, or also start it. Defaults to `register`. A worktree gets a lane the first time devlanes runs in it, not merely because it exists. Values: `register`: Only reserve the lane (its index, port block and domain); nothing runs until the lane is started. `setup`: Also run infrastructure setup and `lane.create` tasks, leaving services stopped. `start`: Also start the lane with the default profile. | `register` |
| <code>lanes.<wbr>primary</code> | string | Branch whose worktree is lane 0, the main lane (`lanes.main` in expressions) and the only worktree that gets a lane by itself. Defaults to the remote's default branch, else `main` or `master`. In CI (a variable such as `CI` or `GITHUB_ACTIONS` is set) the checkout takes lane 0 on any branch when lane 0 is free. | `main` |
| <code>lanes.<wbr>removed_worktrees</code> | `stop` or `clean` | What happens to a lane whose worktree folder is deleted, for example by `git worktree remove` after a merge. Defaults to `stop`, which stops its processes and keeps its data, containers and ports until `devlanes lane gc --yes` or `devlanes lane clean` removes them. Set `clean` to have devlanes clean the lane by itself. Values: `stop`: Stop the lane's processes and keep the lane: its data folder, containers, index and ports stay until `devlanes lane gc --yes` or `devlanes lane clean` removes them. `clean`: Clean the lane by itself: stop its processes, remove its own containers and data folder, and free its index and ports. An idle lane is cleaned once its folder has been gone for 5 seconds, so `git worktree move` keeps it. Its logs stay until log retention removes them. `lane.clean` tasks and `infra.teardown` need the folder to run in, so they are skipped; data they would drop from shared containers stays. | `stop` |
| <code>lanes.<wbr>vars</code> | map of scalar (templated) | Values computed for each lane and read as `${{ lane.<name> }}`, such as a database name, vhost, bucket or Redis index per lane. A var named `domain` replaces the default lane domain, and one named `port` fixes where the lane's port block starts. `${{ domain('pages') }}` gives a name a site of its own in each lane, such as `pages2.localhost` in lane 2. Cannot be combined with `matrix`. | `{"db_name":"acme_${{ lane.name }}"}` |

### profiles

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `profiles` | map of list of string or object | Named selections of services for `devlanes start --profile <name>`. A plain `devlanes start` brings back what the lane ran before; `default` is used when there is nothing to bring back, such as after `devlanes stop`. A profile is a list of names, globs and `+tag` selectors, or a mapping that can also exclude services, skip infra, add `env` and remap script names. | `{"default":["+api","+web"],"e2e":{"include":["*"],"run_scripts":{"dev":"start"}}}` |
| <code>profiles.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Variables for the services this profile starts, above their own `env`. | `{"NODE_ENV":"production"}` |
| <code>profiles.<wbr><name>.<wbr>exclude</code> | list of string | Services removed from the `include` selection, by name, glob or `+tag`. | `["+desktop","sms-stub"]` |
| <code>profiles.<wbr><name>.<wbr>include</code> | list of string | Services this profile starts: names, globs such as `crm-*`, and `+tag` selectors. Globs skip services with `autostart: false`; names and tags include them. | `["+api","+web"]` |
| <code>profiles.<wbr><name>.<wbr>infra</code> | boolean (templated) | Set false to skip containers and `infra.setup`, for example to run only a frontend against another lane's backend. Services that reference `infra.*` then fail to resolve. With several profiles active, infra is skipped only when all of them set false. Defaults to true. | `false` |
| <code>profiles.<wbr><name>.<wbr>run_scripts</code> | map of string | Swap script names for services that use `run_script`: `{ dev: start }` runs the `start` script wherever a service's `run_script` names `dev`. The file stays the same, and each name is swapped once, not chained. | `{"dev":"start"}` |

### project

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `project` | string | Project name, the same in every worktree of the repository. It builds each lane's default domain (`<project>.localhost` for lane 0, `<project><N>.localhost` for lane N), names lanes in commands (`acme@2`), the containers devlanes creates and `DEVLANES_LANE_PROJECT`, and other projects reach this one as `projects.<project>`. Lowercase letters, digits, dots, underscores and dashes. | `acme` |

### services

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `services` | map of object | Long-running processes of each lane, started together after infrastructure, setup and `lane.start` tasks. Each service gets its own port as `PORT` and, with `hosts`, HTTPS hostnames. A service that exits shows as failed and is not restarted. | `{"api":{"hosts":"api.${{ lane.domain }}","run_script":{"file":"apps/api/package.json","name":"dev"}}}` |
| <code>services.<wbr><name>.<wbr>autostart</code> | boolean (templated) | Set false for an on-demand service: a plain `devlanes start` without a `default` profile skips it unless the lane ran it before, and so do globs such as `*` in profiles. Naming it or its `+tag`, on the command line or in a profile, still starts it. An expression is evaluated for each lane and acts like the `true` or `false` it gives, such as `${{ lane.index == 0 }}` to autostart only in the main lane. With `replicas`, the service starts all its copies when any copy's expression gives true. Defaults to true. | `true` |
| <code>services.<wbr><name>.<wbr>cwd</code> | string (templated) | Directory `run` starts in, relative to the project file's directory in the lane's worktree; defaults to that directory. Platform detection also looks here. Cannot be combined with `run_script`, which runs in the script file's directory; `devlanes config check` refuses the pair, including a `cwd` inherited through `extends`. | `apps/api` |
| <code>services.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Environment variables for this service's process. They override `global_env` and are overridden by the active profile's `env` and `--env`; values may be text, numbers, booleans or `${{ ... }}` expressions. Setting `PORT` replaces the port devlanes allocated; a literal number is the same in every lane, so lanes collide. | `{"API_URL":"${{ services.api.url }}"}` |
| <code>services.<wbr><name>.<wbr>extends</code> | string | Template under `templates` whose settings this service inherits; its own settings apply on top. Mappings (`env`, `ready`, `stop`, preset options, `run_script`) merge by key, lists (`tags`, `hosts`, `sync`) and single values replace. Setting `run` or `run_script` drops the inherited other one, and one `ready` probe (`tcp`, `http`, `log`, `cmd`) drops the inherited probe. Templates may extend other templates and are resolved after every project file and local override is merged. | `fastify-api` |
| <code>services.<wbr><name>.<wbr>hidden</code> | boolean (templated) | Keep this service away from machines connected with `devlanes remote connect`. They never see, mirror or reach it, and do not get its `sync` folders. It still runs on this machine. Defaults to false. | `false` |
| <code>services.<wbr><name>.<wbr>hosts</code> | string (templated) or object or list of string (templated) or object | Hostnames the devlanes HTTPS proxy routes to this service, so `https://<host>` reaches it. One entry or a list; an entry is a hostname or a mapping with `host` and optional `port` (a named port), `fallback` and `host_header`. A `*.` wildcard matches subdomains, and an exact host wins over it. The first non-wildcard host becomes `public_url`, non-wildcard hosts join the lane's `DEVLANES_ORIGINS`, and having hosts makes the default `ready` a TCP check. Keep them under `${{ lane.domain }}` so lanes do not collide. | `api.${{ lane.domain }}` |
| <code>services.<wbr><name>.<wbr>hosts.<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>services.<wbr><name>.<wbr>hosts.<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>services.<wbr><name>.<wbr>hosts.<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>services.<wbr><name>.<wbr>hosts.<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>services.<wbr><name>.<wbr>hosts[].<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>services.<wbr><name>.<wbr>hosts[].<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>services.<wbr><name>.<wbr>hosts[].<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>services.<wbr><name>.<wbr>hosts[].<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>services.<wbr><name>.<wbr>keep_awake</code> | boolean (templated) | While this service runs, devlanes never throttles or freezes its lane, whatever `lanes.throttle_after` and `lanes.freeze_after` in the user config say. Set it on workers, queue consumers, schedulers and tunnels that do work without inbound requests, which a frozen lane would otherwise stall. One such service keeps the whole lane awake. An expression is evaluated for each lane, such as `${{ lane.index == 0 }}` to keep only the main lane awake. Defaults to false. | `true` |
| <code>services.<wbr><name>.<wbr>name</code> | string | Label shown in the TUI and human output instead of the service key; any text, spaces allowed, with ` #N` added per replica. Commands, profiles and `${{ services.<key> }}` keep using the key. | `Admin API` |
| <code>services.<wbr><name>.<wbr>next</code> | object | Options for `preset: next`, which adds `--port <self.port> --hostname 127.0.0.1` to the `next dev` command. Without the hostname flag Next listens on `0.0.0.0` instead of loopback. | `{"args":["--turbopack"]}` |
| <code>services.<wbr><name>.<wbr>next.<wbr>args</code> | list of string | Extra arguments for `next dev`, added after the preset's port and hostname flags, for example `[--turbopack]`. A local override or `extends` child that sets `args` replaces the inherited list. Default: none. | `["--turbopack"]` |
| <code>services.<wbr><name>.<wbr>platform</code> | string | Runtime of the service: `node`, `bun`, `deno`, `python` or `rust`. It decides which variables devlanes adds, such as the devlanes CA (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`) and color settings. Detected from the `run_script` file or the working directory when omitted; set it only when detection is wrong. `none` skips detection and adds no platform variables. | `node` |
| <code>services.<wbr><name>.<wbr>preset</code> | `vite` or `next` or `wrangler` or `tauri` | Framework integration that adds the flags and variables a dev server needs to run in a lane, such as `--port <port> --strictPort --host 127.0.0.1` for `vite`, so you do not pass them yourself. Requires `run_script`; with a hand-written `run`, drop the preset and pass its flags yourself. Its options go under a sibling key with the preset's name, for example `vite: { args: [--open] }`. | `vite` |
| <code>services.<wbr><name>.<wbr>pty</code> | boolean (templated) | Run the service in a pseudoterminal, so it sees a terminal as it would in your shell and you can type into it from the TUI. Set false to use pipes instead: stdin is empty and stdout and stderr are read separately. Defaults to true. | `true` |
| <code>services.<wbr><name>.<wbr>ready</code> | `started` or object | When the service counts as up: `devlanes start` waits for it, and `after: services` tasks run once every started service is ready. Write `started` (ready once the process runs) or a mapping with one of `tcp`, `http`, `log` or `cmd`, plus `timeout` (default 60s). Without it, a service with `hosts` waits for TCP on its port and one without hosts uses `started`. A check that times out marks the service failed, which also skips `after: services` tasks; the process keeps running and is not restarted. Values: `started`: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe. | `started` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>cmd</code> | string (templated) or list of string (templated) | Ready once this command exits 0. It runs on the host through `/bin/sh` (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service's environment and working directory. | `curl -fsS ${{ self.url }}/healthz` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>http</code> | object | Ready once a GET request to `path` on the unit's main port answers with `status`, or with a 2xx status when `status` is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends `User-Agent: devlanes-ready`, so an app can leave the checks out of its request logs. | `{"path":"/healthz","status":200,"timeout":"60s"}` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>http.<wbr>host</code> | string (templated) | Host header the check sends, for apps that answer only on their own hostname, such as `${{ self.hosts[0] }}`. The request still goes to 127.0.0.1; only the header changes. Defaults to `127.0.0.1:<port>`. Spaces and control characters are refused. | `${{ self.hosts[0] }}` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>http.<wbr>path</code> | string (templated) | Path requested with GET, such as `/healthz`; defaults to `/`. | `/healthz` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>http.<wbr>status</code> | integer (templated) | Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as `302` for a route that redirects to a login. | `200` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>http.<wbr>timeout</code> | duration (templated) | How long the HTTP check may take to pass, replacing the check's `timeout`; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails. | `15s` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>log</code> | string (templated) | Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected. | `Connected to Redis` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>tcp</code> | boolean or string | Ready once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. `true` uses the unit's main port; a port name such as `inspector` uses that named port. It only proves the socket is open, not that the app works. | `true` |
| <code>services.<wbr><name>.<wbr>ready.<wbr>timeout</code> | duration (templated) | How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and `after: services` tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen. | `15s` |
| <code>services.<wbr><name>.<wbr>replicas</code> | integer | Number of copies of this service to run, each with its own port, for example several queue workers. They run as `<name>.1` to `<name>.N`, `${{ replica.index }}` counts from 1, and `services.<name>` in expressions means the first copy; use `${{ replica.index }}` in `hosts` to give each copy its own hostname. A plain number of at least 1, the same in every lane, because each copy reserves ports in every lane's block; `${{ ... }}` expressions are not allowed. Defaults to 1. | `1` |
| <code>services.<wbr><name>.<wbr>run</code> | string (templated) or list of string (templated) | Command that starts the service. A string runs with `/bin/sh -c`, so pipes, `&&` and multi-line scripts work; a list is an argv run directly without a shell. It starts in the lane's worktree root unless `cwd` is set. devlanes runs it exactly as written, so it cannot be combined with `preset`; prefer `run_script` when the command is a script in a project file. Exactly one of `run` and `run_script` is required after templates are applied. | `pnpm dev` |
| <code>services.<wbr><name>.<wbr>run_script</code> | object | Script to run from a project file, as `{ file, name }`, such as `{ file: apps/api/package.json, name: dev }`. devlanes picks the tool from the file name (`package.json`, `Makefile`, `justfile`, `deno.json`, `pyproject.toml`, `Cargo.toml`), runs the script in that file's directory and checks that it exists before starting. Presets need it to place their flags, and a profile's `run_scripts` can swap the script name. Exactly one of `run` and `run_script` is required after templates are applied. | `{"file":"apps/api/package.json","name":"dev"}` |
| <code>services.<wbr><name>.<wbr>run_script.<wbr>file</code> | string | File that defines the script, relative to the project file's directory, such as `apps/api/package.json`. The script runs in this file's directory. Required unless an override or `extends` child keeps the inherited file. | `apps/api/package.json` |
| <code>services.<wbr><name>.<wbr>run_script.<wbr>name</code> | string | Script, target, recipe, task or binary name in that file. Required unless an override or `extends` child keeps the inherited name. | `dev` |
| <code>services.<wbr><name>.<wbr>stop</code> | object | How devlanes stops the service: `signal` goes to its whole process tree, and after `timeout` every process still running is SIGKILLed. The service then shows as stopped and is not restarted. Defaults to SIGTERM and 10s. | `{"signal":"SIGINT","timeout":"15s"}` |
| <code>services.<wbr><name>.<wbr>stop.<wbr>signal</code> | string | Signal sent to the service's whole process tree when devlanes stops or restarts it. One of `SIGTERM` (default), `SIGINT`, `SIGHUP`, `SIGQUIT`, `SIGUSR1`, `SIGUSR2` or `SIGKILL`; use `SIGINT` for programs that only shut down cleanly on Ctrl-C. | `SIGTERM` |
| <code>services.<wbr><name>.<wbr>stop.<wbr>timeout</code> | duration (templated) | How long to wait after `signal` before devlanes SIGKILLs every process of the service that is still running. The service then shows as stopped; devlanes never restarts it. Defaults to 10s; raise it for programs that need time to flush or close connections. | `15s` |
| <code>services.<wbr><name>.<wbr>sync</code> | list of object | Folders mirrored while the service runs, such as build output a browser loads unpacked from a fixed path. Each entry copies `from` to `to` (default `~/devlanes/sync/<project>/<lane>/<folder>`) and follows changes; machines connected with `devlanes remote connect` get their own copy. | `[{"from":"apps/extension/.output/chrome-mv3","ignore":["*.map"]}]` |
| <code>services.<wbr><name>.<wbr>sync[].<wbr>from</code> | string (templated) | Folder to copy, relative to the project file's directory in the lane's worktree. | `apps/extension/.output/chrome-mv3` |
| <code>services.<wbr><name>.<wbr>sync[].<wbr>ignore</code> | list of string | Glob patterns of files left out of the copy and of later updates. | `["*.map"]` |
| <code>services.<wbr><name>.<wbr>sync[].<wbr>to</code> | string (templated) | Local destination; `~/` is your home and other relative paths start at the project file's directory. Defaults to `~/devlanes/sync/<project>/<lane>/<folder>`. Machines connected with `devlanes remote connect` ignore it and use their own path. | `~/devlanes/sync/acme/${{ lane.name }}/chrome-mv3` |
| <code>services.<wbr><name>.<wbr>tags</code> | list of string | Labels for selecting and grouping services. `+tag` selects every service with the tag in profiles, in commands such as `devlanes start +api`, and in the TUI filter. Tags also set the service's color: the first tag with a `color` under the top-level `tags` wins, then the first tag naming a category such as `api`, `web` or `worker`, so put that tag first. | `["api","essential"]` |
| <code>services.<wbr><name>.<wbr>tauri</code> | object | Options for `preset: tauri`, which lets several Tauri v2 dev apps run side by side, one per lane and replica. The values go into a config overlay at `${{ lane.data_path }}/tauri/<service>.<replica>.conf.json`, passed with `--config` and in `DEVLANES_TAURI_CONFIG`, which Tauri merges over `tauri.conf.json`. | `{"before_dev_command":"","dev_url":"${{ services.desktop_web.url }}","identifier":"io.acme.app.lane${{ lane.index }}.client${{ replica.index }}"}` |
| <code>services.<wbr><name>.<wbr>tauri.<wbr>before_dev_command</code> | string | Replaces `build.beforeDevCommand`, the command Tauri runs before the app. Set it to `""` when the frontend runs as its own devlanes service; otherwise every replica starts its own frontend on the port from `tauri.conf.json`. Default: the command in `tauri.conf.json`. | `""` |
| <code>services.<wbr><name>.<wbr>tauri.<wbr>dev_url</code> | string | Frontend address the app loads, written to `build.devUrl`. Usually another service's `url`, such as `${{ services.desktop_web.url }}`, so each lane's app loads that lane's frontend. Must start with `http://` or `https://`; when unset, `tauri.conf.json` decides. | `${{ services.desktop_web.url }}` |
| <code>services.<wbr><name>.<wbr>tauri.<wbr>identifier</code> | string | Bundle identifier for this lane and replica, which keys the app data, webview data and keychain entries. Required and must differ per lane and replica, for example `com.acme.app.dev.lane${{ lane.index }}.client${{ replica.index }}`; two instances with the same value are refused at start. Only letters, digits, `-` and `.` are allowed, with no empty part between dots. | `io.acme.app.lane${{ lane.index }}.client${{ replica.index }}` |
| <code>services.<wbr><name>.<wbr>tauri.<wbr>product_name</code> | string | Replaces the app's `productName` from `tauri.conf.json` for this instance, for example to tell lanes apart. Default: the value in `tauri.conf.json`. | `Acme Dev` |
| <code>services.<wbr><name>.<wbr>vite</code> | object | Options for `preset: vite`, which adds `--port <self.port> --strictPort --host 127.0.0.1` to the dev server command. `--strictPort` makes Vite exit when the port is taken instead of moving to another port the proxy does not know about. | `{"args":["--open"]}` |
| <code>services.<wbr><name>.<wbr>vite.<wbr>args</code> | list of string | Extra arguments for the Vite command, added after the preset's port and host flags, for example `[--mode, staging]`. A local override or `extends` child that sets `args` replaces the inherited list. Default: none. | `["--open"]` |
| <code>services.<wbr><name>.<wbr>wrangler</code> | object | Options for `preset: wrangler`, which gives each lane's `wrangler dev` its own `--port <self.port>`, `--inspector-port <self.ports.inspector>` and dev registry. The `inspector` port is allocated automatically. `args` that pass either port flag are refused by `devlanes config check`, and a `run_script` script that does is refused at start. | `{"args":["--env-file",".dev.vars"],"isolate_registry":true,"vars":{"PUBLIC_BASE_URL":"${{ self.public_url }}"}}` |
| <code>services.<wbr><name>.<wbr>wrangler.<wbr>args</code> | list of string | Extra arguments for `wrangler dev`, added after the port and `--var` flags, for example `[--env-file, .dev.vars]`. `--port` and `--inspector-port` (or `--inspectorPort`) are refused here, since the preset already sets them per lane. A local override or `extends` child that sets `args` replaces the inherited list. | `["--env-file",".dev.vars"]` |
| <code>services.<wbr><name>.<wbr>wrangler.<wbr>isolate_registry</code> | boolean | Points `WRANGLER_REGISTRY_PATH`, `CLOUDFLARE_DEV_REGISTRY_PATH` and `MINIFLARE_REGISTRY_PATH` at `wrangler-registry` in the lane's data folder (`${{ lane.data_path }}`), so service bindings resolve to workers of the same lane instead of same-named workers in another lane, also when two lanes run from one folder. Set `false` to drop these variables for this service, which then uses Wrangler's shared default registry. Default: `true`. | `true` |
| <code>services.<wbr><name>.<wbr>wrangler.<wbr>vars</code> | map of string or number or boolean | Worker variables for this lane, each passed as `--var KEY:VALUE` so it overrides the same name in the Wrangler config's `vars`. Values can be strings, numbers or booleans (sent as text) and may use expressions such as `${{ self.public_url }}`; names must be non-empty and cannot contain `:`. Local overrides and `extends` children merge with inherited vars per name. | `{"PUBLIC_BASE_URL":"${{ self.public_url }}"}` |

### tags

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `tags` | map of object | Optional color and description per tag. Services list their tags in their own `tags`, and tags group them: `+api` selects every service tagged `api` in profiles and in commands such as `devlanes start +api`. A unit's color comes from the first of its tags with a `color` set here, else the first tag naming a category such as api, web, worker or db, else its preset or name. Tags not listed here work too. | `{"api":{"color":"cyan","description":"HTTP APIs"}}` |
| <code>tags.<wbr><name>.<wbr>color</code> | `red` or `green` or `yellow` or `blue` or `magenta` or `cyan` or `white` or `gray` or `bright-red` or `bright-green` or `bright-yellow` or `bright-blue` or `bright-magenta` or `bright-cyan` | Color of services with this tag in the TUI and CLI output. When a service has several tags with a color, the first one in its `tags` list wins. Without one, devlanes infers a color from category words such as `api` (blue), `web` (green) or `worker` (magenta) in the service's tags, preset or name. Values: `red`: Show the tag's services in the terminal's red color. `green`: Show the tag's services in the terminal's green color. `yellow`: Show the tag's services in the terminal's yellow color. `blue`: Show the tag's services in the terminal's blue color. `magenta`: Show the tag's services in the terminal's magenta color. `cyan`: Show the tag's services in the terminal's cyan color. `white`: Show the tag's services in the terminal's white color. `gray`: Show the tag's services in the terminal's gray color. `bright-red`: Show the tag's services in the terminal's bright-red color. `bright-green`: Show the tag's services in the terminal's bright-green color. `bright-yellow`: Show the tag's services in the terminal's bright-yellow color. `bright-blue`: Show the tag's services in the terminal's bright-blue color. `bright-magenta`: Show the tag's services in the terminal's bright-magenta color. `bright-cyan`: Show the tag's services in the terminal's bright-cyan color. | `red` |
| <code>tags.<wbr><name>.<wbr>description</code> | string | Note on what the tag groups, for people reading the config. | `HTTP APIs` |

### tasks

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `tasks` | map of object | One-shot commands such as installs, migrations and seeds, run by hand with `devlanes run <name>` or automatically at a lane event set with `when`. Tasks of the same event run in declaration order. | `{"migrate":{"run_script":{"file":"apps/api/package.json","name":"migrate"},"when":"lane.start"}}` |
| <code>tasks.<wbr><name>.<wbr>after</code> | `services` | Set to `services` to run a `lane.create` or `lane.start` task after the started services are ready instead of before them, for work that goes through the running app, such as seeding through its API. Values: `services`: Run once every started service is ready; the task is skipped when one of them failed or the start timed out. | `services` |
| <code>tasks.<wbr><name>.<wbr>confirm</code> | string (templated) | Question asked before the task runs, for destructive tasks. Scripts and agents without a terminal must pass `--yes` to run it. | `Reset ${{ lane.db_name }} and re-seed it?` |
| <code>tasks.<wbr><name>.<wbr>cwd</code> | string (templated) | Working directory for `run`, relative to the project file's directory in the lane's worktree; defaults to that directory. Cannot be combined with `run_script`, which runs in the script file's directory. | `apps/api` |
| <code>tasks.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Variables for this task only, above `global_env`. | `{"API_URL":"${{ services.api.url }}"}` |
| <code>tasks.<wbr><name>.<wbr>interactive</code> | boolean (templated) | Give the task your terminal for input and output, for logins, prompts and TUIs. Interactive tasks only run by hand, so they cannot have `when`. Defaults to false. | `true` |
| <code>tasks.<wbr><name>.<wbr>profiles</code> | list of string | Run this task's `when` hook only while one of these profiles is active; `default` also matches a start without `--profile`. Runs by hand are not affected. Requires `when`. | `["default","backend"]` |
| <code>tasks.<wbr><name>.<wbr>pty</code> | boolean (templated) | Run the task in a pseudoterminal instead of plain pipes, for tools that only print colors or progress to a terminal. Defaults to the value of `interactive`. | `true` |
| <code>tasks.<wbr><name>.<wbr>run</code> | string (templated) or list of string (templated) | Command to run: a string runs through `/bin/sh -c` (pipes and multi-line scripts work), a list runs the program directly with those arguments. Use exactly one of `run`, `run_script` or `steps`. | `pnpm dev` |
| <code>tasks.<wbr><name>.<wbr>run_script</code> | object | Script to run from a project file, as `{ file, name }`, such as `{ file: apps/api/package.json, name: migrate }`. It runs with that file's tool (the project's package manager for package.json) in the file's directory, and a missing script fails before anything starts. Prefer it to `run` for scripts that live in a file. Use exactly one of `run`, `run_script` or `steps`. | `{"file":"apps/api/package.json","name":"dev"}` |
| <code>tasks.<wbr><name>.<wbr>run_script.<wbr>file</code> | string | File that defines the script, relative to the project file's directory, such as `apps/api/package.json`. The script runs in this file's directory. Required unless an override or `extends` child keeps the inherited file. | `apps/api/package.json` |
| <code>tasks.<wbr><name>.<wbr>run_script.<wbr>name</code> | string | Script, target, recipe, task or binary name in that file. Required unless an override or `extends` child keeps the inherited name. | `dev` |
| <code>tasks.<wbr><name>.<wbr>steps</code> | list of string | Run these tasks and `infra.setup.<name>` or `infra.teardown.<name>` entries in order, stopping at the first failure, for jobs such as resetting a database. Each step runs whatever its own `when` says, and setup entries run again even when already done. Use instead of `run` or `run_script`. | `["infra.teardown.database","infra.setup.database","migrate","seed"]` |
| <code>tasks.<wbr><name>.<wbr>timeout</code> | duration (templated) | Longest the task may run. When it runs out, the task is stopped (SIGTERM, then SIGKILL after 10 seconds) and counts as failed; a failed task that runs before services fails the start. No limit by default. | `15s` |
| <code>tasks.<wbr><name>.<wbr>when</code> | `lane.create` or `lane.start` or `lane.stop` or `lane.clean` | Lane event that runs this task automatically. Without it the task runs only by hand with `devlanes run <name>` or as a step of another task. Values: `lane.create`: Run on each start until it has succeeded once for the lane, then no longer automatically; for installs, code generation and a first seed. `lane.start`: Run on every start of the lane, before services unless `after: services` is set; for migrations and other steps that are safe to repeat. `lane.stop`: Run before the lane's processes stop when the whole lane stops: `devlanes stop` without unit names, `devlanes daemon stop` and `daemon restart`. Skipped when a signal (logout, system shutdown) stops the daemon. `lane.clean`: Run when the lane is cleaned (`devlanes lane clean`), after its processes stop and before `infra.teardown`. | `lane.create` |

### templates

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `templates` | map of object | Named service settings that services reuse with `extends: <template>`. The service inherits every setting of the template and its own settings win: mappings (`env`, `ready`, `stop`, preset options, `run_script`) merge by key, lists (`tags`, `hosts`, `sync`) and single values replace. Setting `run` or `run_script` drops the inherited other one, and one `ready` probe (`tcp`, `http`, `log`, `cmd`) drops the inherited probe. Templates may extend other templates, are never started themselves, and are applied after every project file and local override is merged. | `{"fastify-api":{"platform":"node","ready":{"http":{"path":"/-/health"}},"tags":["api"]}}` |
| <code>templates.<wbr><name>.<wbr>autostart</code> | boolean (templated) | Set false for an on-demand service: a plain `devlanes start` without a `default` profile skips it unless the lane ran it before, and so do globs such as `*` in profiles. Naming it or its `+tag`, on the command line or in a profile, still starts it. An expression is evaluated for each lane and acts like the `true` or `false` it gives, such as `${{ lane.index == 0 }}` to autostart only in the main lane. With `replicas`, the service starts all its copies when any copy's expression gives true. Defaults to true. | `true` |
| <code>templates.<wbr><name>.<wbr>cwd</code> | string (templated) | Directory `run` starts in, relative to the project file's directory in the lane's worktree; defaults to that directory. Platform detection also looks here. Cannot be combined with `run_script`, which runs in the script file's directory; `devlanes config check` refuses the pair, including a `cwd` inherited through `extends`. | `apps/api` |
| <code>templates.<wbr><name>.<wbr>env</code> | map of scalar (templated) | Environment variables for this service's process. They override `global_env` and are overridden by the active profile's `env` and `--env`; values may be text, numbers, booleans or `${{ ... }}` expressions. Setting `PORT` replaces the port devlanes allocated; a literal number is the same in every lane, so lanes collide. | `{"API_URL":"${{ services.api.url }}"}` |
| <code>templates.<wbr><name>.<wbr>extends</code> | string | Template under `templates` whose settings this service inherits; its own settings apply on top. Mappings (`env`, `ready`, `stop`, preset options, `run_script`) merge by key, lists (`tags`, `hosts`, `sync`) and single values replace. Setting `run` or `run_script` drops the inherited other one, and one `ready` probe (`tcp`, `http`, `log`, `cmd`) drops the inherited probe. Templates may extend other templates and are resolved after every project file and local override is merged. | `fastify-api` |
| <code>templates.<wbr><name>.<wbr>hidden</code> | boolean (templated) | Keep this service away from machines connected with `devlanes remote connect`. They never see, mirror or reach it, and do not get its `sync` folders. It still runs on this machine. Defaults to false. | `false` |
| <code>templates.<wbr><name>.<wbr>hosts</code> | string (templated) or object or list of string (templated) or object | Hostnames the devlanes HTTPS proxy routes to this service, so `https://<host>` reaches it. One entry or a list; an entry is a hostname or a mapping with `host` and optional `port` (a named port), `fallback` and `host_header`. A `*.` wildcard matches subdomains, and an exact host wins over it. The first non-wildcard host becomes `public_url`, non-wildcard hosts join the lane's `DEVLANES_ORIGINS`, and having hosts makes the default `ready` a TCP check. Keep them under `${{ lane.domain }}` so lanes do not collide. | `api.${{ lane.domain }}` |
| <code>templates.<wbr><name>.<wbr>hosts.<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>templates.<wbr><name>.<wbr>hosts.<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>templates.<wbr><name>.<wbr>hosts.<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>templates.<wbr><name>.<wbr>hosts.<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>templates.<wbr><name>.<wbr>hosts[].<wbr>fallback</code> | boolean (templated) | Make this route a catch-all: it is used only when no other route, exact or wildcard, matches the requested hostname. Typical for `*.${{ lane.domain }}` on a gateway that should answer every subdomain other services do not claim. Defaults to false. | `true` |
| <code>templates.<wbr><name>.<wbr>hosts[].<wbr>host</code> | string (templated) | Hostname to route, evaluated for each lane. A leading `*.` makes a wildcard that matches any depth below it but not the name itself; quote it in YAML. Labels hold letters, digits and inner hyphens; case and a trailing dot are ignored. | `api.${{ lane.domain }}` |
| <code>templates.<wbr><name>.<wbr>hosts[].<wbr>host_header</code> | `preserve` or `upstream` | Host header the unit receives. Defaults to `preserve`, the hostname the browser asked for; `upstream` sends the local address, such as `127.0.0.1:<port>`, for servers that reject or misroute requests for a Host that is not their own address. Values: `preserve`: Send the hostname the client requested, such as `api.acme2.localhost`. `upstream`: Send the local address and port the proxy connects to, such as `127.0.0.1:<port>`. | `preserve` |
| <code>templates.<wbr><name>.<wbr>hosts[].<wbr>port</code> | string | Send this hostname to one of the unit's named ports instead of its main port. For a service it is a port name as in `self.ports.<name>`, and naming it here allocates that port; in `infra.hosts` it is the container port for Compose services, or the port's name for plain containers. | `inspector` |
| <code>templates.<wbr><name>.<wbr>keep_awake</code> | boolean (templated) | While this service runs, devlanes never throttles or freezes its lane, whatever `lanes.throttle_after` and `lanes.freeze_after` in the user config say. Set it on workers, queue consumers, schedulers and tunnels that do work without inbound requests, which a frozen lane would otherwise stall. One such service keeps the whole lane awake. An expression is evaluated for each lane, such as `${{ lane.index == 0 }}` to keep only the main lane awake. Defaults to false. | `true` |
| <code>templates.<wbr><name>.<wbr>name</code> | string | Label shown in the TUI and human output instead of the service key; any text, spaces allowed, with ` #N` added per replica. Commands, profiles and `${{ services.<key> }}` keep using the key. | `Admin API` |
| <code>templates.<wbr><name>.<wbr>next</code> | object | Options for `preset: next`, which adds `--port <self.port> --hostname 127.0.0.1` to the `next dev` command. Without the hostname flag Next listens on `0.0.0.0` instead of loopback. | `{"args":["--turbopack"]}` |
| <code>templates.<wbr><name>.<wbr>next.<wbr>args</code> | list of string | Extra arguments for `next dev`, added after the preset's port and hostname flags, for example `[--turbopack]`. A local override or `extends` child that sets `args` replaces the inherited list. Default: none. | `["--turbopack"]` |
| <code>templates.<wbr><name>.<wbr>platform</code> | string | Runtime of the service: `node`, `bun`, `deno`, `python` or `rust`. It decides which variables devlanes adds, such as the devlanes CA (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`) and color settings. Detected from the `run_script` file or the working directory when omitted; set it only when detection is wrong. `none` skips detection and adds no platform variables. | `node` |
| <code>templates.<wbr><name>.<wbr>preset</code> | `vite` or `next` or `wrangler` or `tauri` | Framework integration that adds the flags and variables a dev server needs to run in a lane, such as `--port <port> --strictPort --host 127.0.0.1` for `vite`, so you do not pass them yourself. Requires `run_script`; with a hand-written `run`, drop the preset and pass its flags yourself. Its options go under a sibling key with the preset's name, for example `vite: { args: [--open] }`. | `vite` |
| <code>templates.<wbr><name>.<wbr>pty</code> | boolean (templated) | Run the service in a pseudoterminal, so it sees a terminal as it would in your shell and you can type into it from the TUI. Set false to use pipes instead: stdin is empty and stdout and stderr are read separately. Defaults to true. | `true` |
| <code>templates.<wbr><name>.<wbr>ready</code> | `started` or object | When the service counts as up: `devlanes start` waits for it, and `after: services` tasks run once every started service is ready. Write `started` (ready once the process runs) or a mapping with one of `tcp`, `http`, `log` or `cmd`, plus `timeout` (default 60s). Without it, a service with `hosts` waits for TCP on its port and one without hosts uses `started`. A check that times out marks the service failed, which also skips `after: services` tasks; the process keeps running and is not restarted. Values: `started`: Count the unit ready as soon as it runs, without probing anything. A dev server whose program crashed under a file watcher still counts as ready, so prefer a check when there is something to probe. | `started` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>cmd</code> | string (templated) or list of string (templated) | Ready once this command exits 0. It runs on the host through `/bin/sh` (a list is quoted into one command line) with its output discarded, is retried until the timeout, and each attempt is cut off after 10 seconds. For a service it gets the service's environment and working directory. | `curl -fsS ${{ self.url }}/healthz` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>http</code> | object | Ready once a GET request to `path` on the unit's main port answers with `status`, or with a 2xx status when `status` is unset. Redirects are not followed. Best when the app has a health route that fails while it is broken. Each request sends `User-Agent: devlanes-ready`, so an app can leave the checks out of its request logs. | `{"path":"/healthz","status":200,"timeout":"60s"}` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>http.<wbr>host</code> | string (templated) | Host header the check sends, for apps that answer only on their own hostname, such as `${{ self.hosts[0] }}`. The request still goes to 127.0.0.1; only the header changes. Defaults to `127.0.0.1:<port>`. Spaces and control characters are refused. | `${{ self.hosts[0] }}` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>http.<wbr>path</code> | string (templated) | Path requested with GET, such as `/healthz`; defaults to `/`. | `/healthz` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>http.<wbr>status</code> | integer (templated) | Status code that counts as ready, between 100 and 599; only that exact code passes. When unset, any status from 200 to 299 counts, so a 404 or a redirect keeps waiting (redirects are not followed). Set it to accept another code, such as `302` for a route that redirects to a login. | `200` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>http.<wbr>timeout</code> | duration (templated) | How long the HTTP check may take to pass, replacing the check's `timeout`; defaults to 60s. When it runs out, a service is marked failed but keeps running, and for an infra container the start fails. | `15s` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>log</code> | string (templated) | Ready once a line of output matches this regular expression, with colors and other terminal escape codes removed; recent lines printed before the check began count too. Suits workers without a port: have them print a line once connected. | `Connected to Redis` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>tcp</code> | boolean or string | Ready once a port accepts a TCP connection on 127.0.0.1 or ::1, tried every 150 ms. `true` uses the unit's main port; a port name such as `inspector` uses that named port. It only proves the socket is open, not that the app works. | `true` |
| <code>templates.<wbr><name>.<wbr>ready.<wbr>timeout</code> | duration (templated) | How long the check may take to pass; defaults to 60s. When it runs out, a service is marked failed (its process keeps running and is not restarted) and `after: services` tasks are skipped; for an infra container the start fails. Raise it for services that compile before they listen. | `15s` |
| <code>templates.<wbr><name>.<wbr>replicas</code> | integer | Number of copies of this service to run, each with its own port, for example several queue workers. They run as `<name>.1` to `<name>.N`, `${{ replica.index }}` counts from 1, and `services.<name>` in expressions means the first copy; use `${{ replica.index }}` in `hosts` to give each copy its own hostname. A plain number of at least 1, the same in every lane, because each copy reserves ports in every lane's block; `${{ ... }}` expressions are not allowed. Defaults to 1. | `1` |
| <code>templates.<wbr><name>.<wbr>run</code> | string (templated) or list of string (templated) | Command that starts the service. A string runs with `/bin/sh -c`, so pipes, `&&` and multi-line scripts work; a list is an argv run directly without a shell. It starts in the lane's worktree root unless `cwd` is set. devlanes runs it exactly as written, so it cannot be combined with `preset`; prefer `run_script` when the command is a script in a project file. Exactly one of `run` and `run_script` is required after templates are applied. | `pnpm dev` |
| <code>templates.<wbr><name>.<wbr>run_script</code> | object | Script to run from a project file, as `{ file, name }`, such as `{ file: apps/api/package.json, name: dev }`. devlanes picks the tool from the file name (`package.json`, `Makefile`, `justfile`, `deno.json`, `pyproject.toml`, `Cargo.toml`), runs the script in that file's directory and checks that it exists before starting. Presets need it to place their flags, and a profile's `run_scripts` can swap the script name. Exactly one of `run` and `run_script` is required after templates are applied. | `{"file":"apps/api/package.json","name":"dev"}` |
| <code>templates.<wbr><name>.<wbr>run_script.<wbr>file</code> | string | File that defines the script, relative to the project file's directory, such as `apps/api/package.json`. The script runs in this file's directory. Required unless an override or `extends` child keeps the inherited file. | `apps/api/package.json` |
| <code>templates.<wbr><name>.<wbr>run_script.<wbr>name</code> | string | Script, target, recipe, task or binary name in that file. Required unless an override or `extends` child keeps the inherited name. | `dev` |
| <code>templates.<wbr><name>.<wbr>stop</code> | object | How devlanes stops the service: `signal` goes to its whole process tree, and after `timeout` every process still running is SIGKILLed. The service then shows as stopped and is not restarted. Defaults to SIGTERM and 10s. | `{"signal":"SIGINT","timeout":"15s"}` |
| <code>templates.<wbr><name>.<wbr>stop.<wbr>signal</code> | string | Signal sent to the service's whole process tree when devlanes stops or restarts it. One of `SIGTERM` (default), `SIGINT`, `SIGHUP`, `SIGQUIT`, `SIGUSR1`, `SIGUSR2` or `SIGKILL`; use `SIGINT` for programs that only shut down cleanly on Ctrl-C. | `SIGTERM` |
| <code>templates.<wbr><name>.<wbr>stop.<wbr>timeout</code> | duration (templated) | How long to wait after `signal` before devlanes SIGKILLs every process of the service that is still running. The service then shows as stopped; devlanes never restarts it. Defaults to 10s; raise it for programs that need time to flush or close connections. | `15s` |
| <code>templates.<wbr><name>.<wbr>sync</code> | list of object | Folders mirrored while the service runs, such as build output a browser loads unpacked from a fixed path. Each entry copies `from` to `to` (default `~/devlanes/sync/<project>/<lane>/<folder>`) and follows changes; machines connected with `devlanes remote connect` get their own copy. | `[{"from":"apps/extension/.output/chrome-mv3","ignore":["*.map"]}]` |
| <code>templates.<wbr><name>.<wbr>sync[].<wbr>from</code> | string (templated) | Folder to copy, relative to the project file's directory in the lane's worktree. | `apps/extension/.output/chrome-mv3` |
| <code>templates.<wbr><name>.<wbr>sync[].<wbr>ignore</code> | list of string | Glob patterns of files left out of the copy and of later updates. | `["*.map"]` |
| <code>templates.<wbr><name>.<wbr>sync[].<wbr>to</code> | string (templated) | Local destination; `~/` is your home and other relative paths start at the project file's directory. Defaults to `~/devlanes/sync/<project>/<lane>/<folder>`. Machines connected with `devlanes remote connect` ignore it and use their own path. | `~/devlanes/sync/acme/${{ lane.name }}/chrome-mv3` |
| <code>templates.<wbr><name>.<wbr>tags</code> | list of string | Labels for selecting and grouping services. `+tag` selects every service with the tag in profiles, in commands such as `devlanes start +api`, and in the TUI filter. Tags also set the service's color: the first tag with a `color` under the top-level `tags` wins, then the first tag naming a category such as `api`, `web` or `worker`, so put that tag first. | `["api","essential"]` |
| <code>templates.<wbr><name>.<wbr>tauri</code> | object | Options for `preset: tauri`, which lets several Tauri v2 dev apps run side by side, one per lane and replica. The values go into a config overlay at `${{ lane.data_path }}/tauri/<service>.<replica>.conf.json`, passed with `--config` and in `DEVLANES_TAURI_CONFIG`, which Tauri merges over `tauri.conf.json`. | `{"before_dev_command":"","dev_url":"${{ services.desktop_web.url }}","identifier":"io.acme.app.lane${{ lane.index }}.client${{ replica.index }}"}` |
| <code>templates.<wbr><name>.<wbr>tauri.<wbr>before_dev_command</code> | string | Replaces `build.beforeDevCommand`, the command Tauri runs before the app. Set it to `""` when the frontend runs as its own devlanes service; otherwise every replica starts its own frontend on the port from `tauri.conf.json`. Default: the command in `tauri.conf.json`. | `""` |
| <code>templates.<wbr><name>.<wbr>tauri.<wbr>dev_url</code> | string | Frontend address the app loads, written to `build.devUrl`. Usually another service's `url`, such as `${{ services.desktop_web.url }}`, so each lane's app loads that lane's frontend. Must start with `http://` or `https://`; when unset, `tauri.conf.json` decides. | `${{ services.desktop_web.url }}` |
| <code>templates.<wbr><name>.<wbr>tauri.<wbr>identifier</code> | string | Bundle identifier for this lane and replica, which keys the app data, webview data and keychain entries. Required and must differ per lane and replica, for example `com.acme.app.dev.lane${{ lane.index }}.client${{ replica.index }}`; two instances with the same value are refused at start. Only letters, digits, `-` and `.` are allowed, with no empty part between dots. | `io.acme.app.lane${{ lane.index }}.client${{ replica.index }}` |
| <code>templates.<wbr><name>.<wbr>tauri.<wbr>product_name</code> | string | Replaces the app's `productName` from `tauri.conf.json` for this instance, for example to tell lanes apart. Default: the value in `tauri.conf.json`. | `Acme Dev` |
| <code>templates.<wbr><name>.<wbr>vite</code> | object | Options for `preset: vite`, which adds `--port <self.port> --strictPort --host 127.0.0.1` to the dev server command. `--strictPort` makes Vite exit when the port is taken instead of moving to another port the proxy does not know about. | `{"args":["--open"]}` |
| <code>templates.<wbr><name>.<wbr>vite.<wbr>args</code> | list of string | Extra arguments for the Vite command, added after the preset's port and host flags, for example `[--mode, staging]`. A local override or `extends` child that sets `args` replaces the inherited list. Default: none. | `["--open"]` |
| <code>templates.<wbr><name>.<wbr>wrangler</code> | object | Options for `preset: wrangler`, which gives each lane's `wrangler dev` its own `--port <self.port>`, `--inspector-port <self.ports.inspector>` and dev registry. The `inspector` port is allocated automatically. `args` that pass either port flag are refused by `devlanes config check`, and a `run_script` script that does is refused at start. | `{"args":["--env-file",".dev.vars"],"isolate_registry":true,"vars":{"PUBLIC_BASE_URL":"${{ self.public_url }}"}}` |
| <code>templates.<wbr><name>.<wbr>wrangler.<wbr>args</code> | list of string | Extra arguments for `wrangler dev`, added after the port and `--var` flags, for example `[--env-file, .dev.vars]`. `--port` and `--inspector-port` (or `--inspectorPort`) are refused here, since the preset already sets them per lane. A local override or `extends` child that sets `args` replaces the inherited list. | `["--env-file",".dev.vars"]` |
| <code>templates.<wbr><name>.<wbr>wrangler.<wbr>isolate_registry</code> | boolean | Points `WRANGLER_REGISTRY_PATH`, `CLOUDFLARE_DEV_REGISTRY_PATH` and `MINIFLARE_REGISTRY_PATH` at `wrangler-registry` in the lane's data folder (`${{ lane.data_path }}`), so service bindings resolve to workers of the same lane instead of same-named workers in another lane, also when two lanes run from one folder. Set `false` to drop these variables for this service, which then uses Wrangler's shared default registry. Default: `true`. | `true` |
| <code>templates.<wbr><name>.<wbr>wrangler.<wbr>vars</code> | map of string or number or boolean | Worker variables for this lane, each passed as `--var KEY:VALUE` so it overrides the same name in the Wrangler config's `vars`. Values can be strings, numbers or booleans (sent as text) and may use expressions such as `${{ self.public_url }}`; names must be non-empty and cannot contain `:`. Local overrides and `extends` children merge with inherited vars per name. | `{"PUBLIC_BASE_URL":"${{ self.public_url }}"}` |

## Local overrides

A project file's local overlay holds the changes one developer wants on their machine only, such as a service turned off or a server they run themselves: `devlanes.local.yaml` for `devlanes.yaml`, `devlanes.prod.local.yaml` for `devlanes.prod.yaml`. It sits next to its project file and takes the same keys, every one optional, and the same modeline, `# yaml-language-server: $schema=https://devlanes.run/v1/schema.json`. Keep it out of Git: add it to `.gitignore`. A lane reads the overlay in its own worktree, else the primary worktree's.

```yaml
# devlanes.local.yaml
services:
  web:
    autostart: false   # a plain `devlanes start` skips it unless the lane ran it before or the default profile names it
infra:
  external:
    pgadmin:
      address: 127.0.0.1:5050
      hosts: "pgadmin.${{ lane.domain }}"
```

It is merged over its project file before templates are expanded. Mappings merge by key at every level (services, tasks, templates, profiles, the top-level `tags`, `infra.containers`, `infra.external`, `infra.setup`, `infra.teardown`, every `env`, `lanes.vars`), and a key the local file leaves out keeps its value. Lists and single values replace. `run`, `run_script` and a task's `steps` drop each other, and so do the `ready` checks. A service only in the local file needs `run` or `run_script`. `project` and `extends` are refused there.

A project file builds on others with `extends`, one path or a list, relative to the file: `devlanes.prod.yaml` with `extends: devlanes.yaml` runs the same apps under another project. Each extended file is merged with its own local overlay, in list order, then the file itself and its overlay, by the same rules; later files win. An extended file may be partial and its `project` is ignored, so the extending file sets its own. Files that extend each other are an error. The ports and environment guide has the details under [local overrides](/docs/guides/ports-and-env/#local-overrides) and [extending another project file](/docs/guides/ports-and-env/#extending-another-project-file), and [several projects in one directory](/docs/guides/services/#several-projects-in-one-directory) says what two such projects share.

## User config

`config.yaml` in the devlanes home (`~/.devlanes`, or `$DEVLANES_HOME`), for settings of this machine. Schema: `https://devlanes.run/v1/user-schema.json`.

### daemon

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `daemon` | object | The daemon's lifetime: whether it stops on its own once nothing runs and no TUI is open. | `{"exit_when_idle":false}` |
| <code>daemon.<wbr>exit_when_idle</code> | boolean | Stop the daemon about 15 seconds after the last TUI, or command that shows progress, closes, once nothing runs or starts, no box or client is connected or waiting to reconnect, and no pairing code is outstanding. A daemon that nothing ever watched this way, such as one only scripts or agents used, waits about 5 minutes instead. The next devlanes command starts it again. Containers keep running either way, but their hostnames answer only while the daemon runs. Defaults to true. Only a daemon a command started on demand exits. One installed with `devlanes daemon install` keeps running. | `true` |

### history

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `history` | object | How long the database keeps records of finished service and task runs, and of remote boxes that no longer connect. | `{"remote_hosts_retention":"90d","retention":"7d"}` |
| <code>history.<wbr>remote_hosts_retention</code> | duration | How long a remote box stays listed by `devlanes remote ls` after its last successful connection; defaults to `90d` and must be longer than `0s`. A record without a connection time is never deleted by age, and `state.max_size` can delete records sooner. A box connected with `devlanes remote connect` and not disconnected since is never deleted, so the daemon can connect it again after a restart. | `90d` |
| <code>history.<wbr>retention</code> | duration | How long records of finished service and task runs (start and end times, exit status) are kept after the run ends; defaults to `7d` and must be longer than `0s`. A run that still has log index entries or a metrics summary in the database stays until those expire, and `state.max_size` can delete runs sooner. | `7d` |

### hostnames

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `hostnames` | object | How lane hostnames such as `api.acme2.localhost` resolve to this machine: the strategy (`resolver`), its `tld`, and whether names also go into `/etc/hosts` (`hosts_file`). The defaults need no setup for browsers that resolve `*.localhost`. | `{"hosts_file":"auto","resolver":"localhost"}` |
| <code>hostnames.<wbr>base_domain</code> | string | Reserved for the planned `public` resolver; any value is rejected for now. | `dev.example.invalid` |
| <code>hostnames.<wbr>dns_provider</code> | object | Reserved for the planned `public` resolver's DNS-01 certificates; any value is rejected for now. | `{"cloudflare":{"token":"${{ env.CF_API_TOKEN }}"}}` |
| <code>hostnames.<wbr>dns_provider.<wbr>cloudflare</code> | object | Reserved: Cloudflare credentials for DNS-01 certificates; not supported yet. | `{"token":"${{ env.CF_API_TOKEN }}"}` |
| <code>hostnames.<wbr>dns_provider.<wbr>cloudflare.<wbr>token</code> | string (templated) | Reserved: Cloudflare API token allowed to edit the domain's DNS records. Write an environment expression such as `${{ env.CF_API_TOKEN }}`, not the token itself. | `${{ env.CF_API_TOKEN }}` |
| <code>hostnames.<wbr>hosts_file</code> | `auto` or `never` or `always` | Whether lane hostnames are also written to `/etc/hosts`, for programs that do not resolve `*.localhost` themselves (workerd, musl or pure-Go binaries); defaults to `auto`, which writes them only when a probe at daemon start shows the system resolver fails `*.localhost`. `never` here wins over every project; otherwise a project's `lanes.hosts_file` replaces this value for that project. Writing needs the helper `devlanes setup` installs, only exact names are written (no wildcards), and `resolver: hosts_file` ignores this key. Values: `auto`: Write the exact hostnames only when the daemon's startup check finds that the system resolver does not answer `*.localhost` with a loopback address. `never`: Never write hostnames to `/etc/hosts`. Set in the user config, it applies to every project whatever the project sets. `always`: Always write the exact hostnames, even when the system resolver handles `*.localhost`, for programs that send `*.localhost` to DNS themselves. | `auto` |
| <code>hostnames.<wbr>resolver</code> | `localhost` or `hosts_file` or `resolver` or `public` | Which domain lanes get and what makes their hostnames resolve; defaults to `localhost`. `resolver` also needs `tld` and a one-time `devlanes setup`; `public` is not supported yet. Values: `localhost`: Lane domains end in `.localhost` (`acme.localhost`, `acme2.localhost`), which browsers and many system resolvers send to loopback by themselves. `hosts_file` covers programs that do not. `hosts_file`: Keep `.localhost` names and always write exact hostnames to `/etc/hosts`, as `hosts_file: always` does, whatever `hosts_file` says; a project's `lanes.hosts_file` still takes precedence. Needs the helper `devlanes setup` installs. `resolver`: Lane domains become `<project>.<tld>` and `<project><N>.<tld>`, and so do the names `domain()` returns. They are answered by a DNS server the daemon runs on `127.0.0.1:15353`, so wildcard hostnames resolve too for programs that use the system resolver. Needs `tld` and a one-time `devlanes setup`. Projects that set `lanes.vars.domain` or `lanes.matrix` keep their own domains, and `domain()` gives them `.localhost` names. `public`: Reserved for public DNS names with DNS-01 certificates; not supported yet, so the config is rejected. | `localhost` |
| <code>hostnames.<wbr>tld</code> | string | Top-level domain for the `resolver` strategy, such as `test`; required there. It must be `test`, `internal`, `home.arpa` or a domain under one of them (or under `localhost`), made of valid DNS labels, because any other name could belong to a real site; the file is rejected otherwise, whatever the strategy. A new devlanes CA may sign names under the `tld` set when it is created, whatever the strategy, so switching to another `tld` later needs a new CA. | `test` |

### lanes

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `lanes` | object | Where lane data folders (`lane.data_path`) live on this machine, and how long a lane may sit without activity before devlanes throttles or freezes it. | `{"data_dir":"~/.devlanes/data"}` |
| <code>lanes.<wbr>data_dir</code> | string | Folder that holds every lane's data folder, one subfolder per project; defaults to `data` in the devlanes home (`~/.devlanes/data`). Must be an absolute path or start with `~/`. The daemon reads it when it starts. After a change and a daemon restart, lanes use new, empty folders under the new root; move the old folders there by hand to keep their data. | `~/.devlanes/data` |
| <code>lanes.<wbr>freeze_after</code> | duration | After this long without activity, devlanes freezes a lane: it suspends every process of the lane and pauses the lane's own containers. Shared infra is never paused. Ports and process state are kept, so a frozen lane still counts as running. Activity is a request through the proxy, a new connection to one of the lane's ports, the lane's services using CPU, or a devlanes command on the lane; any of it resumes a frozen lane and restarts the timer. A request through the proxy resumes the lane at once and then goes through; a direct connection to a port is noticed within about half a second. When both are set above `0s`, it must be longer than `throttle_after` (`5m` unless set). A running service with `services.<name>.keep_awake: true` keeps its whole lane awake. Defaults to `15m`; `0s` turns freezing off. The daemon reads it when it starts. | `15m` |
| <code>lanes.<wbr>throttle_after</code> | duration | After this long without activity, a lane's processes run at background priority: lower CPU priority, and efficiency cores on machines that have them. They keep serving requests. Activity is a request through the proxy, a new connection to one of the lane's ports, the lane's services using CPU, or a devlanes command on the lane; any of it restores normal priority and restarts the timer. macOS and Windows only; Linux has no throttling, so lanes there stay at normal priority until `freeze_after`. When both are set above `0s`, it must be shorter than `freeze_after`. A running service with `services.<name>.keep_awake: true` keeps its whole lane at normal priority. Defaults to `5m`; `0s` turns throttling off. The daemon reads it when it starts. | `5m` |

### logs

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `logs` | object | How much service output devlanes keeps under `~/.devlanes/logs`, by age and total size. | `{"retention":{"days":7,"max_size":"5GiB"}}` |
| <code>logs.<wbr>retention</code> | object | Age and total-size limits for stored service output; a cleanup every 10 minutes deletes the oldest output past either one. Defaults to 7 days and `5GiB`. | `{"days":7,"max_size":"5GiB"}` |
| <code>logs.<wbr>retention.<wbr>days</code> | integer | Delete output older than this many days, along with its index entries in the database; defaults to 7 and must be at least 1. The newest log file of each stopped service is kept. | `7` |
| <code>logs.<wbr>retention.<wbr>max_size</code> | string | Total size of stored output across all projects and lanes; defaults to `5GiB` and must be above zero. Over it, the oldest output goes first wherever it is, but each stopped service's newest file and up to 1 MiB of a running service's current file are kept. | `5GiB` |

### metrics

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `metrics` | object | How long CPU and memory history stays in the daemon's database. | `{"retention":"7d"}` |
| <code>metrics.<wbr>retention</code> | duration | How long per-minute CPU and memory samples, per-run summaries and startup memory peaks are kept; defaults to `7d` and must be longer than `0s`. Shorter keeps less history for graphs and for the memory a start is expected to need; `state.max_size` can delete samples sooner. | `7d` |

### ports

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `ports` | object | Port range that every lane of every project takes its block of ports from, so lanes never collide. Defaults to `20000-32767`. | `{"range":"20000-32767"}` |
| <code>ports.<wbr>range</code> | string | Ports devlanes may hand out, written `start-end` (both included, start above 0); defaults to `20000-32767`, below Linux's usual ephemeral range so the OS does not pick them for outgoing connections. Each lane of each project gets its own block of consecutive ports from it, so a smaller range fits fewer lanes. | `20000-32767` |

### proxy

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `proxy` | object | Ports of the daemon's HTTP/HTTPS proxy, which serves every `hosts` entry, and whether other devices on the network may connect to it. | `{"http_port":80,"https_port":443,"lan":false,"via":true}` |
| <code>proxy.<wbr>http_port</code> | integer | Port for plain HTTP; defaults to 80, falling back to 8080 like `https_port`, and must differ from it. While HTTPS runs it redirects known hostnames to HTTPS; without a CA it serves them over plain HTTP. | `80` |
| <code>proxy.<wbr>https_port</code> | integer | Port the HTTPS proxy listens on; defaults to 443. When the port is taken or not allowed (low ports on Linux need extra permission), the daemon falls back to 8443 and service URLs carry the port. | `443` |
| <code>proxy.<wbr>lan</code> | boolean | Accept connections from other devices on the network; defaults to false, which refuses every peer that is not this machine. Those devices must still resolve the hostnames to this machine and trust the CA. | `false` |
| <code>proxy.<wbr>via</code> | boolean | Add `Via: 1.1 devlanes.run` to every response the proxy forwards from a service (`1.1 devlanes.run (remote)` for lanes on a remote box), so a client can tell that devlanes sat in between. Defaults to true; set false if a client or test compares response headers exactly. | `true` |

### remote

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `remote` | object | Settings for `devlanes remote connect`: boxes reached by a command, and how this machine serves clients when it is a box. | `{"allow_connections":false}` |
| <code>remote.<wbr>allow_connections</code> | boolean | Let other machines connect to this one with `devlanes remote connect`. Defaults to true. When false, every client is turned away and `devlanes daemon pair` gives no codes. Connecting from this machine to a box still works. Clients already connected stay until they disconnect. An invalid value counts as false. | `true` |
| <code>remote.<wbr>boxes</code> | map of object | Boxes that `devlanes remote connect <name>` reaches by running a command instead of SSH, keyed by that name. Use it for a container, a WSL distribution or a codespace. Names not listed here are SSH destinations. Changes apply at the next `remote connect`. | `{"acme":{"command":["docker","exec","-i","acme","devlanes","tunnel","--stdio"]}}` |
| <code>remote.<wbr>boxes.<wbr><name>.<wbr>command</code> | list of string | The program and its arguments as a list, one item per argument. No shell runs it. It must run `devlanes tunnel --stdio` in the box with its input and output passed through and no terminal, so use `docker exec -i`, not `-it`. It runs again for every reconnect and cannot ask for input, so sign in to its tool first. | `["docker","exec","-i","acme","devlanes","tunnel","--stdio"]` |
| <code>remote.<wbr>require_pairing</code> | boolean | Serve only clients paired with this machine. Defaults to true. A client's first connection needs a one-time code from `devlanes daemon pair`, and `devlanes daemon revoke` removes a pairing. Set false to serve every client that reaches this machine over SSH or its command. An invalid value counts as true. | `true` |
| <code>remote.<wbr>start_daemon</code> | boolean | Start the devlanes daemon here when a client connects and it is not running. Defaults to true. When false, a client keeps retrying until you run `devlanes daemon start` here. An invalid value counts as false. | `true` |

### start

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `start` | object | What a plain `devlanes start` (no services, no `--profile`) brings up: the services the lane ran before, or its default selection. | `{"restore":true}` |
| <code>start.<wbr>restore</code> | boolean | Bring back the services the lane ran before, with the same profiles, so a reboot or daemon crash costs one `devlanes start`; defaults to true. A lane stopped as a whole with `devlanes stop` has nothing to bring back and starts its `default` profile. Set false to always start the `default` profile (or every `autostart` service). `lane resume` and `daemon restart` restore either way. | `true` |

### startup

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `startup` | object | How many services may be starting at the same time across all lanes and projects, so a large start does not overload the machine. Defaults to one start per CPU core. | `{"max":4,"mode":"fixed"}` |
| <code>startup.<wbr>initial</code> | integer | Adaptive starting window; defaults to `max`, so starts run at full speed until the machine is under pressure. Set it lower to ramp up from a few slots instead; never above `max`. | `2` |
| <code>startup.<wbr>max</code> | integer | Most services starting at once: the constant window in `fixed` mode and the ceiling in `adaptive` mode. Defaults to the number of CPU cores; `all` ignores it. | `8` |
| <code>startup.<wbr>mode</code> | `adaptive` or `fixed` or `all` | `adaptive`, `fixed` or `all`; default `adaptive`. A starting service holds its slot until its ready check (`tcp`, `http`, `log` or `cmd`) passes, it fails, or `slot_timeout` passes; one without a check (`ready: started`) holds it until it settles (`settle_cpu`). Values: `adaptive`: Begin each burst of starts at `initial` (by default `max`, full speed). Halve the slots after 3 s above 90% CPU or when the system reports memory pressure, start nothing while that pressure is critical, and add a slot every 2 s while system CPU stays below `target_cpu`, up to `max`. `fixed`: Allow `max` starts at a time, never more or fewer. A start also waits while free memory cannot fit what it needs. `all`: Start every service immediately, with no limit and no memory check. | `adaptive` |
| <code>startup.<wbr>reserve_memory</code> | string | Free memory every new start must leave, on top of the memory the services already starting are expected to still need (each one's past startup peak, or 512 MiB without history). Defaults to 10% of RAM, at most `2GiB`; `0B` turns it off. A start with nothing else starting never waits for it. `all` ignores it. | `2GiB` |
| <code>startup.<wbr>settle_cpu</code> | string | Only for services without a ready check (`ready: started`, the default for a service without `hosts`): such a service gives its slot back once its process tree uses less than this share of one core for 2 s, meaning it has finished starting up. Services with a `tcp`, `http`, `log` or `cmd` check keep their slot until the check passes instead. Defaults to `10%`. | `10%` |
| <code>startup.<wbr>slot_timeout</code> | duration | Longest a starting service holds its slot; after it, the next queued service may start even if this one is not ready, so services waiting on each other cannot block the queue. Defaults to `20s` and must be above zero; the service itself keeps starting. | `15s` |
| <code>startup.<wbr>target_cpu</code> | string | System CPU use below which `adaptive` mode adds slots; defaults to `75%`, and must be above 0% and at most 100%. Lower it to keep the machine responsive during large starts; other modes ignore it. | `75%` |

### state

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `state` | object | Size budget for the daemon's database; past it, the oldest metrics and history are deleted before their retention ends. | `{"max_size":"512MiB"}` |
| <code>state.<wbr>max_size</code> | string | Size the database (`state.db` with its WAL and shared-memory files) should stay under; defaults to `512MiB` and must be above zero. When it is larger, the cleanup deletes the oldest metric samples first, then the oldest history (finished runs, log index entries, remote boxes), even before their retention ends. Lanes, ports and running units are never deleted, so the file can stay above the budget. | `512MiB` |

### terminal

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `terminal` | string or object | Terminal integration of the CLI, written as a mapping with `progress` (the tab's progress bar during starts). A plain string, the terminal app name older versions used, is still accepted but ignored with a warning. | `{"progress":"auto"}` |
| <code>terminal.<wbr>links</code> | `auto` or `always` or `never` | Whether service URLs, config error locations and log or bundle paths are clickable links (the OSC 8 sequence); defaults to `auto`, which writes them in iTerm2, kitty, WezTerm, Ghostty, Windows Terminal, VTE terminals, Konsole, Alacritty and foot. Elsewhere the same text prints without a link. `DEVLANES_TERM_LINKS=0` or `1` overrides this setting. Values: `auto`: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. `always`: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. `never`: Never use it. | `auto` |
| <code>terminal.<wbr>marks</code> | `auto` or `always` or `never` | Whether long command output (logs of several services, failed starts) marks where each section starts, so the terminal's jump-to-mark or jump-to-prompt keys move between them; defaults to `auto`: marks in iTerm2, and prompt marks (OSC 133) in WezTerm, Ghostty, kitty, foot and Windows Terminal. `DEVLANES_TERM_MARKS=0` or `1` overrides this setting. Values: `auto`: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. `always`: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. `never`: Never use it. | `auto` |
| <code>terminal.<wbr>notifications</code> | `auto` or `always` or `never` | Whether a start that took a while finishing or failing, and a running service crashing, show a desktop notification while the terminal is not focused; defaults to `auto`. The TUI knows focus from the terminal; a command such as `devlanes start` does not, so with `auto` it notifies only in kitty, which itself skips a focused window. Terminals without notifications get the bell. At most one notification per 10 seconds. `DEVLANES_TERM_NOTIFICATIONS=0` or `1` overrides this setting. Values: `auto`: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. `always`: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. `never`: Never use it. | `auto` |
| <code>terminal.<wbr>progress</code> | `auto` or `always` or `never` | Whether starts show their progress in the terminal tab's own progress bar (the OSC 9;4 sequence); defaults to `auto`. Only human output on a terminal gets it, and `DEVLANES_TERM_PROGRESS=0` or `1` overrides this setting. Values: `auto`: Only in terminals known to draw it (Ghostty, WezTerm, iTerm2 3.6.6+, Windows Terminal, ConEmu, Ptyxis 48+, Konsole 26.04+); never inside tmux, screen, CI or a dumb terminal. `always`: In every terminal, for ones `auto` does not recognize such as kitty; a terminal without support may ignore it or show it as text or a notification. Inside tmux it is passed through, which needs `allow-passthrough on`. `never`: Never draw the progress bar. | `auto` |
| <code>terminal.<wbr>title</code> | `auto` or `always` or `never` | Whether the terminal tab's title says what devlanes is doing (`devlanes · acme@0 · starting 3/8` in the TUI, `devlanes start acme@0 · 3/8` or `devlanes logs -f api` for long commands); defaults to `auto`. The previous title is saved on the terminal's title stack and put back on exit, so `auto` sets it only where that stack exists: xterm, iTerm2, kitty, Ghostty and VTE terminals such as GNOME Terminal. Never inside tmux or screen, which manage titles themselves. `DEVLANES_TERM_TITLE=0` or `1` overrides this setting. Values: `auto`: Only in terminals known to support it, on human output to a terminal; never inside tmux, screen, CI or a dumb terminal. `always`: In every terminal, tmux included; a terminal without support may ignore it, or keep a title devlanes cannot put back. `never`: Never use it. | `auto` |

### tls

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `tls` | object | Which certificate authority signs the proxy's HTTPS certificates (`ca`), and which stores `devlanes setup` installs it into (`trust`). | `{"ca":"devlanes","trust":["system","firefox"]}` |
| <code>tls.<wbr>ca</code> | `devlanes` or `mkcert` | Which CA signs the certificate for each lane hostname; defaults to `devlanes`. Choose `mkcert` when your browsers already trust mkcert's CA. Values: `devlanes`: devlanes creates its own CA in `~/.devlanes/ca` on first use, limited to names under `localhost`, `test`, `internal`, `home.arpa` and `hostnames.tld`, so it cannot sign for real sites. Trust it once with `devlanes setup`. `mkcert`: Load mkcert's existing root CA (from `mkcert -CAROOT`) instead, so browsers that already trust it need nothing new. HTTPS stays off when `mkcert` is not on `PATH`; the mkcert CA has no name limits, and `devlanes setup --remove` leaves it trusted. | `devlanes` |
| <code>tls.<wbr>trust</code> | list of `system` or `firefox` or `chrome` or `windows` or `ios-simulator` or `android-emulator` | Stores `devlanes setup` installs the CA into and `devlanes setup --remove` removes it from; an empty list means `[system]`. On Linux, browsers do not read the system store, so add `chrome` and `firefox` there; inside WSL add `windows` for browsers on Windows. `devlanes doctor` shows each store's state. | `["system","firefox"]` |

### tui

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `tui` | object | Whether start commands open the terminal UI, how it looks, and what quitting it does with running lanes. | `{"open_on_start":false}` |
| <code>tui.<wbr>glyphs</code> | `auto` or `unicode` or `ascii` | Characters the TUI draws states, markers and the spinner with; defaults to `auto`. Set `ascii` when your font shows boxes or misaligned symbols. Values: `auto`: Unicode, or ASCII when the locale (the first of `LC_ALL`, `LC_CTYPE` and `LANG` that is set) is not UTF-8. `unicode`: Always Unicode symbols, such as ● ✕ ▾ and a braille spinner. `ascii`: Plain ASCII only, for fonts or terminals that draw Unicode symbols badly. | `auto` |
| <code>tui.<wbr>on_quit</code> | `ask` or `keep` or `stop` | What `q` or Ctrl+C does while lanes of the project on screen are running; defaults to `ask`. Ticking "remember" in the quit dialog writes the choice here. Values: `ask`: Show a dialog each time to choose between keeping the lanes running and stopping them. `keep`: Close the TUI at once; every lane keeps running in the daemon. `stop`: Stop the running lanes of the project on screen, then close the TUI. Lanes of other projects keep running. | `ask` |
| <code>tui.<wbr>open_on_start</code> | boolean | Open the TUI for `start`, `restart`, `lane resume` and `lane new --start` when they run at a terminal; defaults to true. Set false to have them print text instead, as `--no-ui` or `DEVLANES_NO_UI=1` do. | `true` |
| <code>tui.<wbr>theme</code> | `auto` or `dark` or `light` or `ansi` | Color palette of the TUI; defaults to `auto`, which picks one from what the terminal reports. Choose `dark` or `light` when auto guesses wrong; `NO_COLOR` in the environment turns colors off whatever this says. Values: `auto`: Use the true-color dark or light palette when the terminal reports true color (`COLORTERM`) and its background (`COLORFGBG`); otherwise the terminal's 16 colors. `dark`: Always use the true-color palette made for a dark background. `light`: Always use the true-color palette made for a light background. `ansi`: Use only the terminal's 16 named colors, so the TUI follows the terminal's color scheme. | `auto` |

### updates

| Path | Type | Description | Example |
| --- | --- | --- | --- |
| `updates` | object | Release checks: whether the daemon looks for newer devlanes releases (`mode`), and whether the TUI shows release highlights after an update (`highlights`). | `{"mode":"disable"}` |
| <code>updates.<wbr>highlights</code> | boolean | Show the release highlights dialog the first time the TUI opens after an update; defaults to true. Set false to skip the dialog. The new version still counts as seen, so turning this back on does not show the highlights of releases you already skipped. | `true` |
| <code>updates.<wbr>mode</code> | `disable` or `notify` or `auto` | What devlanes does about newer releases: `notify` (the default) checks every 4 hours and when the TUI opens, and shows the new version; `auto` also installs patch releases in the background where the install method allows it; `disable` never checks. `DEVLANES_NO_UPDATE_CHECK=1` or a CI environment also turns checks off. Values: `disable`: Never look for releases; nothing is read from the network. `notify`: Read the latest version from the registry devlanes was installed from, and show it in the TUI and in command output when it is newer. Nothing is installed. `auto`: Like `notify`, and also install patch releases in the background where the install method allows it; they apply the next time you start devlanes at a terminal. Minor releases, which can break things before 1.0, still wait for you to update. | `disable` |
