Command rule evaluation and limits
For: tenant admins and governance reviewers.
Which rule decides
A command is read into parts, every program it runs: each part of a list (&&, ||, ;), a pipeline, a subshell, a substitution ($(...)), a here-document, and what a shell, sudo, ssh, xargs or find -exec runs. Each part is decided on its own (a construct outside what the reader reads also makes the floor ask, below):
- A Refuse that matches the part wins, wherever it stands, as an explicit deny does. Only removing it lifts it.
- Otherwise the first rule in order that matches decides, Allow or Ask. A rule of yours stands above a default it carves into and below one it is broader than: it carves into a default when it matches none of that default's
not_matchexamples. Soterraform destroy -target=module.test.*for one agent stands abovecmd.cloud-deleteand decides those commands, whilerm **stands belowcmd.delete-home, sorm -rf ~still asks.oz0 policy allowplaces a new rule above the first of yours it is narrower than (more fixed tokens, a class where the other has*, or the same pattern for some agents only), and says where it stands among the defaults. - A part no rule matches runs as the job's hooks and approvals say.
The strictest part decides the command, and the approval names the part that asked. Then one fixed floor raises it to Ask when the reader could not read the whole command with certainty: an Allow cannot match what cannot be read. A Refuse over cmd.unreadable makes the floor refuse instead; nothing turns it off.
Since the reader reads only what it models exactly, that Refuse refuses every command the floor would ask about, a large share of what agents run (about 29 in a hundred of the commands agents ran in this project's own sessions), not only the rare command a reader of known dangers could not read: give it to unattended jobs that run fixed commands.
Each rule decides only when it matches itself: two rules that share an ID never decide for each other, and a policy whose rule repeats another's ID has that rule broken (a broken Ask or Refuse asks about every command, a broken Allow allows nothing). A policy of more than 500 command rules, more than a node reads, asks about every command rather than lose a Refuse past the 500th.
Which rules a running job uses
A job decides its commands with the command rules it read when it started on its home node, as it does its spend and labels: a rule added or removed later reaches only jobs that start after it, on every node within a few seconds. A rule's end still stops it inside a running job.
A person's yes to a command that waited longer than the tenant's re-check threshold (an hour by default) is decided again with the rules the node has then, so a Refuse added while it waited stops it.
When that re-check cannot be made (the node cannot read its tool policy, or the check fails), the command is refused: nothing could show that no such Refuse was added. To stop a running job's commands now, use a kill switch.
What it sees, and what it does not
Command rules are a check, not a boundary: the sandbox is. They see only the commands a harness asks about before it runs them, and read each command alone:
- commands a harness runs without asking: Codex runs commands in its read-only sandbox and Gemini CLI its read-only tools without asking (see which harnesses ask), so no rule sees those, and the sandbox is their boundary;
- what a program does once it runs: a script file (
bash deploy.sh), code in another language (python -c), a downloaded file run later, a package's binary, and a program the reader does not know that runs a command it is given; - files earlier calls wrote: within one command, a script, makefile or git hook the command writes and then runs is unreadable, but a file an earlier call wrote is part of the working folder when a later call runs it, read no more than any other file there;
- the session's real directory and variables: a parameter set by an earlier call, or a
cd ~in an earlier call that the shell kept, is not known; a path it cannot resolve is asked about rather than guessed.
So the rules catch a model's mistake and the plain forms of an attack, such as a downloaded script piped into a shell, not an attacker who knows them and writes a script around them. Keep shell sessions in tier 1, give agents only the tools they need, and keep harness.requires_approval where a person should see every command.
Governed, audited, exported
A command rule is part of the tool policy: a change takes a reason, and one that loosens (a new Allow, a default turned off, a Refuse removed or demoted to shadow, a narrowed pattern, an Allow moved above an Ask) waits for another admin where four eyes is on.
Each decision is in the audit trail as run.command.decided, with the agent, the harness tool, the rule's ID, the action, the program and classes of the part that decided and the shadow rules' verdicts, never the command itself.
A job's trace shows each decision as a check named command rules, after the call's hooks and before the call.
oz0 policy export writes the rules under commands: and oz0 policy apply reads them back, showing each change first; a file with command rules needs this release's oz0 (an older one refuses the key). A shadow rule records in those entries what it would have decided.
The guardrails plugin's command guard reads commands with the same reader and the same defaults; on a node whose runtime decides commands itself, it leaves them to these rules, so a person is asked once.
Review and interface limits
A release that loosens or removes a default command rule does not present that change to a tenant admin for acceptance. Review the defaults when upgrading.
The console lists rules in policy order, without sorting by their latest match. The impact estimate on an allowlist card uses audit entries that contain no command text, so it is an upper bound rather than a full replay of past commands. See allow from a card.
The server checks the card's proposed command and records the approval it names without reading the job. Keep the sandbox boundary in place for shell sessions.