Policy
What agents on your machine may do. A top-level block in profile.config.ts, beside
extensions and settings — never inside them.
// profile.config.ts
export default defineProfile({
policy: {
shell: {
allow: ["*"], // any program, but...
args: { "*": "escalate" }, // ...every invocation asks
raw: false, // and never through sh -c
spawn: false, // long-lived children: off entirely
},
env: { allow: [] }, // no host variables reach any agent
fs: { read: ["~/work"], write: ["~/work/output"] },
},
})
Declaring nothing means no opinion, not denied — every capability falls through to the agent's own policy. A machine with no profile policy runs agents with your privileges, which is the right default for your own tools and the wrong one for anything you installed.
Why this is yours and not the agent's
Two configs can declare policy, and they answer different questions.
axon.config.ts is a statement about the agent: "this agent only reads
./src". The author knows what their agent needs, and says so.
profile.config.ts is a statement about your machine: "nothing on my laptop
touches ~/.ssh". That cannot depend on every agent author agreeing with you forever —
which is exactly why it lives here, where you can write it once and have it hold for an
agent you installed and never read.
A ceiling, not a default
An agent narrows within your policy and can never widen past it. The stricter of the two layers wins on every call.
| Your profile says | The agent says | Result |
|---|---|---|
| nothing | anything | the agent's rule |
true | anything | the agent's rule |
false | anything | denied |
"escalate" | anything but false | escalate |
| a glob | a glob | both carried, evaluated per call |
That last row is what makes the ceiling hold. Two glob rules are never merged into one,
because allow: ["git status"] unioned with allow: ["git push"] would permit a command
neither layer allowed on its own. Both are kept and evaluated against the real subject at
call time, so widening is impossible by construction — and a denial can name which layer
decided it.
List surfaces intersect rather than union: net.allow and env.allow keep only what both
layers admit, denylists union, and shell.raw is an AND. A profile that turned the raw
shell off cannot have it re-enabled by an agent.
A blanket "*": "escalate" therefore reaches every capability an agent names — a ceiling a
specific grant cannot punch through.
It applies whether or not the terminal is running
Your policy is read from profile.config.ts on every agent load — axon dev, axon run inside a script, an agent spawned by another agent. It is parsed directly rather than
imported and evaluated, so it never depends on the terminal having loaded your config.
That is deliberate, and it is the difference between a ceiling and a suggestion: a rule that only applied when the TUI happened to be open would be advisory, and advisory is not a ceiling.
What each capability accepts
The block is the same shape an agent declares — fs, net, shell, env, tools,
limits, isolation — read as a ceiling instead of a grant. One grammar, documented once:
policy → — every field, what it accepts, and what each of the two enforcement layers can express.
When a rule pauses a call
An "escalate" rule does not deny — it asks you. The call pauses, you answer, and
answering "allow always" writes a grant that stops it asking again.
A grant can only ever satisfy an escalation. It can never overturn a deny from either
layer: "stop asking me this" and "override the ceiling" are different acts, and one
keypress must not undo a machine-wide rule you set deliberately.
Escalations → — the prompt, the three outcomes, and where grants are written.
See also
profile.config.ts — the file this lives in. Settings — the other top-level block. Kernel & Policy — why the two-layer split exists.