---
title: "Harness tool controls"
description: "Apply hooks, policy and approval to a harness session."
canonical_url: "https://docs.oz0.ai/build/parts/harnesses/tool-controls"
---
# Harness tool controls

> Apply hooks, policy and approval to a harness session.

For: plugin authors.

Apply hooks, policy and approval to a harness session.

## Hooks and approvals for the session's tools

Every tool call in a Claude harness session asks first. The session can use only the tools in `harness.tools`, and each call runs only when the job allows it:

- [Hooks](https://docs.oz0.ai/build/parts/hooks) see it as a `pre_tool_call` named `<harness>.<tool>`, such as `claude-agent-sdk.Bash`, with the tool's input as `arguments`. They allow it, deny it, change its input or ask a person, as for any tool call.
- Tools in `harness.requires_approval` wait for a person's [approval](https://docs.oz0.ai/agents/approvals) of each call, once the hooks have let it through:

```yaml [agents/acme-coder.yaml]
harness:
  tools: [Read, Glob, Grep, Bash]
  requires_approval: [Bash]          # someone approves every command
```

Default-deny. A call to a tool the agent does not list in `harness.tools` (or, when it lists none, that its harness does not list as its default `tools:`) is refused on the spot, whatever the session's own settings say: the agent reads `WebFetch is not among the tools this agent declares`. An agent with `oz.ask_user` in its `tools:` may also [ask people questions](https://docs.oz0.ai/agents/asking-people) from the session.

A call that no hook matches and that needs no approval runs at once. The others go to the job's workflow, through the edge, so a call can wait days for a person without the session losing its place. A refused call is not made: the agent reads why, as the SDK reports a denied tool.

### Which harnesses ask before a shell command

A hook, and the tenant's [command rules](https://docs.oz0.ai/govern/command-rules), see a shell command only when the harness asks before it runs it. The command rules, on by default on every node with nothing installed, send catastrophic commands such as `rm -rf ~` to a person; they, and the [guardrails plugin's command guard](https://docs.oz0.ai/build/plugins/verified/guardrails/commands#the-command-guard), read only those:

<table>
<thead>
  <tr>
    <th>
      Harness
    </th>
    
    <th>
      Asks before a shell command
    </th>
    
    <th>
      Named in hooks
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Claude Agent SDK (<code>
        harness-claude
      </code>
      
      )
    </td>
    
    <td>
      Every <code>
        Bash
      </code>
      
       call
    </td>
    
    <td>
      <code>
        claude-agent-sdk.Bash
      </code>
      
      , with the command in <code>
        arguments.command
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Claude Managed Agents (<code>
        remote-claude-managed
      </code>
      
      )
    </td>
    
    <td>
      Every <code>
        bash
      </code>
      
       call
    </td>
    
    <td>
      <code>
        claude-managed.bash
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      goose (<code>
        approve
      </code>
      
       mode) and OpenCode (<code>
        "*": "ask"
      </code>
      
      )
    </td>
    
    <td>
      Every call
    </td>
    
    <td>
      <code>
        goose.execute
      </code>
      
      , <code>
        opencode.execute
      </code>
      
      , with the agent's input and the client terminals' <code>
        command
      </code>
      
      , <code>
        args
      </code>
      
       and <code>
        cwd
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Codex (<code>
        read-only
      </code>
      
       mode)
    </td>
    
    <td>
      Only commands that need more than its read-only sandbox
    </td>
    
    <td>
      <code>
        codex.execute
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Gemini CLI
    </td>
    
    <td>
      Shell commands, but not its read-only tools
    </td>
    
    <td>
      <code>
        gemini.execute
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A harness of your own
    </td>
    
    <td>
      Only with <code>
        permissions: true
      </code>
      
      , for every call
    </td>
    
    <td>
      <code>
        &lt;harness>.&lt;tool>
      </code>
    </td>
  </tr>
</tbody>
</table>

A Claude session's calls of an imported plugin's MCP servers (its install's harness extras) are not the harness's own tools: each is asked of the job and decided as the plugin's function it names ([tool policy](https://docs.oz0.ai/govern/tool-policy)). Claude's tools that read MCP servers' resources are refused in every session, whatever the agent declares.

So Codex's sandboxed commands, Gemini CLI's read-only tools and every call of a harness without `permissions` run without a hook or a command rule seeing them: those are checks, and the [sandbox](https://docs.oz0.ai/concepts/security/sandbox-tiers#sandboxes) is what bounds such a session.

The [Plugin improver](https://docs.oz0.ai/build/plugins/verified/plugin-improver), an agent that proposes reviewed, evidence-backed changes to your plugins, runs only on harnesses that ask before every call: the Claude Agent SDK (`improver`), OpenCode and goose (`improver-opencode`, `improver-goose`); its `PLUGIN_IMPROVER_HARNESS` setting picks one. Codex and Gemini CLI declare `cannot_ask`, so they run no routine fire and no shadow run, and the improver does not offer them. Its agents have a shell to run the target's own tests (`Bash`; on ACP, `execute`, and no `fetch`), so every session of theirs runs on sandbox tier 1, whoever started the job, and the tenant's [command rules](https://docs.oz0.ai/govern/command-rules) decide each command.

A node a tenant admin opted out of the tier runs them at tier 0, which the job's page and the changeset's card say ("the session ran at sandbox tier 0"): keep the improver off such nodes with its install's node selector.

<warning>

Sessions run in a sandbox on nodes with [sandbox tier 1](https://docs.oz0.ai/concepts/security/sandbox-tiers#sandboxes): Linux with bubblewrap, or macOS. Hooks and approvals decide whether a call may run. The sandbox then limits its access: the session writes only to its work and state directories, sees nothing else on the node, and reaches only hosts allowed by the tenant, plugin and agent `egress:` rules.

Give the agent only tools its task needs. The platform requires tier 1 by default if the agent lists `Bash`, `Write` or `Edit`, leaves the tool list empty (which includes them by default), has an ACP `execute` tool, or uses a harness the platform cannot check. This applies whoever starts the job. Without an eligible node, the job fails after 10 minutes and names the remedies: provide a tier-1 node or use a node's [sandbox opt-out](https://docs.oz0.ai/fleet/nodes#the-sandbox-opt-out).

An agent limited to read tools (`Read`, `Glob`, `Grep`) can run on any node. A [dev server](https://docs.oz0.ai/fleet/dev-mode#no-sandbox-floor) permits these sessions on any node and reports that fact.

A [webhook-started flow](https://docs.oz0.ai/build/parts/flows#start-one-from-a-webhook) can reach an agent with `Bash`, `Write` or `Edit` only when its session runs in tier 1 without the plugin's secrets. An opted-out node cannot run that session. A harness-plugin secret [bound to a host](https://docs.oz0.ai/admin/credential-injection) appears in the session as a placeholder; the sandbox inserts its value only into requests to that host.

</warning>

A session that fails fails the job; it is not retried. Inside the session, the Claude Agent SDK retries a failed model call itself, up to four times, so a call reaches the provider at most five times, as for Pydantic AI agents. A session lost with its node is another matter: see below.


## Sitemap

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