Skip to content

Tool policies

A ToolBlock policy decides which tools the agent is allowed to call and which it can't. In the dashboard these rules live under a policy of type ToolBlock — see Policies for the general overview. This page covers how to write the patterns inside the policy.

Where you see it in the dashboard

When you create or edit a policy and pick ToolBlock as the type, the dashboard shows a JSON editor for the tool_pattern, blocked_tools, or tools array. The "Test pattern" preview at the bottom lets you paste a tool name and see whether any pattern matches — useful for debugging.

Policies list with the New policy button highlighted in the top right. Policies list with the New policy button highlighted in the top right.
Governance · Policies · New policy

What a tool name looks like

The agent calls tools by name. The canonical tool name format:

Type Format Example
Built-in tool lowercase string bash, file_write, execute_code
MCP tool mcp://{server}/{tool} mcp://filesystem/read
Custom tool custom:{name} custom:my_tool

The policy matcher is name-based. The SDK sends the tool name to the gate, the gate checks it against every active ToolBlock policy, and the verdict comes back as allow, block, or require_approval.

How to write the patterns

Each entry in a ToolBlock policy is one of:

  • Exact name — "stripe.charge" blocks only that one tool.
  • Glob — "send_*" blocks anything starting with send_.
  • * alone — blocks everything.

Each entry is capped at 4096 bytes. The cap exists because the matcher scans every pattern on every gate call — a 10 MB pattern would burn CPU on each call.

The matcher runs case-insensitively against the canonical tool name.

What a ToolBlock policy does

A ToolBlock policy blocks tool calls whose name matches one of its patterns. There is no action = require_approval field on a ToolBlock — for "I want a human to approve before this tool runs", create an approval rule instead (see Human approval). The two are separate rule objects; ToolBlock and approval rules don't share a configuration schema.

ToolBlock is always Hard: it never lets through, regardless of the budget's enforcement_mode. See Reliability matrix. If the gate cannot evaluate the ToolBlock check (Redis or policy cache unavailable), it fails closed — 403 TOOL_BLOCKED (SDK error_code = "NR-T001"). The agent never runs an unverified sensitive operation.

ToolBlock and approval rules are distinct rule objects — they don't share a configuration schema. A ToolBlock policy always blocks; to require human review first, use an approval rule instead.

A worked example

Suppose your agent has these tools: tavily_search, send_email, db.write, db.drop, stripe.charge, read_file.

You want to:

  • Allow read-only operations (tavily_search, read_file)
  • Block destructive operations (db.drop, stripe.charge)
  • Require approval for any outbound communication (send_email)
  • Allow normal DB writes (db.write) but block db.drop

Two ToolBlock policies and one approval rule:

The example below shows the request payload shape — what you POST to the dashboard / API. It's not the storage schema, just the public input format.

tool_block_policy.json
{
  "policies": [
    {
      "name": "Block destructive",
      "type": "ToolBlock",
      "scope": "Org",
      "config": {
        "tool_pattern": ["db.drop", "stripe.*"]
      }
    }
  ],
  "approval_rules": [
    {
      "name": "Outbound needs approval",
      "tool_patterns": ["send_email"],
      "action_label": "send email to customer",
      "expires_in_seconds": 300
    }
  ]
}

send_email now triggers the approval flow (a human clicks Approve in the dashboard before the call goes through). db.drop and stripe.charge are blocked outright. The two policies do not interact — ToolBlock checks and approval-rule checks are independent paths in the gate.

Validation at policy creation

The dashboard rejects invalid patterns at save time:

Error Cause Fix
400 bare_string_pattern "pattern": "send_*" instead of "pattern": ["send_*"] Always use an array, even for one entry
400 pattern_too_long An entry longer than the per-pattern byte cap Split into multiple patterns
400 invalid_glob Contains control characters Remove \n, \r, \t

Plan gating

ToolBlock policies require the Custom Policies feature, which is on Growth+ plans. Lite and Starter can have BudgetLimit and RateLimit policies, but not ToolBlock.

Approval rules (separate from ToolBlock) are gated by the approvals plan feature. The per-tier cap matches:

Plan Approval rules allowed Why
Lite 0 approvals feature disabled
Starter 0 approvals feature disabled (server-side invariant: approval_rules > 0 ⇒ approvals == true)
Growth 20 approvals enabled
Scale unlimited approvals enabled, no cap
Enterprise unlimited approvals enabled, no cap

The gate enforces approval_rules = 0 server-side on Lite and Starter — even if you mint a key on a higher tier and downgrade, existing rules are kept (for audit) but no new rule can be created until the plan is upgraded back.

On Lite / Starter, the dashboard shows ToolBlock policy creation greyed out with an "Upgrade" link.

How to debug a block you didn't expect

If your agent reports error_code = "NR-T001" (wire TOOL_BLOCKED) on a call you think should be allowed:

  1. Open the workflow in the dashboard.
  2. Click Effective policy. The merged set shows every ToolBlock pattern that could match.
  3. Open the Audit log and filter by decision = block and the tool name. The log shows which pattern matched.
  4. If a pattern is too broad (* matches everything), narrow it in the policy editor.
  5. If the pattern is wrong entirely, deactivate the policy and re-create it with the correct list.

See also

  • Policies — the dashboard view, aggregation rules, and most-restrictive-wins semantics
  • Sensitive tools — the policy-driven way to express "this tool needs review" (not a built-in SDK list)
  • Tool catalog — common tool names with risk ratings
  • Human approval — the approval-rule path, distinct from ToolBlock

Deep dive

A ToolBlock check operates on the canonical tool name and nothing else. The gate resolves the effective tool list from the request before evaluating anything, so a call carrying one tool and a call carrying a list are treated alike. Patterns are collected from the tool_pattern, blocked_tools and tools keys, trimmed and de-duplicated, so "bash" and "bash " collapse into one entry. Matching runs case-insensitively against the canonical name, and control characters in either the incoming tool name or a pattern are rejected before the check runs.

The pattern language is small. * is the only metacharacter: it matches any run of characters, so send_* matches anything starting with send_ and bash.* matches the bare name bash as well as dotted continuations like bash.foo and bash.foo.bar. A pattern with several stars, such as *.drop_*, matches when every literal segment appears in order, which is why *.drop_* matches s3.drop_table but not s3.execute_drop. | collapses several alternatives into a single entry. Neither ? nor ** is supported — both are matched as ordinary characters — and a bare literal matches only itself, never a substring. Each entry is capped at 4096 bytes, bounding the work the matcher does on every gate call.

ToolBlock is always hard, independent of a budget's enforcement mode, and it is evaluated ahead of rate-limit and budget accounting, so a blocked call never receives a budget envelope. If the check cannot be evaluated at all, the gate fails closed. A block reports the pattern that fired, which is what the audit log records, so an unexpected block can always be traced to one entry.

Validation at creation time rejects the configurations that would otherwise mean "block nothing": an empty string, an empty array, an empty config object, and the plural tool_patterns key. A typo such as block_tools or blocked_tool is an error rather than a silently ignored rule. Because * on its own blocks everything, narrowing a policy is the operator's responsibility, and the merged effective set is what the dashboard shows before a change ships. Matching never inspects tool arguments: a rule is about which tool runs, not what it is called with.