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.

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 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 }). Do not throw.
  • 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 / 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 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:

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.

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

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

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, not policy.
  • Do not call createAgentContext inside 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.