inputGuardrails / outputGuardrails right on the tool, next to approval and restartSafe.
send_email and never for any other tool.
Quick Start
1
Block a tool's arguments by rule
An input guardrail returns
[false, 'reason'] to block. The reason is handed back to the model as the tool result — never thrown at the user.2
Rewrite the arguments before the tool runs
Return
[true, newArgs] to rewrite. The rewritten object is what the tool receives.3
Substitute the result
An output guardrail sees the raw result before it re-enters the LLM context. Return
[true, newResult] to substitute, or [false, 'reason'] to block.4
Chain several guardrails
Pass an array — each guardrail’s allowed value feeds the next; the chain short-circuits on the first failure.
Verdict shapes accepted
A guardrail’s return value tells the runtime whether to allow, rewrite, or block.How per-tool guardrails run
Per-tool guardrails are the layer closest to the tool in both directions. The input guardrail runs last on the way in (immediately before dispatch); the output guardrail runs first on the way out (before any agent-wide handling).The order inside
FunctionTool.execute is restartSafe → approval → input guardrail → tool → output guardrail. Human approval stays upstream of every automated rewrite, so a reviewer audits exactly what the model proposed. The input guardrail runs immediately before dispatch, so “the tool never runs with arguments its own guardrail did not see” holds.Detecting a denial
A blocked call returns aToolGuardrailDenial object rather than throwing.
guardrail_denied: true, so it can pick different arguments, choose another tool, or explain to the user — never a stack trace.
Reusing an existing guardrail object
Any object exposingvalidateToolCall(toolName, args) or validateToolResult(toolName, result) — including an existing agent-wide ToolGuardrailChain — drops in as a single entry.
direction is passed through the base CallableToolGuardrail constructor rather than declared as a subclass readonly direction = INPUT field, because subclass field initializers run after super() and would leave the base constructor seeing undefined. You only hit this when subclassing CallableToolGuardrail directly.Per-tool vs agent-wide guardrails
Per-tool guardrails share the same machinery as agent-wide ones — a guardrail written for one scope drops into the other.Best Practices
Scope the guardrail to where the rule lives
Scope the guardrail to where the rule lives
When a rule belongs to one tool — “
send_email recipients must be on-domain” — declare it on that tool, not agent-wide. An agent-wide guardrail fires for every tool and has to branch on toolName.Prefer rewriting arguments over blocking
Prefer rewriting arguments over blocking
Returning
[true, sanitisedArgs] is safer than [false, reason] and hoping the model retries correctly. Fix the call rather than bouncing it back.Redact in the output guardrail rather than reject
Redact in the output guardrail rather than reject
When a result carries a secret, return
[true, redacted] so the model still gets a usable answer, instead of [false, ...] which throws the whole result away.Keep guardrail logic pure
Keep guardrail logic pure
A guardrail should inspect and transform its input with no side effects. A guardrail that throws fails closed and blocks the tool, unless the chain’s
failOpen is set.Related
Guardrails
Agent-wide input and output guardrails.
Approval
Human-in-the-loop sign-off before a tool runs.
Per-Tool Guardrails (Python)
The same feature in the Python SDK.

