Skip to content

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 the Guard SDK and Mastra:

Terminal window
npm install @arcjet/guard @mastra/core

Import 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! });

The integration exposes four surfaces:

  • guardTool() wraps a Mastra createTool({ execute }). On DENY the tool never runs. The model receives a structured ArcjetDenialResult ({ arcjetDenied, reason, message, retryable, retryAfterSeconds? }). Don’t throw. A throw becomes a generic tool error and drops the fields.
  • guardProcessor() is a Mastra Processor for inputProcessors / outputProcessors. On DENY, processInput / processInputStep call abort() (tripwire). processInputStep screens later agentic steps so tool continuations cannot skip the inbound gate.
  • guardHooks() provides beforeToolCall / afterToolCall for MCP / workspace or toolset tools that were not authored through guardTool. beforeToolCall can return { proceed: false, output } on DENY. afterToolCall is observe-only.
  • mastraAgentContext() reads MASTRA_THREAD_ID_KEY, then resource, then workflow.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:

Terminal window
mkdir -p ~/.claude/skills
cp -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.

OptionHelpersDescription
actionAllGuard label and capture name. A string, or for hooks a function of { toolName }.
rulesAllSDK rules, or a function of processor { text } or tool input. Omit to submit none.
metadataAllNested JSON, or a function of the same input as rules.
onGuardErrorAll"deny" (default) or "allow".

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],
});

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),
],
},
);

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).

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.

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 });
  • There is no guardInbound. Channels already hit processInput.
  • There is no guardApproval. requireApproval is human HITL confirmation, not policy.
  • Don’t call createAgentContext inside a Mastra callback.
  • Don’t also wrap these tools with @arcjet/guard/vercel-ai/v7.
  • Don’t throw from guardTool to 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.
  • Prompt injection on every step: guardProcessor in both inputProcessors and, if you want later tool continuations screened, rely on processInputStep (the same processor object handles both).
  • Rate limit plus PII on a tool: tokenBucket keyed on a trusted ID, plus localDetectSensitiveInfo() on a free-text note.
  • MCP tools you did not wrap: guardHooks with an action function of { toolName } that returns a past-tense slug per tool.