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. One class is named in configuration, the engine hands it to every session it spawns, and the session gets nothing the class does not 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 may write, relative to its own working directory |
Schema v2 adds two fields, and adds nothing else
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.mode: "workdir"joinsvalueonto the session's own working directory — a per-session cache, for example.
⚠ 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.
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 malformation is a refusal, not a default. An unrecognised field, an unknown
env_mintmode, a version outside the accepted set — each one fails to load. Nothing is silently ignored, so a class that loads is the class you wrote. - 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 Security.
Where the class comes from at spawn time
One class is named in the workspace configuration and applies to every session the engine spawns there. ⚠ It is a single setting for the whole workspace today: changing it changes the surface of every session, not one. If a particular job needs a different toolchain, that is worth knowing before you edit the setting rather than after.