Mastra agent guard
Mastra agents run authored tools, processors, and MCP/workspace/toolset tools. Arcjet Guard sits at those boundaries so a remote policy can allow or deny the action before a side effect runs.
What is Arcjet?
Arcjet is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.Use protect() on HTTP routes. Use
the helpers on this page for agent tools and other actions that have no HTTP
request.
You need an Arcjet account and an ARCJET_KEY. Launch
one client at module scope and reuse it.
Framework wrappers take an action string such as email.sent. That slug
selects the matching remote policy
and names the event in the Arcjet Console. Direct guard() calls use the field
name label for the same slug. Don’t pass label to a wrapper such as
guardTool().
You can submit SDK rules in code, rely on a published remote policy, or combine both. For more information about the decision model, see Agent guards.
Vercel AI SDK, LangChain, Eve, and LangGraph wrappers are on Framework integrations.
Install
Section titled “Install”Install the Guard SDK and Mastra:
npm install @arcjet/guard @mastra/coreImport helpers from the versioned path @arcjet/guard/mastra/v1. There is no
unversioned alias. @arcjet/guard/mastra does not resolve. The Mastra
integration requires @mastra/core 1.x and Node.js 22 or later.
Launch one client at module scope:
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });Helpers
Section titled “Helpers”The integration exposes four surfaces:
guardTool()wraps a MastracreateTool({ execute }). OnDENYthe tool never runs. The model receives a structuredArcjetDenialResult({ arcjetDenied, reason, message, retryable, retryAfterSeconds? }). Don’t throw. A throw becomes a generic tool error and drops the fields.guardProcessor()is a MastraProcessorforinputProcessors/outputProcessors. OnDENY,processInput/processInputStepcallabort()(tripwire).processInputStepscreens later agentic steps so tool continuations cannot skip the inbound gate.guardHooks()providesbeforeToolCall/afterToolCallfor MCP / workspace or toolset tools that were not authored throughguardTool.beforeToolCallcan return{ proceed: false, output }onDENY.afterToolCallis observe-only.mastraAgentContext()readsMASTRA_THREAD_ID_KEY, then resource, thenworkflow.runId. It never mints an ID.
There is no guardInbound. Mastra channels already hit processInput. There
is no guardApproval. Mastra requireApproval is human-in-the-loop (HITL)
confirmation, not policy.
Never call createAgentContext inside a Mastra callback. Don’t also wrap
these tools with @arcjet/guard/vercel-ai/v7.
The Mastra integration skill ships with @arcjet/guard. After npm install,
copy or symlink it into your coding agent’s skills directory:
mkdir -p ~/.claude/skillscp -r node_modules/@arcjet/guard/skills/integrate-arcjet-guard-mastra ~/.claude/skills/Then ask the agent to add Arcjet Guard to this Mastra project. In Claude Code,
run /integrate-arcjet-guard-mastra. The skill source is
integrate-arcjet-guard-mastra.
Helper options
Section titled “Helper options”| Option | Helpers | Description |
|---|---|---|
action | All | Guard label and capture name. A string, or for hooks a function of { toolName }. |
rules | All | SDK rules, or a function of processor { text } or tool input. Omit to submit none. |
metadata | All | Nested JSON, or a function of the same input as rules. |
onGuardError | All | "deny" (default) or "allow". |
Screen inbound with guardProcessor
Section titled “Screen inbound with guardProcessor”Mastra channels already run through processInput. There is no guardInbound.
Screen for prompt injection with guardProcessor in inputProcessors.
On DENY, processInput / processInputStep call abort() and Mastra raises
a tripwire. Use a separate action name for outbound text on
outputProcessors. processInputStep screens later agentic steps so a tool
continuation cannot skip the inbound gate.
Helpers default to onGuardError: "deny". "allow" is a legitimate choice on
the inbound processor, because failing closed there stops the agent answering
during an outage.
import { Agent } from "@mastra/core/agent";import { guardProcessor } from "@arcjet/guard/mastra/v1";import { detectPromptInjection } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const inbound = guardProcessor(arcjet, { action: "message.received", rules: ({ text }) => [detectPromptInjection()(text)],});const outbound = guardProcessor(arcjet, { action: "message.completed", rules: ({ text }) => [detectPromptInjection()(text)],});
export const agent = new Agent({ id: "support-agent", name: "support-agent", instructions: "Help the user.", model: "openai/gpt-4o", inputProcessors: [inbound], outputProcessors: [outbound],});Human approval is not a policy gate
Section titled “Human approval is not a policy gate”Mastra requireApproval pauses for a human. It is not a remote policy. Use
guardTool for authored tools or guardHooks for unwrapped tools.
On DENY the tool’s execute never runs. The model receives
{ arcjetDenied: true, reason, message, retryable, retryAfterSeconds? }.
Don’t throw. A throw becomes a generic tool error and drops the fields. Scan
free-text args (a note, reason, or body). An opaque orderId doesn’t trip
email, phone, card, or IP detection, so don’t pass it to
localDetectSensitiveInfo.
import { createTool } from "@mastra/core/tools";import { z } from "zod";import { guardTool } from "@arcjet/guard/mastra/v1";import { tokenBucket, localDetectSensitiveInfo } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});// Factory then text – same shape as `detectPromptInjection()(text)`.const detectPii = localDetectSensitiveInfo();
export const lookupOrder = guardTool( arcjet, createTool({ id: "lookup-order", description: "Look up an order by ID", inputSchema: z.object({ orderId: z.string(), note: z.string(), }), async execute({ orderId, note }) { return { orderId, note, status: "shipped" }; }, }), { action: "order.looked-up", rules: (input) => [ lookupLimit({ key: input.orderId, requested: 1 }), detectPii(input.note), ], },);Hooks can deny unwrapped tools
Section titled “Hooks can deny unwrapped tools”Unlike Eve (observe-only hooks), Mastra beforeToolCall can stop a call. Use
guardHooks for MCP, workspace, or toolset tools you did not pass through
guardTool. Applying both to the same authored tool double-calls the guard.
beforeToolCall returns { proceed: false, output } on DENY so those tools
never execute. afterToolCall is observe-only.
import { guardHooks } from "@arcjet/guard/mastra/v1";import { tokenBucket } from "@arcjet/guard";import { arcjet } from "./arcjet.js";
const mcpLimit = tokenBucket({ bucket: "mcp-access", refillRate: 20, intervalSeconds: 60, maxTokens: 20,});
export const hooks = guardHooks(arcjet, { action: ({ toolName }) => `${toolName}.invoked`, rules: ({ toolName }) => [mcpLimit({ key: toolName, requested: 1 })],});Pass hooks to the Agent constructor (or to generate / stream).
Fail-closed default
Section titled “Fail-closed default”Mastra helpers default to onGuardError: "deny". If Guard cannot be evaluated,
the tool does not run and inbound processInput aborts the turn. "allow" is
a legitimate choice on the inbound processor because failing closed there
stops the agent answering during an outage.
Correlation
Section titled “Correlation”Set MASTRA_THREAD_ID_KEY / MASTRA_RESOURCE_ID_KEY on RequestContext
before generate / stream. mastraAgentContext() reads them. It never
calls createAgentContext. Preference order is thread, then resource, then
workflow.runId. If none is a valid 1-256 printable-ASCII string, the call is
uncorrelated rather than joined to a generated ID.
import { RequestContext, MASTRA_THREAD_ID_KEY, MASTRA_RESOURCE_ID_KEY,} from "@mastra/core/request-context";
const requestContext = new RequestContext();requestContext.set(MASTRA_THREAD_ID_KEY, conversationId);requestContext.set(MASTRA_RESOURCE_ID_KEY, userId);
await agent.generate(message, { requestContext });What not to use
Section titled “What not to use”- There is no
guardInbound. Channels already hitprocessInput. - There is no
guardApproval.requireApprovalis human HITL confirmation, not policy. - Don’t call
createAgentContextinside a Mastra callback. - Don’t also wrap these tools with
@arcjet/guard/vercel-ai/v7. - Don’t throw from
guardToolto signal a denial. Return the payload so the model can inspect it. - Don’t import
@arcjet/guard/mastra. The path is@arcjet/guard/mastra/v1.
Common patterns
Section titled “Common patterns”- Prompt injection on every step:
guardProcessorin bothinputProcessorsand, if you want later tool continuations screened, rely onprocessInputStep(the same processor object handles both). - Rate limit plus PII on a tool:
tokenBucketkeyed on a trusted ID, pluslocalDetectSensitiveInfo()on a free-textnote. - MCP tools you did not wrap:
guardHookswith anactionfunction of{ toolName }that returns a past-tense slug per tool.