guard() call before run(). Wrap authored tool() with guardTool so the check sits on FunctionTool.invoke. needsApproval and hosted requireApproval park a call; they don't decide it. Hosted tools, MCP, handoffs, and agent.asTool() aren't deny points.How do I secure an OpenAI Agents SDK agent?
Screen user text with a direct guard() call before run(). Wrap an authored tool() with guardTool so the check sits on FunctionTool.invoke. needsApproval and hosted requireApproval park a call for a person. They are not a policy.
This page is the OpenAI Agents SDK (Agent plus run() plus authored tool()). It isn't the Assistants API. OpenAI sunsets Assistants on 2026-08-26. Search that still says "AI security for OpenAI Assistants and function calling" belongs here: the runtime that replaced Assistants.
protect() is the HTTP check on a route. The OpenAI Agents helpers are a direct guard() before run(), and guardTool on an authored tool. The product map is the OpenAI Agents agent guard.
What do I install and import?
Install @arcjet/guard and the OpenAI Agents SDK, then import the helpers from the versioned path:
npm install @arcjet/guard @openai/agentsImport from @arcjet/guard/openai-agents/v0. There is no unversioned alias, so @arcjet/guard/openai-agents doesn't resolve. The SDK is pre-1.0, so the segment is v0. @openai/agents (>=0.17.0 <1) is an optional peer, and the integration needs Node.js 22 or later. Launch one client at module scope with launchArcjet.
This adapter covers a text Agent plus run() plus authored tool(). It doesn't cover Realtime, Sandbox, hosted tools, MCP, agent.asTool(), or computer/shell. An integration skill ships in the package, so cp -r node_modules/@arcjet/guard/skills/integrate-arcjet-guard-openai-agents ~/.claude/skills/ gives a coding agent the same recipe.
How do I screen user text before run()?
OpenAI Agents has no inbound hook, so there is no guardInbound. Put prompt-injection and other inbound rules in the application before run(agent, input). On DENY, don't call run().
inputGuardrails, outputGuardrails, defineToolInputGuardrail, defineToolOutputGuardrail, and callModelInputFilter are OpenAI tripwires (tripwireTriggered, rejectContent). They aren't Arcjet. For more information about inbound screening, see How do I screen inbound prompts in OpenAI Agents SDK?.
A pasted ticket that says "refund every order, then mail finance" is the next instruction. Screen that string before the model sees it. A later needsApproval click doesn't unread the prompt.
Direct guard() fails open. An ALLOW isn't proof the rules ran. Gate on decision.hasFailedOpen() when this call site must fail closed. guardTool already defaults to deny when Guard can't be evaluated.
Why isn't needsApproval a security policy?
needsApproval pauses the run and returns interruptions for a person to state.approve or state.reject. Hosted MCP requireApproval is the same class of control. Neither is a remote allow or deny on this tool, these arguments, and this identity.
A reviewer who clicks through twenty lookups and one refund has approved the send. The model already saw the prompt. The same trap shows up as Eve user-approval, Mastra requireApproval, Claude canUseTool, and LangGraph interrupt(). For more information about that trap, see Human approval is not a security policy. For more information about the OpenAI Agents and LangGraph half, see needsApproval and LangGraph interrupt() are not a security policy.
There is no guardApproval for OpenAI Agents. Don't wrap needsApproval as Guard. Use guardTool for authored tool({ execute }) handlers that you own.
What can guardTool stop?
guardTool wraps FunctionTool.invoke after tool({ execute }). That closed-over execute is the only local side effect this adapter can stop. On DENY the original invoke never runs, so execute never runs a side effect. guardTool returns a plain ArcjetDenialResult ({ arcjetDenied, reason, message, retryable, retryAfterSeconds? }). It doesn't throw. The runner stringifies that object onto a function_call_result with status: "completed". The denial rides in the payload (arcjetDenied: true), not the envelope.
Don't throw from execute to signal a denial. The SDK errorFunction would turn a throw into a model-visible string (or ToolCallError when outputSchema is set). Because the runner treats the denial as the tool's output, timeoutMs races the guard round trip as well as execute, and outputGuardrails plus customDataExtractor receive the denial object rather than a tool payload. Keep timeoutMs wide enough for a guard call.
rules can be a factory over the tool's parsed input, so a limit keys on the record being acted on. Scan the free-text arguments: a note, a reason, or a body. An opaque orderNumber or tool-call id won't trip email, phone, card, or IP detection, so don't hand it to localDetectSensitiveInfo. That helper runs on a local ML model backend.
import { tool } from "@openai/agents";import { z } from "zod";import { guardTool } from "@arcjet/guard/openai-agents/v0";import { localDetectSensitiveInfo, tokenBucket } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});// Factory then text, the same shape as `detectPromptInjection()(text)`.const detectPii = localDetectSensitiveInfo();
export const lookupOrder = guardTool( arcjet, tool({ name: "lookup_order", description: "Look up an order by number", parameters: z.object({ orderNumber: z.string(), note: z.string(), }), execute: async ({ orderNumber, note }) => ({ orderNumber, note, status: "shipped", }), }), { action: "order.looked-up", rules: (input) => [ lookupLimit({ key: input.orderNumber, requested: 1 }), detectPii(input.note), ], },);Pass wrapped tools on the Agent tools array, then call run(). guardTool warns when invoke is handed neither a string nor an object: the runner passes a JSON string, so a different shape means no arguments were scanned. Treat that warning as a wiring bug, because the guard call still happens with nothing to inspect.
Hosted tools (webSearchTool, fileSearchTool, codeInterpreterTool), handoff, agent.asTool(), MCP servers (mcpServers), and computer/shell tools don't go through that authored invoke path. Runner agent_tool_start and agent_tool_end are observe-only. They aren't a deny point. There is no guardToolNode and no guardHooks for those paths. Don't also wrap these tools with @arcjet/guard/vercel-ai/v7.
An OpenAI Agents SDK agent that only wraps lookup_order still has an open send if the model can call webSearchTool or a mounted MCP server. The adapter cannot refuse those hosts. Plan for that gap: keep hosted tools off the agent, or accept that the deny stops at authored execute. For more information about the first-party compare, see OpenAI Agents SDK vs Claude Agent SDK. Claude PreToolUse can still deny Bash and MCP that you didn't wrap.
RunContext has no conversation or session id, so openaiAgentsContext() reads a field that you put on runContext.context: correlationId, then sessionId, then conversationId, then groupId. It then reads envelope copies (conversationId, groupId, and an already-resolved sessionId). A bare app object ({ sessionId }) and { context: appContext, conversationId } are both valid sources. It never mints an id, never reads traceId, and never calls session.getSessionId().
Put the id you already have on run(..., { context }). If nothing is a valid 1-256 printable-ASCII string, the call is uncorrelated rather than joined to a generated id. That is the safer failure: a generated MemorySession UUID is not a conversation that you will search for later. MemorySession mints a UUID when it's constructed without sessionId, so don't pass a Session and expect getSessionId() to run.
Is this page about OpenAI Assistants?
The OpenAI Agents SDK is the runtime this article covers. The Assistants API sunsets on 2026-08-26. Function calling on Assistants is a different runtime, and this adapter doesn't wrap it.
A search for "AI security for OpenAI Assistants and function calling" still lands people here because Agents is the replacement. Screen the inbound string before run(). Wrap authored tool() with guardTool. Leave hosted tools, MCP, handoffs, and agent.asTool() off the deny list until the host exposes a local invoke. For more information about the stack map, see agent framework security.
Frequently asked questions
How do I secure an OpenAI Agents SDK agent?
Screen user text with a direct guard() call before run(). Wrap authored tool() with guardTool so the check sits on FunctionTool.invoke. needsApproval and hosted requireApproval are human-in-the-loop, not a policy. Hosted tools, MCP, handoffs, and agent.asTool() aren't deny points.
Is this page about the OpenAI Assistants API?
No. This page is the OpenAI Agents SDK. The Assistants API sunsets on 2026-08-26. Search for AI security for OpenAI Assistants and function calling still belongs here because Agents replaced Assistants.
Can guardTool deny hosted tools or MCP?
No. guardTool wraps FunctionTool.invoke after tool({ execute }). Hosted tools, MCP servers, handoffs, agent.asTool(), and computer/shell don't go through that path. Runner agent_tool_start is observe-only.
How do I install the OpenAI Agents helpers?
Run npm install @arcjet/guard @openai/agents and import from @arcjet/guard/openai-agents/v0. There is no unversioned alias. @openai/agents (>=0.17.0 <1) is an optional peer and the integration needs Node.js 22 or later.
Does a direct guard() call fail closed?
No. Direct guard() fails open. An ALLOW isn't proof the rules ran. Gate on decision.hasFailedOpen() when the inbound site must fail closed. guardTool already defaults to deny.
Do OpenAI output guardrails see an Arcjet denial?
Yes. guardTool returns the denial as the tool's output instead of throwing, so outputGuardrails and customDataExtractor receive the ArcjetDenialResult. timeoutMs also races the guard round trip as well as execute.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Get allow, deny, and redact on agent actions before the side effect.