Template Permissions¶
By default every agent Leo spawns gets the full leo MCP tool surface: it can spawn more agents, stop its siblings, toggle scheduled tasks, and message anyone. A template's permissions block narrows that.
templates:
scout:
workspace: ~/agents/scout
permissions:
deny_tools: [leo_spawn_agent, leo_stop_agent, leo_toggle_task]
can_message: [rocket, olympus, "scout-*"]
can_spawn: [codex]
can_consult: [fable, opus]
permissions is optional, and so is every key inside it. A template without one behaves exactly as it did before this feature existed.
Both doors: MCP tools and the leo CLI¶
Permissions are applied in two places, from one policy:
- The agent's
leoMCP server, which omits denied tools and checks the allowlists before calling the daemon. - The
leoCLI, which every agent can run from a shell. This matters more than it looks:leo_skillpoints agents at the CLI for several of these operations, so shelling out is the path a cooperative agent takes by default, not an evasion route a hostile one has to go looking for. Denyingleo_spawn_agentwithout gatingleo agent spawnwould not reduce spawning at all — it would just move it.
| CLI command | Governed by |
|---|---|
leo agent spawn | leo_spawn_agent + can_spawn |
leo agent worktree | leo_spawn_agent, plus can_spawn when --template is given |
leo agent stop / start / reset / restart / rename | leo_stop_agent |
leo run <task> | leo_run_task |
leo task enable / disable | leo_toggle_task |
leo service stop / restart / reparent | leo_stop_agent |
Lifecycle commands with no exact tool equivalent map to leo_stop_agent: denying "stop other agents" plainly means to deny disrupting them. The leo service entries are there for the same reason — stop and reparent take down every live agent session, discarding in-flight context, which is the withheld capability at greater scale.
leo agent delete is deliberately absent from this table. There is no leo_stop_agent-equivalent MCP tool for deletion, by design — agents cannot delete agents, themselves or otherwise. Deleting an agent record (and, for worktree agents, its checkout) is a human-operator action only, whatever a template's permissions allow.
leo agent worktree spawns from the source agent's template by default, and that new agent inherits the source template's permissions, so escalation is only possible via --template — which is why the allowlist applies there.
The agent is told, not just refused¶
leo_skill documents several of these capabilities through more than one route — the leo CLI and the daemon's HTTP API both spawn agents. A restricted agent reading those instructions cold would try one and be refused, so every skill it loads is prefixed with a short notice naming what its template withholds:
> **Permissions notice.** This agent's template withholds some Leo capabilities:
>
> - denied tools: leo_spawn_agent, leo_stop_agent
> - may only spawn templates: codex
>
> Anything below that uses a withheld capability will be refused — through
> the leo MCP tools and the `leo` CLI alike. The other routes documented
> here reach the same capabilities and are withheld too: do not fall back
> to the daemon's HTTP API, another agent's tmux session, or editing
> leo.yaml to work around this. Ask the operator instead.
The wording is deliberate. It says the capability is withheld rather than that every route is blocked, because only the MCP tools and the CLI actually enforce it — for the HTTP API and tmux, instruction is the only lever there is. An unrestricted agent's skills are unchanged.
A guardrail, not a security boundary¶
Both checks read the policy from an environment variable in the agent's own process, and every agent runs as the same user holding the same daemon token. An agent that means to get around them can clear the variable, call the daemon's HTTP API directly, or drive another agent's tmux session.
Use them to shape what an agent reaches for — to stop a reporting template from spinning up fleets, or keep a noisy worker from paging every other agent. Do not use them to contain an agent you do not trust.
Fields¶
| Field | Meaning |
|---|---|
deny_tools | Leo MCP tool names removed from this template's agents. |
can_message | Allowed to targets for leo_send_message. |
can_spawn | Allowed template values for leo_spawn_agent. |
can_consult | Allowed template values for leo_consult. |
A denied tool is never registered, so it does not appear in the agent's tool list at all. Calling it anyway — a model may remember it from an earlier session — returns tool "leo_spawn_agent" is not permitted for this agent.
The three allowlists are checked before the daemon is called, and are appended to the tool's description so the model sees the boundary instead of finding it by failing:
Empty means unrestricted¶
An absent or empty allowlist places no restriction. To take a capability away completely, deny the tool:
This is deliberate: YAML and JSON both collapse an empty list on a round trip, so can_message: [] could not reliably mean "nobody" — it would quietly become "anybody" the first time the config was saved from the web UI.
Glob patterns¶
Allowlist entries match exactly, or as a glob (*, ?, [...]), so generated agent names stay addressable — scout-* covers scout-leo and scout-olympus. Matching is case-sensitive.
leo_send_message accepts shorthand agent names that the daemon resolves, and the permission check runs on the literal argument, before that resolution. Two consequences worth knowing:
- An allowlist of
[rocket]rejectsto: "rock", even though the daemon would have resolved it. Fail-closed, with the allowed targets in the error. - An entry authorizes whatever that string resolves to at send time, not a fixed agent.
can_message: [olympus]permits messaging whichever live agentolympusresolves to — an exact name, a display name, a repo short, or an-olympussuffix. That is the same resolution you would get typing it yourself, so write entries the way you would address the agent. Ambiguous queries are rejected by the daemon rather than delivered to a guess.
Matching here is case-sensitive while the daemon's resolver is not, so [Rocket] will not authorize to: "rocket". Write entries in the case the agent will use.
leo_skill cannot be denied¶
Every agent's system context tells it to call leo_skill to operate Leo, so a template that could deny it would only produce a confusing dead end. Listing it in deny_tools is a config error.
Validation¶
leo validates the block on every config load and before every web-UI save:
deny_toolsentries must be real tool names. A typo would otherwise leave the tool available — the exact opposite of what was asked for — so it is an error, and the message lists the valid names.can_spawnandcan_consultentries that are not globs must name a defined template.can_messageis not validated. Agent names are generated at spawn time, so there is nothing to check them against.
Because those references are checked, a template named in another template's can_spawn/can_consult cannot be deleted until the reference is removed — the same rule a persistent task's template: already follows. Renaming is handled for you: the new name is cascaded into every allowlist that named it.
Applying a change¶
Permissions are resolved from config and handed to the agent's MCP server in its environment at process start. Editing a template's permissions therefore affects newly spawned agents immediately, but an already-running agent keeps the set it started with until it is restarted:
leo agent restart and leo agent reset both re-resolve permissions from current config in both directions — a restriction you added is applied, and one you removed is dropped. An agent whose template no longer exists in config keeps whatever it started with: there is no policy left to resolve against, and silently lifting the restriction would be the wrong guess.
Scope¶
Permissions apply to ephemeral agents spawned from a template, and to persistent tasks that target one via template:.
They do not apply to:
- Scheduled (
runtime: oneshot) tasks, which invokeclaude -pthrough their own launch path. - Consultant subagents started by
leo_consult, which likewise build their own launch spec. Note thatcan_consultstill governs which templates an agent may consult — it is the consultant's own tool surface that is unrestricted.
leo agent list, logs, and attach are read-only and ungated, as are the config-editing commands (leo task add/remove) — those have no tool equivalent and are operator surface, not agent surface.
There is also no inbound policy: can_message controls who an agent may message, not who may message it. Enforcing that would require the daemon to authenticate the caller, which it cannot do today — every agent carries the same token.
Web UI¶
The four lists are editable on a template's config page, inside the Advanced section — Deny tools, Can message, Can spawn, Can consult. Each is a comma-separated list; clearing a field lifts that restriction.