Permission classes: what an agent may run
How a permission class bounds an agent's tools, what the file looks like, and how to write your own.
A permission class is a small JSON file that says what an agent session may do on the machine that hosts it. A class is named in configuration and the engine delivers it to the sessions it spawns — for a coding agent, only where that agent reads such a document (see Which backends receive a grant).
⚠ A class only ever NARROWS. It cannot grant a capability the agent would not otherwise have — deny is the default and a class is a subtraction from what the host already permits.
First: there are TWO class formats, and which one you write depends on the agent
This is the thing to settle before you write a line, because the two files look nothing alike and each is refused by the wrong reader.
| you are bounding | the file you write | who reads it |
|---|---|---|
| a substrate process — an ordinary executable the engine runs under mandate | the engine's own class schema (below) | the engine itself, before it spawns |
| a coding-agent session — the common case | the agent vendor's settings document, which the engine provisions into the session's working directory | the agent's own permission layer |
Why it is split this way, in one sentence: the engine can enforce a policy on a process it launches itself, but a coding agent enforces its own tool permissions — so for those the engine's job is to deliver the right document, not to police the tools. ⚠ Not every agent has such a document. Where one does not exist, what bounds the session is the confinement around it, not a class — and the product says so per agent rather than implying a policy it cannot write.
The agent-session class, which is the one most readers want
allowlists tool patterns the session may use. Both the bare name and the absolute path are worth listing: they are different strings to the matcher, and a session that invokes the absolute path is refused by a class that only allowed the bare name.denysubtracts. ⚠ Read the next section before trusting adenyline.envis what the session inherits. A toolchain that needsPATHor a root variable gets it here; without it, a tool that exists on the host is not on the session's path.
The engine's own class schema, for substrate processes
| field | what it does |
|---|---|
version | the schema this file is written against. The accepted set is closed — a file naming a version outside it fails to load rather than being coerced, so nobody ever runs under a policy other than the one they named |
name | a human label, for the record and for your own reading |
commands_allow | the executables a session may run, each with the subcommands it may pass. An entry names an absolute argv0 |
write_scope | where the session is meant to write, relative to its own working directory. ⚠ It records intent and is not enforced by the class; what bounds writes is the confinement around the run |
Schema v2 adds two per-command fields
pathputs a directory on the session'sPATH.env_mintdeclares an environment variable whose value the engine mints at spawn, never a value read from the file.modeis one ofworkdir(joinsvalueonto the session's own working directory — a per-session cache, for example),artifact-run-dirorhost-go-cache.
⚠ v2 is additive, never a redefinition. A v1 file may not carry v2 fields: it is refused rather than accepted-and-ignored, so "version 1" means the same thing in every build.
Fields for the container posture
Both versions also accept fields that matter only when a run is placed in a container
(run.confinement = container, see Sandboxing):
| field | what it does |
|---|---|
network | none (the default) or allowed. Under the container posture it is the mechanism: none gives the container no network. Under every other posture it is a label in the record only — nothing restricts traffic by it; egress control does that. |
container_cpus, container_memory_bytes, container_pids_limit | the container's resource caps — declare all three or none. Inert on every other posture. |
container_user | the non-root user the container runs as; root is refused. |
Writing your own — five things worth knowing before you do
- Name absolute paths. An entry whose
argv0is a bare name depends on whatever the session'sPATHhappens to be, which is not a policy. - A shell in
commands_allowgrants everything reachable from a shell. If a class admits a shell —Bash(bash:*),Bash(sh:*), or a shell incommands_allow— then every tool that class denies by name may still be reachable through it, and the class's owndenylines become decoration. This is the single easiest way to write a class that reads strict and is not. ⇒ If a session genuinely needs a shell, grant it deliberately and stop treating the rest of the list as a boundary — and put the real boundary somewhere that a shell cannot step around. - A malformed value is a refusal, not a default — an unknown
env_mintmode, a version outside the accepted set, v2 fields in a v1 file each fail to load. ⚠ A misspelled field NAME is not caught: the engine's class reader skips a key it does not know, so a typo such ascomands_allowloads as a class with no commands at all. Check every field name against this page before relying on a class. - Write the narrow class first, then widen it when something is refused by name. The refusal tells you what to add; guessing forward tells you nothing and usually grants more than the work needs.
- A class is not a sandbox. It bounds which commands a session may invoke. The filesystem and network boundary around the session is a separate mechanism, configured separately — see Sandboxing and Egress control.
Where the class comes from at spawn time
workspace.worktree_grant_class names the class for every session the engine spawns in the
workspace, and workspace.grant_class.<role> overrides it for one fleet role. Changing the
workspace-wide class changes the surface of every session that has no role-specific class — worth
knowing before you edit the setting rather than after.