Profile policy
A ceiling over every agent on this machine. An agent narrows within it and can never widen it — so a profile that says "ask me before any shell command" means every agent asks, including one you installed from the registry and never read.
// profile.config.ts
export default defineProfile({
policy: {
tools: {
search: "escalate", // ask before any @axon/search tool
},
process: {
run: "escalate", // every one-shot command asks
spawn: false, // long-lived children: off entirely
},
},
})
That search: "escalate" is what the rule looks like from the other side. The agent calls
a search tool, the call suspends, and you answer — whichever agent it was, whether or not
its own config had anything to say about searching:

Answer allow always and a grant is written, so that call stops asking. Answer deny
— or write false instead of "escalate" — and the agent is told the call was blocked:

It sits at the top level, beside extensions and settings — never inside them.
settings is the set of keys the terminal acts on; policy is read by the runtime. A
policy block nested under settings is silently ignored rather than honoured, because a
boundary that moves depending on where you wrote it is not a boundary.
Why the profile, not the agent
An agent's own policy block is a statement about what that agent needs. That is the
right layer for "this agent only reads ./src" — the author knows. It is the wrong layer
for "nothing on my laptop touches ~/.ssh", because it depends on every agent author
agreeing with you, forever, including in agents you install without reading.
The ceiling is resolved in the kernel, at the one seam every consumer passes through:
the TUI, axon dev, axon run in a script, a test harness. A ceiling applied only on the
terminal's spawn path would be advisory, and the CLI is exactly where someone scripting
would step around it without meaning to.
The two layers
Both layers keep their rules. They are not merged into one — the pair is carried down and evaluated per call, so a verdict can always name which layer produced it.
That split matters more than it looks. Merging two glob rules is where a ceiling springs a
leak: a profile allowing git status unioned with an agent allowing git push would
permit a command that neither layer permits alone. Composing the shape and deferring the
verdict is what makes that impossible.
The stricter layer wins, ranked deny > escalate > allow. On a tie the profile is
reported as the source, because telling you the agent denied something your profile also
denies sends you to the wrong file to fix it.
| Profile says | Agent says | Result |
|---|---|---|
false | anything | denied — a bare profile denial is final |
"escalate" | anything but false | asks — the agent cannot skip the prompt |
true | its own rule | the agent's rule stands |
| (nothing) | its own rule | the agent's rule stands |
| glob rule | glob rule | both evaluated; stricter verdict wins |
A profile grant is a ceiling, not a grant: true removes a constraint you might have
imposed, it does not hand an agent a capability its own policy withholds.
Rule shapes
Any capability takes one of four forms:
tools: {
github: true, // allow
payments: false, // deny — final if written at the profile
search: "escalate", // ask, every time
shell: { // glob rule
allow: ["git *"],
escalate: ["docker *"],
deny: ["rm -rf *"],
},
}
Inside a glob rule, deny beats escalate beats allow regardless of the order you
write them. A consequence worth knowing: an escalate: ["**"] catch-all alongside an
allowlist makes the allowlist unreachable — everything matches the catch-all first. If you
mean "ask me about all of it", write "escalate" on its own.
A glob rule that is declared but matches nothing is a denial, not silence. Naming what is permitted and hitting none of it is an answer.
Rules key on the namespace a module registers its tools under, not on individual
functions — search: "escalate" covers every tool @axon/search brings.
fs, limits and isolation
Three keys behave differently, because they are OS-layer facts rather than per-call decisions:
fs and limits are enforced by bind mounts and cgroups, which the mediator never
sees and which cannot express an intersection of globs. When the profile declares either,
the profile's value is taken outright — a ceiling a bind mount ignores is not a ceiling.
isolation is a tier, not a rule, so it resolves as a maximum: none < auto <
hardened. A profile asking for hardened cannot be loosened by an agent, but an agent
may harden beyond its profile.
Declaring any of fs, network or limits at the profile also opts every agent on the
machine into rootless confinement, even one that declares no policy of its own. Setting
nothing at all means full machine access — the deliberate default, so that a new user is
not fighting a sandbox before they have an agent worth confining.
When the config is broken
A profile that fails to parse yields no ceiling rather than refusing to boot. It fails open, matching the rest of the profile: the config loader reports the breakage where you can see it, and a ceiling that made a typo unbootable would strand you inside the very file you need to edit.
Worth knowing if you rely on the ceiling for something that matters — it is a safety rail for your own machine, not an adversarial sandbox. The confinement layer is what contains a hostile agent; see policy for what the capsule enforces.
See also
policy — the agent-level block, and what each key means.
Kernel & Policy — how enforcement works, and where the mediator sits.
profile.config.ts — the rest of the file.