AI agent security

Mastra security guide

Use this eight-item guide on Mastra 1.63.x. Pin @mastra/core with the adapters that you import, keep keys off RequestContext, screen messages with guardProcessor, don't treat requireApproval as a gate, wrap createTool, deny MCP with guardHooks, set a thread ID before generate, and run the same scanners in CI that you run in the editor.

8 min read
In short: Use this eight-item guide on Mastra 1.63.x. Pin @mastra/core with the adapters that you import, keep keys off RequestContext, screen messages with guardProcessor, don't treat requireApproval as a gate, wrap createTool, deny MCP with guardHooks, set a thread ID before generate, and run the same scanners in CI that you run in the editor.

What is the Mastra security guide?

Use this guide on Mastra 1.63.x (@mastra/core 1.x).

Mastra agents combine models, tools, memory, and processors in one runtime. When a user message reaches generate() or stream(), the agent may call tools you wrote, tools from MCP, or tools from a workspace mount. Untrusted text in the message—or in memory the agent recalls—can steer those calls toward actions you never intended.

You need three separate controls, and Mastra treats them in three places. Screen inbound text on inputProcessors. Wrap authored tools that have a local execute. Deny MCP, workspace, and toolset tools on hooks, because those never hit your execute function. Treating those as one list is how createPullRequest still runs after you "blocked prompt injection."

Arcjet Guard is a runtime policy layer for AI agents. You define rules—prompt-injection detectors, rate limits, PII checks, allowlists—and Guard allows or denies inbound text and tool calls before side effects run. The examples below use Guard for those three controls. They wrap Mastra createTool handlers and hook MCP and workspace tools—they don't wrap Vercel AI SDK tool({ execute }), Eve defineTool, or other frameworks' tool types.

Mastra also ships detectors and requireApproval for human confirmation. A clean detector score or an approval click isn't authorization for a side effect. See Mastra guardrails vs an action gate.

HTTP request protection on a route doesn't see generate()—you still need runtime controls inside the agent lifecycle.

Work through these eight topics in order. Each one is a control you can verify, not a slogan.

  • Patch and lock dependencies. Stay on @mastra/core 1.x.
  • Keep API keys off the client and out of RequestContext.
  • Screen inbound text on inputProcessors before the model sees it.
  • Don't treat requireApproval or Mastra detectors as a policy gate.
  • Wrap authored createTool handlers that have local execute.
  • Deny MCP, workspace, and toolset tools on hooks.
  • Set a thread or resource ID on RequestContext before generate.
  • Catch issues in the editor and in CI before they ship.

For a shorter wiring recipe, see How to secure a Mastra agent. For the Guard helpers used in the examples, see the Mastra agent guard.

How do you keep Mastra dependencies safe?

Apply patches on a schedule you actually keep, and out of band for a critical advisory. Commit a lockfile so every environment installs the same graph, and turn on Dependabot, Renovate, or an equivalent bot for npm. Run npm audit (or your package manager's audit) in CI and fail on known high-severity issues you haven't waived.

Pin @mastra/core and every @mastra/* package that you actually import (@mastra/memory, @mastra/mcp, a sandbox provider) together. A memory adapter major that doesn't match core is a silent persistence miss.

Mastra 1.63 includes a logger adapter contract for trace-correlated output. That is an observability change, not a deny. Treat sandbox and MCP packages as review events: they add network and filesystem access. Prefer a short function that you maintain over a trivial one-liner package. See trivial packages.

How should Mastra secrets be handled?

Model keys such as OPENAI_API_KEY belong on the server, loaded at process start. Don't put them on RequestContext unless the value is a caller-owned ID that you already have. Don't put them in agent instructions that later land in a trace.

Mastra storage and scores can persist tool arguments. Redact before you write. Don't log tool arguments or the raw prompt. See redacting sensitive data from logs and storing secrets in environment variables.

How do you screen inbound Mastra messages?

Mastra runs inputProcessors before the model sees a message. That is the natural inbound boundary—screen there so injection checks happen inside the agent lifecycle, not only on the HTTP route that called generate(). The framework has no separate inbound hook like Eve's channel gate, so the examples use guardProcessor on inputProcessors. On deny, processInput and processInputStep call abort(), and Mastra raises a tripwire.

processInputStep screens later agentic steps so a tool continuation can't skip the inbound gate. Use a separate action name for outbound text on outputProcessors.

import { Agent } from "@mastra/core/agent";
import { detectPromptInjection } from "@arcjet/guard";
import { guardProcessor } from "@arcjet/guard/mastra/v1";
import { arcjet } from "./arcjet.js";
const inbound = guardProcessor(arcjet, {
action: "message.received",
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],
});

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.

The Mastra PromptInjectionDetector and PIIDetector classify, redact, or abort text. They don't see the tool name. A clean detector score isn't authorization for createPullRequest. See Mastra guardrails vs an action gate.

Why isn't requireApproval a security policy?

Mastra can park a tool call until a person clicks approve in a UI. That is human-in-the-loop: valuable when a reviewer needs context, but not the same as a policy that runs on every invocation regardless of who is watching.

requireApproval parks the call until a person clicks. That stops the send if the reviewer sees enough context to refuse. It is a human hold, not a remote policy. See human approval is not a security policy.

Don't use requireApproval as your policy deny point—the pause isn't a remote allow or deny.

How do you gate authored tools vs MCP and workspace tools?

After inbound screening, side effects happen on tool calls. Mastra splits tools into two families: ones you authored with createTool and ones mounted from MCP, a workspace, or a toolset. Arcjet matches that split—wrap local execute, deny remote tools on hooks.

Wrap createTool with guardTool. On deny the function never runs. The model gets a structured ArcjetDenialResult. Don't throw. A throw becomes a generic tool error and drops the fields.

import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { localDetectSensitiveInfo, tokenBucket } from "@arcjet/guard";
import { guardTool } from "@arcjet/guard/mastra/v1";
import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({
bucket: "lookups",
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
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),
],
},
);

MCP, workspace, and toolset tools skip that wrap. Use guardHooks. beforeToolCall can return { proceed: false, output } so those tools never execute. afterToolCall runs after Mastra has called the host. A log of the comment isn't a deny.

import { tokenBucket } from "@arcjet/guard";
import { guardHooks } from "@arcjet/guard/mastra/v1";
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 or stream. Don't apply both helpers to the same authored tool. That double-calls the guard. See How to secure Mastra MCP tools.

Set MASTRA_THREAD_ID_KEY / MASTRA_RESOURCE_ID_KEY on RequestContext before generate / stream. mastraAgentContext() reads them. It never mints an ID.

How does the editor and CI catch Mastra security bugs?

Turn on TypeScript strict, ESLint, and secret scanning in the editor. Trunk, Semgrep, TruffleHog, and Gitleaks all have editor plugins and CI jobs. Run the same scanners in CI that you run locally—an editor warning that isn't a CI failure will be ignored.

Fail the build on secrets in the diff and on known vulnerable dependencies. None of these replace a review of which tools have local execute and which ones you actually wrapped—or which MCP, workspace, and toolset mounts still call the host with no guardHooks. They catch the mistakes that are cheap to find automatically.

What do you do after this guide?

Confirm each item against one sensitive agent: a prompt that asks for a pull request, an authored lookup that includes a note, and an MCP tool that you didn't wrap. Then apply AI agent runtime security for sequence, budget, and identity controls that aren't Mastra-specific.

Frequently asked questions

What is the Mastra security guide?

Use this eight-item guide on Mastra 1.63.x. Pin @mastra/core with the adapters that you import, keep keys off RequestContext, screen messages with guardProcessor, don't treat requireApproval as a gate, wrap createTool, deny MCP with guardHooks, set a thread ID before generate, and run the same scanners in CI that you run in the editor.

Are Mastra PromptInjectionDetector and PIIDetector enough?

No. They classify or abort text. They don't see the tool name. A clean detector score isn't authorization for createPullRequest.

Does processInputStep matter?

Yes. processInputStep screens later agentic steps so a tool continuation can't skip the inbound gate.

Should I apply guardTool and guardHooks to the same tool?

No. That double-calls the guard. Use guardTool for authored createTool and guardHooks for MCP, workspace, and toolset tools that you didn't wrap.

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.