AI agent security

Vercel AI SDK security guide

Use this eight-item guide on Vercel AI SDK 7. Stay on Node 22+ and ESM, keep keys off NEXT_PUBLIC_ variables, screen text before generateText, wrap tool({ execute }), don't declare contextSchema, use guardAction for app-invoked work, scan free-text arguments, and run the same scanners in CI that you run in the editor.

6 min read
In short: Use this eight-item guide on Vercel AI SDK 7. Stay on Node 22+ and ESM, keep keys off NEXT_PUBLIC_ variables, screen text before generateText, wrap tool({ execute }), don't declare contextSchema, use guardAction for app-invoked work, scan free-text arguments, and run the same scanners in CI that you run in the editor.

What is the Vercel AI SDK security guide?

Use this guide on the Vercel AI SDK 7 (ai >=7 <8, @ai-sdk/provider-utils >=5 <6). AI SDK 7 requires Node.js 22 or later and ESM-only imports—CommonJS require() doesn't work.

The Vercel AI SDK is a toolkit for calling models and defining tools in TypeScript. When you call generateText or streamText, the model may invoke tool({ execute }) handlers you registered—send email, charge a card, call an internal API. User prompts, RAG chunks, and tool outputs can all contain text that tries to steer those calls. That is prompt injection.

You need two separate controls. Screen inbound text before generation starts, then gate each tool call with rules you define—rate limits, PII checks, allowlists—before the side effect runs. A clean inbound score doesn't authorize sendEmail. The SDK's human-approval primitives pause for a person; they don't replace an action gate.

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 both controls. They wrap AI SDK tool({ execute }) handlers only—not Mastra createTool, Eve defineTool, LangGraph tool(), or other frameworks' tool types.

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

  • Patch and lock dependencies. Stay on AI SDK 7 and Node 22+.
  • Keep API keys off the client and out of NEXT_PUBLIC_ variables.
  • Screen user text before generateText / streamText.
  • Wrap model-invoked tools before execute runs. Correlate the run.
  • Don't declare contextSchema on a tool that an action gate wraps.
  • Gate app-invoked work separately from model-invoked tools.
  • Validate arguments. Scan free-text notes, not opaque IDs.
  • Catch issues in the editor and in CI before they ship.

For a shorter wiring recipe, see How do I secure a Vercel AI SDK agent?. For the Guard helpers used in the examples, see the Vercel AI SDK agent guard. Prompt injection protection for LangChain, LlamaIndex, and Vercel AI SDK only places detectors on the route and the retriever—a clean inbound score still isn't authorization for sendEmail.

How do you keep Vercel AI SDK 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 ai and every @ai-sdk/* package together so a provider major can't drift under the core SDK. AI SDK 7 is ESM-only: set "type": "module" or use .mjs, and don't load it through CommonJS require().

Node.js 18 and 20 are unsupported—Node 20 reached end-of-life on 2026-04-30. Node 22 is Maintenance LTS until 2027-04-30; prefer Node 24 LTS for production. Node 26 is Current, not LTS. Put the floor you choose in engines.node so installs fail loudly on an unsupported runtime.

Treat added provider packages as review events: they often gain network or filesystem access. Prefer a short function that you maintain over a trivial one-liner package. See trivial packages.

How should Vercel AI SDK secrets be handled?

Model and gateway keys such as OPENAI_API_KEY and AI_GATEWAY_API_KEY belong on the server, loaded at process start. In a Next.js app, anything named NEXT_PUBLIC_* is inlined into the client bundle—anyone can read it—so never put a signing key or other server secret there. See the Next.js security checklist if the agent lives in a Next.js route.

Don't log tool arguments or the raw prompt. A support dump that includes the last user message is a leak. Prefer a secrets manager in production, and redact before records leave the process. See storing secrets in environment variables and redacting sensitive data from logs.

How do you screen inbound Vercel AI SDK text?

The AI SDK doesn't expose a first-class inbound hook, so screen in your route handler, job, or webhook before you call generateText or streamText. That is where a user prompt or retrieved chunk is still plain text—not yet part of the model's context.

With Guard, call arcjet.guard() with prompt-injection rules on that string. Launch the client once at module scope. Create one createAgentContext at the HTTP route, job, or webhook that starts the run—don't stash it in module state or AsyncLocalStorage. Direct guard() fails open: an ALLOW isn't proof the rules ran, so gate on decision.hasFailedOpen() when this call site must fail closed. On deny, don't start generation.

import { detectPromptInjection } from "@arcjet/guard";
import { createAgentContext } from "@arcjet/guard/vercel-ai/v7";
import { arcjet } from "./arcjet.js";
const context = createAgentContext({ correlationId: conversationId });
const inbound = detectPromptInjection();
const decision = await arcjet.guard({
label: "message.received",
rules: [inbound(prompt)],
correlationId: context.correlationId,
});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) {
throw new Error("message blocked");
}

A caller-supplied correlationId must be 1-256 characters of printable ASCII. Prefer a conversation ID, request ID, or ticket ID over a generated value.

How do you gate Vercel AI SDK tools and actions?

After inbound screening, side effects happen when the model calls a tool—or when your application performs a risky step on its own. Guard's AI SDK helpers cover three callers:

  • guardTool() for model-invoked work. The LLM decided to call the tool.
  • guardAction() for app-invoked work. Your code performs the risky step.
  • captureAction() for observe-only records. No decision.

The wrapped tool must have execute—a tool with none has nothing to wrap. It also can't already declare contextSchema, because Guard uses that slot and throws if the tool already owns it.

On deny the email provider never runs and the model receives an ArcjetDenialResult. Don't throw from guardTool: a throw becomes a generic tool error and drops the fields. guardAction throws so application code can catch—ArcjetDeniedError on a deny, ArcjetGuardUnavailableError when the policy could not be evaluated.

import { launchArcjet, policyInput, tokenBucket } from "@arcjet/guard";
import {
aiToolsContext,
createAgentContext,
guardTool,
} from "@arcjet/guard/vercel-ai/v7";
import { generateText, tool } from "ai";
import { z } from "zod";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const emailLimit = tokenBucket({
bucket: "email",
refillRate: 5,
intervalSeconds: 60,
maxTokens: 5,
});
export async function runAgent(
user: { id: string; allowedRecipients: string[] },
prompt: string,
) {
const sendEmail = guardTool(
arcjet,
tool({
description: "Send an email",
inputSchema: z.object({
recipient: z.string().email(),
body: z.string(),
}),
execute: ({ recipient, body }) =>
emailProvider.send({ to: recipient, body }),
}),
{
action: "email.sent",
actor: user.id,
rules: () => [emailLimit({ key: user.id, requested: 1 })],
inputs: ({ recipient, body }) => ({
recipient: policyInput.server.string(recipient),
allowed_recipients: policyInput.server.stringList(
user.allowedRecipients,
),
body: policyInput.local.string(body),
}),
},
);
const tools = { sendEmail };
const context = createAgentContext({ correlationId: user.id });
return generateText({
model: "openai/gpt-4o-mini",
prompt,
tools,
toolsContext: aiToolsContext(context, tools),
});
}

Derive actor from an authenticated server-side identity. A policy can be conditioned on the actor, so an actor taken from model-produced tool input lets whoever controls the prompt escape their own policy scope.

If you omit toolsContext, Guard still runs but the call is uncorrelated. The first such call always warns. Omitting rules still makes the guard call so a remote policy can apply.

AI SDK 7 human-approval primitives aren't a remote policy. See human approval is not a security policy.

How does the editor and CI catch Vercel AI SDK 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, on known vulnerable dependencies, and on CommonJS require("ai") if your repo is ESM-only. None of these replace a review of which tools have execute and which ones you actually wrapped. They catch the mistakes that are cheap to find automatically.

What do you do after this guide?

Confirm each item against one sensitive route: a prompt that asks to email finance, an authored sendEmail that you wrapped, and an app-invoked GitHub review that uses guardAction. Then apply the Next.js security checklist if the route is a Next.js handler, and AI agent runtime security for sequence, budget, and identity controls.

Frequently asked questions

What is the Vercel AI SDK security guide?

Use this eight-item guide on Vercel AI SDK 7. Stay on Node 22+ and ESM, keep keys off NEXT_PUBLIC_ variables, screen text before generateText, wrap tool({ execute }), don't declare contextSchema, use guardAction for app-invoked work, scan free-text arguments, and run the same scanners in CI that you run in the editor.

Can I wrap a Mastra or Eve tool with this adapter?

No. The v7 adapter is for AI SDK tool({ execute }) only. Mastra, Eve, LangGraph, OpenAI Agents, Claude Agent SDK, Genkit, Strands, and official CrewAI each have their own adapter.

What happens if I omit toolsContext?

Guard still runs but the call is uncorrelated. The first uncorrelated call warns. Pass aiToolsContext(context, tools) on generateText.

Should actor come from a tool argument?

No. Derive actor from an authenticated server-side identity. An actor taken from model-produced input lets whoever controls the prompt escape their own policy scope.

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.