The short answer
In the OpenAI Agents SDK for Python, a tool input guardrail runs before a function tool executes and can skip the call, replace its output with a message, or raise a tripwire. A tool output guardrail runs after the tool and can replace the output or raise a tripwire.1 You attach them with tool_input_guardrails and tool_output_guardrails, on a single tool or on a local MCP server, where they apply to every tool the server exposes.1
They don’t cover everything. The docs list what they skip: handoffs, hosted tools including HostedMCPTool, built-in execution tools including ShellTool and ComputerTool, and agents used as tools.1 If your agent can run shell commands or use a hosted MCP server, those calls need a different control.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
Three kinds of guardrail
The SDK uses the word guardrail for three different checks, and they run at different moments.
| Guardrail | Runs on | When | Can stop a tool call? |
|---|---|---|---|
| Agent input guardrail | The user’s input, first agent only | In parallel with the agent by default | Not reliably: tools may already have run1 |
| Agent output guardrail | The final output, last agent only | After the agent finishes | No; the tools already ran1 |
| Tool input guardrail | One tool call’s arguments | Before that call executes | Yes1 |
| Tool output guardrail | One tool call’s result | After that call | No, but it can replace what the model sees1 |
The first row is the trap. By default, agent input guardrails run concurrently with the agent, so the agent may already have consumed tokens or run tools before a tripwire cancels it. Setting run_in_parallel=False makes them finish before the agent starts.1 If a guardrail is meant to stop something, it must finish before the thing it stops.
For tool calls, the tool input guardrail is the one that matters. It sees the tool’s name and arguments and runs before the side effect.
A tool input guardrail on an MCP server
Local MCP servers (MCPServerStdio, MCPServerStreamableHttp, and the older SSE transport) accept guardrails directly, and the SDK attaches them to every MCP tool that remains after filtering.2 Here is a guardrail that refuses SQL that drops or truncates, attached to a database MCP server:
Code Program code, for developers. The text around it says what it does.
import json, re
from agents import Agent, Runner, ToolGuardrailFunctionOutput, tool_input_guardrail
from agents.mcp import MCPServerStdio
DESTRUCTIVE = re.compile(r"\b(drop|truncate)\b", re.I)
@tool_input_guardrail
def no_drop_or_truncate(data):
args = json.loads(data.context.tool_arguments or "{}")
sql = str(args.get("sql") or args.get("query") or "")
if DESTRUCTIVE.search(sql):
return ToolGuardrailFunctionOutput.reject_content(
"DROP and TRUNCATE are not allowed from this agent. Ask the user to run it."
)
return ToolGuardrailFunctionOutput.allow()
async def main():
async with MCPServerStdio(
name="db",
params={"command": "npx", "args": ["-y", "your-postgres-mcp-server"]},
tool_input_guardrails=[no_drop_or_truncate],
) as server:
agent = Agent(name="orders", mcp_servers=[server])
print((await Runner.run(agent, "Clean up the orders table")).final_output)
The shape follows the SDK’s own examples: parse data.context.tool_arguments, return reject_content(...) to refuse with a message the model reads, or allow().2
Two things to know about this pattern. The regular expression is a placeholder: DROP inside a comment trips it, and SQL split across separate calls passes it. A real check parses the SQL. And reject_content returns a message to the model, which will usually try something else. That is what you want for a refusal, but log it, because a model that keeps trying is worth knowing about.
The tools guardrails don’t see
This list, from the SDK’s guardrails page, is the most useful thing to check against your own agent.1
| Tool or path | Tool guardrails apply? | Why |
|---|---|---|
Function tools (@tool, function_tool) |
Yes | The guarded pipeline |
| Local MCP server tools, with server-wide guardrails | Yes | Attached to every remaining tool2 |
HostedMCPTool |
No | The Responses API executes it2 |
WebSearchTool, FileSearchTool, CodeInterpreterTool, ImageGenerationTool |
No | Hosted tools |
ShellTool, LocalShellTool, ComputerTool, ApplyPatchTool |
No | Built-in execution tools |
| Handoffs | No | A separate pipeline |
Agent.as_tool() |
No | No guardrail options today |
The shell row deserves attention. An agent with ShellTool can run psql with any SQL in it, and no tool guardrail sees that call. The same goes for ApplyPatchTool writing files. If you give an agent those tools, the control has to live outside the SDK: in the sandbox, the operating system user, or the credentials the shell can reach.
Hosted MCP is the other gap. Because OpenAI’s infrastructure makes the call, your process never gets a chance to inspect it. What you do get is approval: HostedMCPTool takes require_approval in its tool_config, with an on_approval_request callback for deciding in Python.2
Guardrails, filters and approvals together
The SDK offers three controls for MCP tools, and they combine well.
- Filters decide which tools exist.
create_static_tool_filtertakes an allow list and a block list, applying the allow list first. A callable filter can decide per agent.2 A tool that isn’t listed can’t be called, which beats any check on its arguments. - Approvals decide whether a person must say yes. Local servers take
require_approval: always, never, a per-tool map, or a callable. The callable doesn’t receive the call’s arguments, so it can only decide by tool name.2 - Guardrails decide on the arguments. They are the only one of the three that sees what the call will actually do.
Order matters too. For a tool that needs approval, input guardrails normally run after the approval, just before execution. ToolExecutionConfig(pre_approval_tool_input_guardrails=True) runs them before the approval prompt, and the call is still rechecked after.1 Running them first saves a person from approving a call that would have been refused anyway.
A checklist for an Agents SDK app
- List every tool the agent has, and mark which rows of the table above they fall in.
- Filter MCP tools down to the ones the task needs.2
- Put tool input guardrails on each local MCP server that can write.2
- Require approval for writes, by tool name, and run guardrails before approval.1
- Set
run_in_parallel=Falseon any agent input guardrail meant to block.1 - Treat
ShellTooland hosted tools as unguarded and limit them some other way.1 - Keep a record of refusals and approvals outside the model’s context, so you can review them later.
Where an outside gate fits
Guardrails live inside your Python process, next to the agent. That is convenient, and it means anything that runs outside that pipeline, or a second agent built without your guardrails, goes unchecked. An outside gate sits between the agent and the system it writes to, so every client meets the same rules.
We build one, Deedglass. Its docs carry recipes for nine agent frameworks, including the OpenAI Agents SDK, each a runnable file of 15 lines or fewer with pinned packages; no live model run backs them.3 The OpenAI recipe hands the agent Deedglass’s MCP server through MCPServerStdio, so its calls meet a tool-call policy and risky ones wait for a person’s approval. It is the same local-MCP path as the guardrail above, so it has the same gaps: it doesn’t see ShellTool or hosted MCP calls. For approvals on MCP tool calls in general, see our approvals post; for checks on the tool list itself, tool poisoning. To try the checks without installing anything, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.