Config Reference¶
Complete field-by-field reference for leo.yaml.
Config lives at ~/.leo/leo.yaml (the Leo home directory).
See Delegation profiles for stable work roles routed to templates by an active profile.
defaults¶
Settings inherited by all tasks and templates unless overridden.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | No | Default model, validated by the resolved harness (claude and codex: any non-whitespace string — the CLI resolves aliases like sonnet/fable and full IDs like claude-fable-5 itself; opencode: must be provider/model). Defaults to sonnet. Does not cascade to a scope whose resolved harness differs from defaults.harness — see Harnesses → Cross-harness model cascade. |
max_turns | int | No | Default maximum agent turns per execution. Defaults to 15. Ignored by codex and opencode (no per-turn cap upstream). |
harness | string | No | Adapter name for this scope and everything that cascades from it. One of claude, codex, opencode. Defaults to claude. All three run every leo primitive (tasks, ephemeral agents, persistent tasks) — see Harnesses. |
harness_options | map | No | Adapter-specific options, strictly validated by the resolved harness. For claude: permission_mode, bypass_permissions, remote_control, agent, allowed_tools, disallowed_tools, append_system_prompt. For codex: permission_mode. For opencode: permission. See Harnesses for the full reference and merge rules. |
environments | list | No | Ordered named environments applied to everything that does not set its own list. |
idle_suspend_after | string | No | Idle interval (Go duration, e.g. 24h) after which an ephemeral agent is auto-stopped (dormant, auto-wakes on the next message). Empty/unset disables it. See Idle-suspend. |
push.chat | bool | No | Whether an agent's Leo Chat messages push to paired phones. Unset means on; a template's push.chat overrides it. |
push.needs_input, push.finished, push.errored | bool | No | Whether an attention transition into that state pushes to paired phones. Each state is its own switch; unset means on, so nothing changes until you turn one off. A template's own push block overrides these per state. Edits apply to the next transition with no restart. |
dispatch.viewer | map | No | Dispatch viewer settings: placement (pane, window or background, default pane), max_panes (1–6, default 3), and main_pane_height (20–90 percent, default 60). |
dispatch.max_concurrent | int | No | Cap on dispatch turns running at once (default 6, 0 = unlimited, negative is rejected). New dispatches and interactive follow-ups beyond the cap queue in one FIFO line instead of being rejected. See Dispatches → Records and limits. |
dispatch.approval_timeout | string | No | How long an interactive claude dispatch's permission prompt waits for the orchestrator's decision (Go duration, positive, at most 24h; default 30m) before the prompt falls back to the dispatch's pane. See Dispatches → Permission prompts. |
dispatch.adopt_timeout | string | No | How long a restarted daemon waits for an adopted interactive dispatch to show life (its bridge reconnecting, a hook report, or its pane changing) before closing it as daemon restarted (Go duration, positive, at most 1h; default 2m). See Dispatches → Daemon restarts. |
Custom Anthropic-compatible endpoints (z.ai GLM, OpenRouter, Moonshot, DeepSeek, MiniMax, …) are configured via each scope's own env: map (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN) — see Harnesses → providers is gone.
harness_options by harness¶
Every scope's harness_options map is strictly validated by that scope's resolved harness: — unknown keys, wrong types, and invalid enum values are all rejected at config load and before every web-UI save. Full behavior (merge rules, resume, MCP bridge, auth, etc.) is in Harnesses.
claude (7 keys):
| Key | Type | Meaning |
|---|---|---|
permission_mode | string | acceptEdits, auto, bypassPermissions, default, dontAsk, plan. |
bypass_permissions | bool | Legacy --dangerously-skip-permissions fallback; only consulted when permission_mode is empty. |
remote_control | bool | Enables --remote-control. |
agent | string | Path to a subagent file, passed via --agent. |
allowed_tools | list of strings | Tool whitelist. |
disallowed_tools | list of strings | Tool blacklist. |
append_system_prompt | string | Extra text appended to the system prompt. |
codex (1 key):
| Key | Type | Meaning |
|---|---|---|
permission_mode | string | read-only (codex default), workspace-write, danger-full-access, or approve-for-me (workspace-write, with escalations routed to codex's automatic approval reviewer rather than a human). |
opencode (1 key):
| Key | Type | Meaning |
|---|---|---|
permission | map | Per-tool allow/ask/deny, or a nested pattern map of the same. Delivered via a per-spawn OPENCODE_CONFIG_CONTENT overlay, not argv. |
web¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | bool | No | false | Enable the web dashboard. |
port | int | No | 8370 | TCP port for the web UI. |
bind | string | No | 127.0.0.1 | Bind address. Loopback-only by default. |
allowed_hosts | list of strings | No | [] | Extra hostnames/IPs accepted in the Host and Origin headers, in addition to loopback. Required when bind is non-loopback. Entries must not include a port. |
trusted_proxies | list of strings | No | [] | IPs or CIDRs whose requests with a non-empty Remote-User header are authenticated as the operator. |
public_urls | list of strings | No | [] | Origins a paired phone reaches this daemon at (e.g. https://leo.example.com behind a Cloudflare Tunnel). leo pair advertises them; each one's exact host[:port] is accepted in Host/Origin. Absolute http(s) URLs with no path, query or credentials. Empty = the listener's LAN addresses. |
When enabled, the daemon serves a web dashboard with agent monitoring, task management, agent dispatch, config editing, and cron preview.
Authentication¶
Both browser and API access require the same token. On first start the daemon mints a random 64-hex-char token and writes it to ~/.leo/state/api.token (mode 0600).
Browser login. Visit the dashboard and you'll be redirected to /login. Paste the token there and a 7-day session cookie is set (HttpOnly, SameSite=Strict). For convenience:
prints a one-click URL (http://<bind>:<port>/login?token=...) — the login page auto-submits if the token is in the query string. The URL contains the token; don't share it.
Click Sign out at the bottom of the sidebar to destroy the session.
API access. /api/* endpoints take the token in an Authorization: Bearer header:
Rotate the token by deleting api.token and restarting the daemon. Existing browser sessions remain valid until they expire (7 days).
Token scope. There are two tokens, with different privileges:
| Token | File | Accepted on | Held by |
|---|---|---|---|
| Operator | ~/.leo/state/api.token | everything — /login, the browser UI, /api/*, /web/* | you, your scripts, channel plugins |
| Agent | ~/.leo/state/agent.token | /api/* and /web/agent/{name}/{message,send,interrupt} only | every supervised agent, via LEO_API_TOKEN |
| Device | ~/.leo/state/devices/devices.json (SHA-256 hashes only) | everywhere the operator token is as a bearer, except /login | a phone paired with leo pair |
The operator token grants access to the full daemon API — including routes that restart the service, mutate config, and write prompt files. Treat it like a root credential.
Agents get the narrower token so a credential that escapes an agent — into a transcript, a log, a channel message — cannot be exchanged for a web session. It is rejected at /login and on the config editor, which renders template env: values in full. Rotate either by deleting its file and restarting the daemon.
This bounds blast radius; it is not a sandbox. Agents run as your user and can read ~/.leo/leo.yaml directly, so any credential in config is reachable by an agent that goes looking. Don't install a channel plugin you wouldn't trust with the contents of your config.
Non-loopback access¶
bind defaults to 127.0.0.1. To expose the web UI on your LAN:
web:
enabled: true
bind: 0.0.0.0
port: 8370
allowed_hosts:
- 192.0.2.10 # the IP your LAN will use to reach this host
- leo.local # or a hostname
allowed_hosts entries are checked against the incoming Host and Origin headers to defend against DNS-rebinding and drive-by cross-origin POSTs. Entries must be bare hostnames or IPs — no port, no scheme. allowed_hosts is required when bind is non-loopback; leo validate will fail otherwise.
For an Authelia, oauth2-proxy, or similar ForwardAuth proxy, configure its peer IPs or CIDRs in trusted_proxies. Requests arriving directly from those peers with a non-empty Remote-User header are treated as the operator. The proxy must overwrite or strip any client-supplied Remote-User header (for example, Authelia ForwardAuth with Traefik's authResponseHeaders does this). The proxy's public hostname must also be in allowed_hosts; its forwarded port is ignored for trusted peers. Zoned IPv6 addresses such as fe80::1%en0 are rejected by validation.
web:
enabled: true
bind: 0.0.0.0
port: 8370
allowed_hosts: [leo.example.com]
trusted_proxies: [192.0.2.9, 198.51.100.5]
Never trust a range that includes untrusted LAN hosts: anyone who can reach the port directly from a trusted address could forge the header.
The daemon prints a startup warning when bind is non-loopback.
Pairing a phone¶
leo pair prints a QR code and a one-line JSON payload — {daemon_id, name, urls, pairing_code} — for the Leo iOS app (leo pair --json prints the payload with warnings, expires_at and the exact qr_payload string instead; see leo pair). leo web enable turns the listener on first if it is off. The code works once and expires after 5 minutes. The app redeems it at POST /api/v1/pair (the one unauthenticated route; rate-limited to 5 attempts per minute per peer and 20 overall) for its own device token. leo devices list shows paired phones; leo devices revoke <id> invalidates a token immediately; the device's open streams (SSE, attach) are closed within about 2 seconds, and its push registration goes with it.
Push. When an agent's attention moves into needs_input, finished or errored, the daemon encrypts a small alert (AES-256-GCM, aad leo-push-v1, under the key the phone registered) and POSTs it to {relay_url}/push for every registered device, asynchronously with a bounded queue, a 10 s timeout and one retry on a 5xx or network error. A 410 from the relay deletes that registration. The agent name never reaches the relay or Apple unencrypted (the collapse id is a hash). Repeats of the same state do not re-notify. The alert's title is the short state line ("Has a question", "Needs permission: Bash", "Finished", "Hit an error"); an optional body says what needs attention — the question, the command or path awaiting permission, or the finished turn's summary (clipped to 400 characters, omitted when there is nothing to show, and older app builds ignore it). The content stays end-to-end encrypted to the phone, but it appears in the notification, so what shows on a locked screen follows iOS's Show Previews setting (When Unlocked keeps it off the lock screen). Which states push is configurable per state, daemon-wide and per template, via push (see defaults and templates); a change applies live. If the config cannot be read when a transition arrives, the push is sent anyway.
Routes for the app (bearer auth; device or operator token):
| Route | Behaviour |
|---|---|
PUT /api/v1/devices/{id}/push | {grant, key, relay_url} push registration (key: base64 of 32 bytes, AES-256; relay_url must be https://). A device may write only its own; the operator api.token may write any. |
DELETE /api/v1/devices/{id}/push | Unregisters push for that device (same auth; idempotent). |
GET /api/v1/agents/{name}/attach | WebSocket onto tmux attach -r. Binary frames: terminal output. Text frames: JSON control — ping/pong (the daemon pings every 30 s) and {"type":"resize","cols":N,"rows":M}. Optional ?cols=&rows=. Binary input frames close the socket with 1008. |
chat¶
Leo Chat settings.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
whisper_model | string | No | -- | whisper.cpp ggml model for voice notes (~ expands). Unset: voice notes are delivered untranscribed. |
permission_timeout | duration | No | 120s | How long a Claude permission prompt waits for an answer in chat, at most 1h. |
question_timeout | duration | No | 1h | How long an AskUserQuestion questions card waits for an answer in chat, at most 1h. |
client¶
Remote-host definitions used by the leo agent CLI when leo is invoked as a client of a different machine. Empty on server configs.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
default_host | string | No | — | Host name to use when --host and LEO_HOST are unset. |
hosts | map | No | {} | Named host definitions keyed by short name. |
Each entry under hosts has:
| Field | Type | Required | Description |
|---|---|---|---|
ssh | string | Yes | SSH target passed verbatim (e.g. user@host, or a Host alias from ~/.ssh/config). |
ssh_args | list | No | Extra arguments inserted between the target and the remote command (e.g. ["-p", "2222"]). |
leo_path | string | No | Absolute path to leo on the remote host. Defaults to $HOME/.local/bin/leo (matches install.sh). Override when leo is installed elsewhere or the remote's non-interactive SSH shell doesn't have it on PATH. |
tmux_path | string | No | Path to tmux on the remote host. Used by agent attach and agent logs --follow. Defaults to tmux (relies on PATH). Set to /opt/homebrew/bin/tmux for macOS arm64 homebrew remotes, /usr/local/bin/tmux for macOS intel. |
client:
default_host: prod
hosts:
prod:
ssh: alice@leo.example.com
ssh_args: ["-p", "2222"]
leo_path: /usr/local/bin/leo
tmux_path: /opt/homebrew/bin/tmux
dev:
ssh: alice@devbox.local
Why leo_path exists: SSH runs a non-interactive shell on the remote, which doesn't source .zshrc / .bashrc. If leo lives in ~/.local/bin and PATH is only extended in .zshrc, bare leo won't resolve. The default full path avoids that; set leo_path explicitly when the remote installs leo elsewhere (Homebrew, /usr/local/bin, etc.).
Resolution order for the target host: --host flag → LEO_HOST env → default_host → first entry sorted by key → localhost (only when no hosts are configured). --host localhost is a hard override. On a host with a follow block, the "first entry" step is skipped: with no default_host the CLI targets localhost, since the lead's entry is there to be followed, not to be the CLI's remote. See the Remote CLI guide.
api_clients¶
External API clients — agents or jobs Leo does not supervise — each with a scoped bearer token. Manage them with leo client add|list|rm; see API Clients.
| Field | Type | Required | Description |
|---|---|---|---|
can_message | list | One of the two | Agent names or globs the client may start a turn in (POST /web/agent/<target>/message). |
can_post | list | One of the two | Agent names or globs the client may post chat text to, with no turn (POST /web/agent/<target>/chat/post). |
An empty scope denies everything, and a client with neither list is rejected at load.
Channels¶
Leo does not ship with any built-in messaging channel. Channels are Claude Code plugins the user installs separately (e.g. Telegram, Slack, webhook). In leo.yaml they are referenced by plugin ID strings like plugin:telegram@claude-plugins-official on the channels: field of templates and tasks.
Leo passes the resolved list to the spawned Claude process via the LEO_CHANNELS environment variable. The plugin owns its own credentials, routing, and inbound-message handling.
To install a channel plugin:
Then reference it:
templates:
assistant:
workspace: ~/agents/assistant
channels: [plugin:telegram@claude-plugins-official]
Development Channels¶
For channel plugins that aren't yet published to a registry (or for local plugin development), templates and tasks accept a parallel dev_channels: field. Leo passes each entry to Claude Code via --dangerously-load-development-channels <id> and exports the list in LEO_DEV_CHANNELS.
templates:
assistant:
workspace: ~/agents/assistant
channels: [plugin:blackpaw-telegram@blackpaw-plugins]
dev_channels: [plugin:blackpaw-telegram@blackpaw-plugins]
Validation matches channels — each entry must be a valid plugin ID.
Claude Code displays a confirmation prompt before loading development channels. For supervised agents, Leo watches the tmux pane and auto-accepts the prompt so the agent starts non-interactively. Silent/nonexistent entries are ignored by Claude Code without warning — verify spellings carefully.
environments¶
Top-level map of named env maps (environments.<name>.<KEY>: value), referenced by name from defaults, templates, tasks and leo agent spawn --environment. See Environments. A value may be a secret reference resolved at launch.
appearance_rules¶
Ordered list of rules that give agents a palette color and/or an emoji, e.g. - when: { field: environments, op: contains, value: prod } with color: red. Fields are name, environments, template and harness; ops are is, contains, starts_with and glob. A value set by hand on an agent wins over every rule. See Agent Appearance.
tasks¶
Each task is a named entry under the tasks map. Tasks are invoked by the in-process cron scheduler or manually via leo run <task>.
tasks:
daily-briefing:
schedule: "0 7 * * *"
timezone: America/New_York
prompt_file: prompts/daily-briefing.md
enabled: true
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
workspace | string | No | ~/.leo/workspace/ | Working directory. |
schedule | string | Yes | -- | 5-field cron expression. |
timezone | string | No | System default | IANA timezone (e.g., America/New_York). |
prompt_file | string | Yes | -- | Path to prompt file, relative to workspace. |
model | string | No | defaults.model | Model override, validated by the resolved harness. Does not fall back to defaults.model when this task's harness differs from defaults.harness — see Cross-harness model cascade. |
harness | string | No | defaults.harness | Adapter override for this task. claude, codex, and opencode all support one-shot and runtime: persistent tasks (persistent tasks deliver into agents, which all three harnesses support). See Harnesses. |
harness_options | map | No | merged with defaults.harness_options (same harness only) | Adapter-specific options — for claude: permission_mode, bypass_permissions, allowed_tools, disallowed_tools, append_system_prompt. bypass_permissions at task scope is honored (not defaults-only). For codex: permission_mode. For opencode: permission. See Harnesses. |
max_turns | int | No | defaults.max_turns | Max turns override. Ignored by codex/opencode. |
environments | list | No | defaults.environments | Ordered named environments for this task's run (or implicit persistent agent). Not allowed on a persistent task that names a template:. |
timeout | string | No | 30m | Max duration before kill (e.g., 30m, 1h). |
retries | int | No | 0 | Retry attempts on failure. |
channels | list | No | -- | Channel plugin IDs used by the channel notification of notify_on_fail (oneshot tasks) or reply delivery (persistent tasks). Only valid on a channel-supporting harness. For a runtime: persistent task with template: set, must be a subset of the target template's channels:. |
dev_channels | list | No | -- | Unpublished channel plugin IDs loaded via --dangerously-load-development-channels. |
notify_on_fail | bool | No | false | On a oneshot task that fails (all attempts exhausted, not interrupted), post a failure card to the owner's Leo Chat thread, and, when channels: is set, also spawn a short child claude invocation to notify those channels. On a persistent task, enqueue a follow-up failure-notice prompt into the target agent (requires channels:). |
enabled | bool | No | false | Whether the scheduler should run this task. |
silent | bool | No | false | Prepend silent-mode preamble to prompt. |
runtime | enum | No | oneshot | oneshot spawns a fresh claude -p per firing; persistent injects the prompt into a supervised agent instead. See Persistent Tasks. |
template | string | No | -- | Only meaningful with runtime: persistent. Names a templates: entry — the task's target is that template's agent. Omit for an implicit target: an agent named after the task, synthesized from the task's own workspace/model/channels/dev_channels. See Persistent Tasks. |
queue_max | int | No | 5 | Only meaningful with runtime: persistent. Max queued firings per target agent; overflow is rejected with "queue full". 0 also means "use the default". |
owner | string | No | see Leo Chat | Agent whose Leo Chat thread gets this task's cards (leo_chat_post from a run). Validated as an agent name only; resolved when a card posts. Unset: a persistent task's own agent, else the agent whose workspace matches, else assistant. |
Silent Mode¶
When silent: true, Leo prepends a preamble instructing the agent to work without narration. The agent should deliver its final message via a configured channel plugin or output NO_REPLY if there's nothing to report.
templates¶
Templates are reusable blueprints for spawning ephemeral agents. Dispatch them via the HTTP API, a channel plugin that exposes agent commands, or the web UI.
templates:
coding:
model: sonnet
workspace: ~/agents
harness_options:
remote_control: true
permission_mode: auto
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
workspace | string | No | ~/.leo/agents/ | Base directory for agent workspaces. Repos are cloned as subdirectories. |
channels | list | No | -- | Channel plugin IDs for spawned agents. Only valid on a channel-supporting harness. |
dev_channels | list | No | -- | Unpublished channel plugin IDs loaded via --dangerously-load-development-channels. |
model | string | No | defaults.model | Model, validated by the resolved harness. |
harness | string | No | defaults.harness | Adapter override for this template. All three harnesses support ephemeral agents. See Harnesses. |
harness_options | map | No | merged with defaults.harness_options (same harness only) | Adapter-specific options — for claude: permission_mode, bypass_permissions, remote_control, agent, allowed_tools, disallowed_tools, append_system_prompt. remote_control cascades like the rest (template, then defaults.harness_options) and defaults to true when neither sets it. See Harnesses. |
max_turns | int | No | defaults.max_turns | Max turns. |
mcp_config | string | No | -- | Path to MCP config file. |
add_dirs | list | No | -- | Additional directories. |
env | map | No | -- | Environment variables. Wins over environments. A value may be a secret reference. |
environments | list | No | defaults.environments | Ordered named environments; replaces (does not extend) the inherited list. |
idle_suspend_after | string | No | defaults.idle_suspend_after | Idle interval (Go duration) before agents from this template are auto-stopped (dormant, auto-wakes on the next message). Empty inherits the default. |
isolation | string | No | -- | worktree runs every dispatch and consult of this template from a throwaway managed Git worktree at the caller's committed HEAD — the same as passing isolation: "worktree" to leo_dispatch, which takes precedence when given. The subagent's prompt names the worktree and warns that uncommitted changes in the caller's tree are not visible. Lets a reviewer run tests with a writable sandbox (e.g. codex permission_mode: workspace-write) without touching the real tree. See Dispatches. |
push.chat | bool | No | defaults.push.chat | Per-template override of whether this template's agents push their Leo Chat messages. |
push.needs_input, push.finished, push.errored | bool | No | defaults.push.* | Per-state override of whether agents from this template push to paired phones. Resolution per state: this template, then defaults.push, then on. Agents with no template (implicit persistent-task agents) use defaults.push. |
permissions | map | No | -- | Narrows the leo MCP tool surface for agents spawned from this template (deny_tools) and which agents/templates they may message, spawn, or consult (can_message, can_spawn, can_consult). See Permissions. |
When dispatching with a repo (/agent coding owner/repo via a channel plugin, or leo agent spawn coding --repo owner/repo), Leo clones the repo into <workspace>/<repo> using gh. The agent session is named leo-<template>-<owner>-<repo>. A repo is optional — leo agent spawn coding with no repo runs the template as-is directly in workspace, and the agent is named after the template (coding in this example).
Templates also back persistent tasks (runtime: persistent): a task with template: <name> delivers its prompts into that template's agent instead of spawning claude -p, spawning/resuming it on demand rather than at leo service boot. Multiple tasks can share one template: and therefore one agent and one FIFO queue. A runtime: persistent task with no template: gets an implicit target instead — an agent named after the task, synthesized from the task's own fields. See Persistent Tasks for the full mechanics (ensure-exists, queueing, completion reporting, leo agent reset).
Sharing templates between machines (templates_file)¶
Set templates_file to keep agent templates in a separate file (for example in a git repo you sync across machines) while the rest of leo.yaml stays per-machine:
# ~/dotfiles/leo/templates.yaml: one top-level key, same schema as `templates:` above
templates:
coding:
model: opus
environments: [work]
- Path.
~is expanded; a relative path resolves against the directory holdingleo.yaml. The file must exist and parse, or Leo refuses to load the config and names the path. Any top-level key other thantemplatesis an error. - Merge. Effective templates are the shared ones, then the local
leo.yamltemplates by name. A local template replaces a shared one of the same name entirely; fields are never merged. - Per-machine values belong in local
environments(referenced by name from shared templates) or in~paths. A shared template that references an environment the machine lacks fails validation like any other. - Source. The web UI shows a
shared,localorlocal · overrides sharedbadge on each template, andleo template listgains a SOURCE column (--jsonaddssource). - Writes. Editing or deleting a shared template writes the shared file; editing or deleting a local template writes
leo.yaml. Each file is rewritten only when its content changed, so editing a local template never dirties your repo. Deleting a local override uncovers the shared template again. Renaming a shared template keeps it in the shared file. New templates go toleo.yaml, unless you tick shared on the web create form or passleo template add --shared. Comments in either file are not preserved on a rewrite. - Revisions. The config revision (the web/API
If-Match) covers both files, so agit pullthat changes the shared file makes a stale edit fail its revision check. - Reload. Leo does not watch the shared file and never runs git. After a
git pull, runleo service reload, use the web reload, or make any save. - Trust. The shared file has the same trust level as
leo.yaml: templates can setharness_options(for example, bypass permissions). Only pointtemplates_fileat a repo you control. - Renaming a shared template rewrites task references in this machine's
leo.yamlonly; other machines whose tasks use the old name fail validation after their next pull.
Following a lead host (follow)¶
A host can mirror another Leo host's templates, environments, delegation and defaults over ssh instead of sharing a file. See Following a lead host for setup, the cache and errors.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
host | string | Yes | — | A client.hosts name: the lead. Not localhost or a loopback ssh target. |
interval | duration | No | 5m | How often the daemon re-pulls the lead (minimum 30s). The cache is called stale after 3 × interval without a successful pull. |
- Layering, lowest to highest: the lead, the follower's
templates_file(templates only), the follower'sleo.yaml. A local template, environment or delegation profile replaces the lead's entry of the same name entirely. Indefaultsand the otherdelegationkeys, a key set locally replaces the lead's value for that key (defaults.dispatch.*anddefaults.push.*one level deeper;harness_optionswhole; an empty local value overrides too, exceptdefaults.model: "",defaults.max_turns: 0anddelegation.active_profile: "", which count as unset). - Trust. The follower runs
leo config export --followon the lead over ssh with theclient.hostsentry, the same trust asleo agent --host. The lead needs no config. Lead entries can setharness_options(for example, bypass permissions): only follow a host you control. - Writes. Lead entries are read-only on the follower (
owned_by_lead);leo template override <name>(and the web Override locally button) copies one intoleo.yamlfor editing. - Invalid entries. A lead entry that fails on the follower (unknown harness, harness binary not on
PATH, a missingCLAUDE_CONFIG_DIR, ...) is listed aslead · invalidand unusable instead of failing the whole config. - CLI target. With
followset and noclient.default_host, the CLI's default host is localhost.
Idle-suspend¶
Ephemeral agents can be auto-stopped after a period of inactivity to free local resources (the claude process and tmux session are killed) while preserving the record, workspace, and stored conversation — an agent that goes dormant this way is no different from a manually stopped one, except that it auto-wakes on the next message. Off by default — enable it by setting an interval:
defaults:
idle_suspend_after: "24h" # global default
templates:
reviewer:
idle_suspend_after: "30m" # per-template override
Or per spawn: leo agent spawn reviewer owner/repo --idle-suspend 24h.
The cascade is spawn flag → template → defaults; the resolved interval is stamped onto the agent at spawn time. Behavior:
- Activity is measured by the agent's tmux
session_activity— injected prompts, interactive typing in an attached pane, and the agent's own output all count. - An agent with a client attached is never auto-stopped, even past the interval (so reading scrollback won't yank the session out from under you).
- An idle-stopped agent shows as
stoppedinleo agent list, same as any other dormant agent. It auto-wakes on the next incoming message (e.g.leo_send_message), rejoining its prior conversation via--resume. You can also start or stop it manually:
A manual leo agent stop does not auto-wake — that behavior is specific to the idle sweep. Start it explicitly when you're ready to resume it.
- Dormant agents stay dormant across daemon restarts (they are not resurrected at boot), and their worktrees are never removed — only
leo agent deleteremoves a record or worktree.
State directory¶
Leo's runtime state lives under ~/.leo/state/ (or <home>/state for a non-default leo home): sessions.json (session-id bookkeeping — including the post-hoc-discovered codex/opencode session ids, see Harnesses → Session driver semantics), history.json, api.token (see Authentication), and on a follower follow-cache.json and follow-status.json (see Following a lead host). There is no more per-harness state directory — codex and opencode drive a resident tmux TUI the same way claude does, so their session ids live in the same sessions.json store rather than separate per-tmux-session files.
If you're updating from a pre-uniform-tmux-TUI build, stale state/opencode/*.json and state/transcripts/*.log files from the old model are inert and safe to delete; see the migration note in Harnesses.
Override Cascade¶
Task and template settings override defaults: