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.
Use protect() for HTTP routes. Use the Mastra helpers on this page for tools,
processors, and hooks. Vercel AI SDK, LangChain, and Eve 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 }). Do not throw.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 / 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 HITL, not policy.
Never call createAgentContext inside a Mastra callback. Do not 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.
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],});requireApproval is not a policy gate
Section titled “requireApproval 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 }. Scan free-text args (a
note, reason, or body). An opaque orderId will not trip email / phone / card
/ IP, so do not 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 / 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, not policy. - Do not call
createAgentContextinside a Mastra callback. - Do not also wrap these tools with
@arcjet/guard/vercel-ai/v7. - Do not import
@arcjet/guard/mastra. The path is@arcjet/guard/mastra/v1.
Related
Section titled “Related”- Framework integrations
- Vercel Eve agent guard
- Agent guards
- Example: arcjet/examples#193
(
mastra-agent)