---
title: "Tool policy"
description: "Decide what each call of a tenant's tools may do, Allow, Ask or Refuse, by what each function says it does: Read, Write, Sends out, Destructive or Unknown."
canonical_url: "https://docs.oz0.ai/govern/tool-policy"
---
# Tool policy

> Decide what each call of a tenant's tools may do, Allow, Ask or Refuse, by what each function says it does: Read, Write, Sends out, Destructive or Unknown.

For: tenant admins and governance reviewers.

Set a tool policy rule to allow, ask about or refuse a function call. Start with the rule commands on this page; use the companion references for conditions, origins, sequence checks and the runtime boundaries.

A tenant's tool policy decides what happens to each call of its plugins' functions. MCP tools say what they do with annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`); the policy reads them, and the tenant decides. They are hints from the tool, and the policy is yours.

## The words

The console, the CLI, approval cards, the shadow report, errors and these pages use the same words.

**What the policy does with a call:**

<table>
<thead>
  <tr>
    <th>
      Word
    </th>
    
    <th>
      What happens
    </th>
    
    <th>
      Stored as
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Allow
    </td>
    
    <td>
      the call runs
    </td>
    
    <td>
      <code>
        allow
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Ask
    </td>
    
    <td>
      a person decides each call before it runs, as an <a href="https://docs.oz0.ai/agents/approvals">
        approval
      </a>
    </td>
    
    <td>
      <code>
        approve
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Refuse
    </td>
    
    <td>
      the call is not made, and the agent is told why
    </td>
    
    <td>
      <code>
        refuse
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Judge
    </td>
    
    <td>
      unavailable: a switch set to Judge is refused, and a plugin's suggested <code>
        judge
      </code>
      
       is written as Ask
    </td>
    
    <td>
      none yet
    </td>
  </tr>
</tbody>
</table>

A person's own answer to an approval stays **Approve** or **Deny**: it says what that person did with one call, not what a rule does. The API, the stored policy and the files of older commands keep the stored names, so scripts that write `approve` keep working, and every command also takes `ask`.

**What a function does**, its type, from its annotations (the first that fits):

<table>
<thead>
  <tr>
    <th>
      Type
    </th>
    
    <th>
      Which functions
    </th>
    
    <th>
      By default
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Unknown
    </td>
    
    <td>
      those that declare no annotations, and those that are not read-only and say nothing about <code>
        destructiveHint
      </code>
      
      , which MCP reads as destructive
    </td>
    
    <td>
      Ask
    </td>
  </tr>
  
  <tr>
    <td>
      Read
    </td>
    
    <td>
      those that say <code>
        readOnlyHint: true
      </code>
    </td>
    
    <td>
      Allow
    </td>
  </tr>
  
  <tr>
    <td>
      Destructive
    </td>
    
    <td>
      those that say <code>
        destructiveHint: true
      </code>
    </td>
    
    <td>
      Ask
    </td>
  </tr>
  
  <tr>
    <td>
      Sends out
    </td>
    
    <td>
      those that say <code>
        destructiveHint: false
      </code>
      
       and reach beyond the tenant (they do not say <code>
        openWorldHint: false
      </code>
      
      ): what they send leaves the tenant, such as mail, webhooks and posts
    </td>
    
    <td>
      Allow
    </td>
  </tr>
  
  <tr>
    <td>
      Write
    </td>
    
    <td>
      the rest: they change something inside the tenant, or in a system they manage, and destroy nothing
    </td>
    
    <td>
      Allow
    </td>
  </tr>
</tbody>
</table>

An open-world write whose data stays in the system it manages, such as a repository's issues, says so with `"oz0/class": "write"` in its annotations or its `_meta` (Python's MCP SDK keeps only the second: `@server.tool(annotations=..., meta={"oz0/class": "write"})`), and is then Write rather than Sends out, but only in a reviewed listing: a plugin installed from the official index under its reviewed namespace, whose review checked that the data stays there.

Anyone else's mark changes nothing, and the function stays Sends out, because the type is the group `oz0 plugin policy --set` and the install screen switch functions by: a mark any author could set would take mail out of the Sends out group. The mark changes only the type; the policy decides by the annotations themselves, below.

Two markers go with a type. **Hints only**: a [remote MCP server's](https://docs.oz0.ai/govern/tool-policy/plugin-functions#remote-mcp-servers) function, whose annotations count only where they make a call stricter, so a remote Read function still asks. **Host tool**: a function that acts on a node's host (`oz0/class: host.read`, `host.write` or `host.destructive`). Rules by name and by kind cover host tools by themselves; a condition on [`tool.class`](https://docs.oz0.ai/govern/tool-policy/conditions#action-classes) covers them only with its host form, which `oz0 policy ask --class` (and `allow` and `refuse`) writes for you.

The policy keeps four defaults, one for each of Read, Destructive, Unknown and **Reaches outside** (every function that does not say `openWorldHint: false`, and every remote server's): a function gets the strictest default that applies to it. Write and Sends out functions take the Reaches outside default when they reach beyond the tenant, and Allow otherwise.

Rules name functions, `<tool>.<function>` with `*` matching anything, and win over the defaults, in order: the first rule that matches a call decides it. A plugin's own `requires_approval` still asks a person, unless the policy refuses the call. The platform puts one kind of rule of its own before yours: a [chat channel's](https://docs.oz0.ai/build/apps/chat-channels/conversations#questions-and-approvals-in-the-thread) post function and the function that reads its deliveries are refused to other plugins' agents and flows, which post with the channel's `send` function instead. A rule of yours that names such a function exactly (`oz0 policy ask oz0_slack.post`) replaces the platform's.

Size. A pattern is at most 256 bytes, wherever a policy holds one (a rule's `match`, a sequence rule's functions, the functions the tenant vouches for as records): 256 bytes hold the longest name a pattern spells out, a function by its address, `<plugin>/<tool>.<function>`.

A policy holds at most 1,000 rules and 2 MiB as JSON. The edge reads the rules on every tool call of the tenant's jobs, so setting a policy over these bounds is refused with the place that is over.

A policy stored before them that holds a longer pattern cannot be read whole: the edge refuses the tenant's tool calls, saying so, and sends its nodes nothing new until an admin sets the policy again; its lint, its approval counts and a replay of it say why instead of running.

### Moving a plugin to v1.0

Since v1.0 an Unknown function asks a person at each call (before, it ran). So do its hooks: a hook whose function declares nothing waits for an approval each time it is asked, and while it waits or once it is denied it counts as its `on_timeout` answer (a guard that fails closed stops the call). Either:

- Declare what each function does, which is the fix: `readOnlyHint: true` for a function that only reads; `destructiveHint: false` for one that writes but destroys nothing, or `destructiveHint: true` for one that does; and `openWorldHint: false` for one that stays inside the tenant. In Python, `@server.tool(annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False))` (see [Tools](https://docs.oz0.ai/build/parts/tools/definitions#say-what-a-tool-does)). `oz0 plugin new` writes tools that do. `oz0 dev` lists the functions of your plugin that the policy does not let run freely, each with its fix.
- Or, while the authors annotate them, let the tenant run them as before: one rule per function, `oz0 policy allow jev.decide --reason "..."`, or every Unknown function at once, `oz0 tenant tool-policy set --undeclared allow`.

## Change it one rule at a time

`oz0 policy` changes the tenant's tool policy one rule at a time, in these words:

```bash [Terminal]
oz0 policy show
oz0 policy allow 'web.*' --reason "ticket 812"
oz0 policy ask jev_support.refund --deciders team:payments --not-starter --reason "refunds need a person"
oz0 policy refuse github.merge_pull_request --when "!args.repo.startsWith('acme/')" --reason "only our repositories"
oz0 policy explain rule-3f9c0a2b81d4
oz0 policy edit rule-3f9c0a2b81d4 --action ask --reason "a person decides after all"
oz0 policy remove rule-3f9c0a2b81d4 --reason "the trial is over"
```

```text
Tool policy of tenant default, set by operator:admin 2m ago:
By default, a function no rule names gets the default for its type:
  Read             Allow
  Destructive      Ask
  Reaches outside  Allow
  Unknown          Ask
Rules, in order: the first that matches a call decides it.
  rule-3f9c0a2b81d4  jev_support.refund  Ask (decided by team:payments; never by the job's starter) · refunds need a person
  rule-taint-default *                   Ask (when job.tainted && tool.outbound; preset taint-default) [only raises a decision]
```

Where a new rule goes. `oz0 policy allow`, `ask` and `refuse` add one rule where a plugin's switch puts its rule: just before the first rule that decides one of its functions for every caller, so it wins over that rule. Rules that decide only some calls (a condition, a caller, another plugin's functions) and narrower rules (`github.merge_pull_request` under `github.*`) stay before it and keep winning for theirs, and floors such as the taint-default rule still raise a decision.

A rule with the same pattern that decides for every caller is changed in place, keeping its ID. A rule with `--when`, `--caller` or `--other-plugins` goes before the first rule that decides one of its functions, so it wins for the calls it names. The command says where the rule went; `--dry-run` says it and saves nothing.

Flags, not text. Every setting of a rule is a flag:

<table>
<thead>
  <tr>
    <th>
      Flag
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        --when &lt;cel>
      </code>
    </td>
    
    <td>
      decide only the calls a <a href="https://docs.oz0.ai/govern/tool-policy/conditions#conditions-on-each-call">
        condition
      </a>
      
       holds for
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --caller plugin:&lt;name>
      </code>
      
      , <code>
        publisher:&lt;namespace>
      </code>
      
      , <code>
        source:&lt;kind>
      </code>
    </td>
    
    <td>
      only calls by these plugins (<a href="https://docs.oz0.ai/govern/tool-policy/conditions#rules-by-caller">
        rules by caller
      </a>
      
      ); repeatable
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --other-plugins
      </code>
    </td>
    
    <td>
      only calls of another plugin's functions
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --type read
      </code>
      
      , <code>
        destructive
      </code>
      
      , <code>
        declares-nothing
      </code>
      
      , <code>
        not-read
      </code>
      
      , <code>
        reaches-outside
      </code>
    </td>
    
    <td>
      only functions of this kind (a rule by kind); see below for what each takes in
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --class read
      </code>
      
      , <code>
        write
      </code>
      
      , <code>
        egress
      </code>
      
      , <code>
        destructive
      </code>
    </td>
    
    <td>
      only calls of this <a href="https://docs.oz0.ai/govern/tool-policy/conditions#action-classes">
        action class
      </a>
      
      , host tools included: it writes the condition with each class's host form
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --shadow
      </code>
    </td>
    
    <td>
      decide nothing and record what the rule would have decided (<a href="https://docs.oz0.ai/govern/tool-policy/review-changes#shadow-mode-and-ends">
        shadow mode
      </a>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --expires &lt;time or duration>
      </code>
    </td>
    
    <td>
      when the rule stops, such as <code>
        2026-12-31T00:00:00Z
      </code>
      
       or <code>
        72h
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --deadline
      </code>
      
      , <code>
        --escalation
      </code>
      
      , <code>
        --confirm
      </code>
      
      , <code>
        --deciders
      </code>
      
      , <code>
        --not-starter
      </code>
    </td>
    
    <td>
      for an Ask rule: <a href="https://docs.oz0.ai/govern/tool-policy/review-changes#approval-deadlines-and-escalation">
        its approvals
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --origin &lt;argument>=&lt;origin>
      </code>
    </td>
    
    <td>
      an <a href="https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#where-arguments-came-from">
        origin rule
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --after-...
      </code>
      
      , <code>
        --then-...
      </code>
      
      , <code>
        --within
      </code>
    </td>
    
    <td>
      a <a href="https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#sequence-rules">
        sequence rule
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --id
      </code>
      
      , <code>
        --reason
      </code>
      
      , <code>
        --decision-ref
      </code>
    </td>
    
    <td>
      the new rule's ID, and why
    </td>
  </tr>
</tbody>
</table>

Write and Sends out are types, not kinds a rule can name: `--class write` and `--class egress` decide by the action class, call by call.

One rule at a time. `remove <id>`, `edit <id>` and `explain <id>` work on one rule (`oz0 policy show` lists the IDs). `edit` changes only what its flags name, in the rule's place; `--action`, `--match`, `--enforce` (a shadow rule then decides) and `--no-when`, `--no-expires`, `--no-callers`, `--no-deciders` are its own. `explain` says what a rule does with today's catalog: the functions it names, which of them it decides, the earlier rules that decide some of them first, the later rules it hides, and what still raises a decision of its functions (floors, origin and sequence rules, a plugin's `requires_approval`):

```text
Rule rule-gh: github.* Refuse
It names 4 functions in today's catalog: github.get_file, github.create_issue, github.merge_pull_request, github.delete_branch.
It decides github.create_issue, github.merge_pull_request for every caller: Refuse.
Hidden by the earlier rule rule-7a github.* Allow (for Read functions), which decides github.get_file first.
It hides the later rule rule-9c github.merge_pull_request Ask for github.merge_pull_request.
```

Every change takes a reason (`--reason`, at most 500 characters; scripts may set `OZ0_REASON` instead) and may take `--decision-ref`, the decision or case it follows.

Admins and owners of the tenant may change the policy, everyone in it may read it, and the audit log records each change (`tenant.tool_policy`) with its reason.

In a tenant with [four eyes](https://docs.oz0.ai/govern/reviewed-changes#four-eyes-for-governance-changes) on, a change that loosens the policy waits for another admin; without it, the change applies at once.

A plugin's configuration that loosens always waits for another admin; a function's switch that loosens waits for another admin only where four eyes are on ([two paths](https://docs.oz0.ai/govern/tool-policy/plugin-functions#two-paths-for-a-plugins-functions)).

In the console, **Govern › Policy** shows the policy under **Rules**, each rule with its own deadline and deciders, and lets admins edit it; the tenant-wide approval deadline and escalation ladder are under **More**, **Approval defaults**.

**Build › Tools and skills** shows, beside each function, what the policy does with its calls, and links to **Policy**.

## Related tasks and reference

- [Export and apply a tool policy](https://docs.oz0.ai/govern/tool-policy/files)
- [Rules for plugin and remote functions](https://docs.oz0.ai/govern/tool-policy/plugin-functions)
- [Function conditions and action classes](https://docs.oz0.ai/govern/tool-policy/conditions)
- [Rules for taint, origins and sequences](https://docs.oz0.ai/govern/tool-policy/origins-and-sequences)
- [How the runtime enforces tool policy](https://docs.oz0.ai/govern/tool-policy/runtime)
- [Preview, review and lint tool policy changes](https://docs.oz0.ai/govern/tool-policy/review-changes)
- [Assign duties to tool calls](https://docs.oz0.ai/govern/tool-policy/duties)
- [Command rules in a tool policy](https://docs.oz0.ai/govern/tool-policy/command-rules)

<legacy-sections>
<span id="a-plugins-suggested-policy">

a plugins suggested policy

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/plugin-functions#a-plugins-suggested-policy).

<span id="a-rule-by-address">

a rule by address

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/conditions#a-rule-by-address).

<span id="action-classes">

action classes

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/conditions#action-classes).

<span id="approval-deadlines-and-escalation">

approval deadlines and escalation

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#approval-deadlines-and-escalation).

<span id="backtest-a-change">

backtest a change

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#backtest-a-change).

<span id="calls-that-carry-a-secret">

calls that carry a secret

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#calls-that-carry-a-secret).

<span id="command-rules">

command rules

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/command-rules#command-rules).

<span id="conditions-on-each-call">

conditions on each call

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/conditions#conditions-on-each-call).

<span id="duties">

duties

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/duties#duties).

<span id="hooks-come-before-the-rules">

hooks come before the rules

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#hooks-come-before-the-rules).

<span id="how-often-a-rule-will-ask">

how often a rule will ask

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#how-often-a-rule-will-ask).

<span id="label-presets">

label presets

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#label-presets).

<span id="no-audit-entry-no-call">

no audit entry no call

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#no-audit-entry-no-call).

<span id="one-plugins-functions">

one plugins functions

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/plugin-functions#one-plugins-functions).

<span id="remote-mcp-servers">

remote mcp servers

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/plugin-functions#remote-mcp-servers).

<span id="rules-by-caller">

rules by caller

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/conditions#rules-by-caller).

<span id="rules-from-plain-english">

rules from plain english

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#rules-from-plain-english).

<span id="sequence-rules">

sequence rules

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#sequence-rules).

<span id="shadow-mode-and-ends">

shadow mode and ends

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#shadow-mode-and-ends).

<span id="suggested-rules">

suggested rules

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#suggested-rules).

<span id="the-older-commands">

the older commands

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/files#the-older-commands).

<span id="the-policy-lint">

the policy lint

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/review-changes#the-policy-lint).

<span id="the-policys-version">

the policys version

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#the-policys-version).

<span id="the-switch-is-placed-by-the-server">

the switch is placed by the server

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/plugin-functions#the-switch-is-placed-by-the-server).

<span id="the-taint-default-rule">

the taint default rule

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#the-taint-default-rule).

<span id="the-whole-policy-as-a-file">

the whole policy as a file

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/files#the-whole-policy-as-a-file).

<span id="two-paths-for-a-plugins-functions">

two paths for a plugins functions

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/plugin-functions#two-paths-for-a-plugins-functions).

<span id="where-arguments-came-from">

where arguments came from

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/origins-and-sequences#where-arguments-came-from).

<span id="where-it-applies">

where it applies

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#where-it-applies).

<span id="words-the-policy-does-not-know">

words the policy does not know

</span>

: [Read the section](https://docs.oz0.ai/govern/tool-policy/runtime#words-the-policy-does-not-know).

</legacy-sections>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
