Sensitive tools
A sensitive tool is one that should never run without a human paying attention. Sending an email, moving money, deleting a record — all of these have consequences the agent can't easily undo.
The way to express "this tool needs review" in NullRun is a
ToolBlock policy — a glob pattern that fails
the gate at /gate evaluation time, regardless of the SDK's local
context. This page covers the recommended patterns and how to wire
them.
@sensitive vs ToolBlock
Two distinct mechanisms, often confused:
@sensitive(SDK-side) — a parameterless decorator that marks a function so its kwargs are extracted into theBusinessImpactpredicate bag. Used with approval rules that evaluate a typed predicate. Affects the SDK only; the gate still has the final say.ToolBlock(server-side) — a policy rule evaluated by the gate on every/gatecall. The gate fails-CLOSED if it cannot reach Redis or the policy cache to evaluate. This is the canonical "this tool is forbidden" mechanism.
Use @sensitive when you want a typed BusinessImpact approval
flow (e.g. "refunds over $500 need approval"). Use ToolBlock
when you want a hard rule ("never call bash").
What a sensitive tool is, in policy terms
There is no built-in tool catalogue shipped by the SDK — every
enforcement decision is evaluated by the gate on every /gate call,
so you can't accidentally miss a tool you didn't register locally.
You express "sensitive" with one of two complementary mechanisms:
@sensitive(SDK-side) — marks a function so its kwargs flow into theBusinessImpactpredicate bag used by approval rules (typedmoney_amount/tool_parameters). Use this when you want a typed predicate — e.g. "refunds over $500 need approval".ToolBlock(server-side) — a policy rule evaluated by the gate. The gate fails-CLOSED if it cannot reach Redis or the policy cache to evaluate. Use this for hard rules — "never callbash".
For the typed predicate wiring, see
Decorators & extractors → money_outflow(...)
and tool_params(...).
Recommended starter patterns (see Tool catalog → Recommended ToolBlock starter list for the maintained list):
| Category | Pattern examples |
|---|---|
| Money | mcp://payments/refund*, mcp://stripe/charge, mcp://stripe/refund |
| Email & messaging | mcp://gmail/send, mcp://slack/post, send_email |
| Database destructive | mcp://postgres/drop_table, mcp://postgres/delete_row, execute_sql |
| External API writes | mcp://*/post, mcp://*/put, mcp://*/delete |
| Files & storage | mcp://s3/delete, file_delete, bash |
| Admin | mcp://admin/delete_user, mcp://admin/disable_user |
These are the canonical tool names a policy matches against. The exact name comes from your MCP server / framework integration — see the tool catalog for the curated list with risk ratings.
Why the SDK does not ship a built-in list
A built-in "sensitive tools" SDK list would force every framework to register its tools against NullRun's expectations — and would be silently wrong for any tool not on the list. The current model inverts this:
- You write a ToolBlock policy that names the tools you care about.
- The policy is evaluated server-side on every
/gatecall. - The decision is returned to the SDK as
TOOL_BLOCKED(403); the SDK raisesNullRunToolBlockedErrorwitherror_code = "NR-T001".
The gate does not inspect tool arguments — it cannot distinguish
two calls to the same tool by payload. If you want a narrower rule
(e.g. "block refunds over $500"), use a typed BusinessImpact
predicate: the SDK extracts the argument bag and the gate evaluates
a DNF of up to 5 named parameters against Equals / OneOf /
NumericRange / Regex / Exists matchers. See
Human approval → typed predicates.
Why ToolBlock is enforced at the gate
ToolBlock is enforced at the gate: sensitive operations never run
when the policy engine is unreachable. If the gate returns
403 TOOL_BLOCKED (SDK error_code = "NR-T001"), the SDK raises
before your function body executes. ToolBlock is always Hard,
regardless of the budget's enforcement_mode.
What's NOT in a ToolBlock policy
A ToolBlock policy matches tool name only — not:
- prompt content or semantic intent
- the recipient of a payment (use a typed predicate instead)
- tool arguments beyond the
BusinessImpactextraction - the tool's runtime sandbox (that's your infrastructure concern)
Read operations are never sensitive regardless of the tool. The canonical name alone decides.
Where the sensitive list lives
You write the policy in the dashboard under Policies (sidebar under Governance). Click New policy, pick Tool block as the policy type, and the modal shows the Tool pattern field where you enter the glob(s). The dashboard shows you the canonical tool name for every framework integration. Your policy applies to:
- All workflows under the org (default)
- A specific workflow (scope to
workflow_id) - A specific API key (scope to
api_key_id)
Per the aggregation rules: ToolBlock patterns union across applicable policies — every pattern that matches fires.
Audit trail
When a sensitive tool is blocked, the audit log records the
block with reason TOOL_BLOCKED (SDK error_code = "NR-T001"),
the pattern that matched, and the workflow + api_key + tool_name.
The audit log is hash-chained — see
Audit records.
This gives you a complete audit trail of every blocked attempt,
regardless of whether the block came from your policy or from the
default TOOL_BLOCKED rejection of an unknown tool name.
For sensitive tools you want to allow after explicit human review,
pair them with an approval rule instead of removing them from
the blocking surface. The approval row in the dashboard gives you
the audit trail, and the SHA-256 action_digest ensures the grant
is bound to the exact action payload the SDK sent on /gate. See
Human approval.
See also
- Decorators & extractors →
@sensitive— the two@sensitiveforms (bare + factory), themoney_outflow(...)/tool_params(...)impact extractors, and the_nullrun_extractorcontract that ties them to/execute - Tool policies — the actual rule structure
- Tool catalog — recommended patterns with risk ratings
- Human approval — the safer alternative to disabling a ToolBlock rule
- Circuit breaker → fail-CLOSED matrix