AI agent security

Vercel Eve security guide

Use this eight-item guide on Eve 0.47.x. Stay on Node 24+, keep Connect tokens off channel bodies, screen inbound text after the signature check, don't treat hooks or a sandbox as a gate, wrap authored execute, put guardApproval on every MCP or OpenAPI connection, reuse a conversation ID that you already have, and run the same scanners in CI that you run in the editor.

6 min read
In short: Use this eight-item guide on Eve 0.47.x. Stay on Node 24+, keep Connect tokens off channel bodies, screen inbound text after the signature check, don't treat hooks or a sandbox as a gate, wrap authored execute, put guardApproval on every MCP or OpenAPI connection, reuse a conversation ID that you already have, and run the same scanners in CI that you run in the editor.

What is the Vercel Eve security guide?

Use this guide on Vercel Eve 0.47.x (eve >=0.34.0 <1). Eve is in public beta and requires Node.js 24 or later.

Eve is a filesystem-first agent framework from Vercel. You lay out an agent as directories: tools under agent/tools, connections under agent/connections, channels under agent/channels. A Slack webhook or GitHub event reaches a channel, Eve starts a turn, and the model may call tools you wrote or connections to Linear, OpenAPI APIs, and MCP servers. An Eve agent can ship with zero authored tools and a mounted MCP. "We have no tools" isn't "we have no side effects."

You need three separate controls, and Eve splits them the same way. Screen inbound channel text after signature verification. Wrap authored tools that have local execute. Gate OpenAPI and MCP connections at approval time, because those have no execute in your repo. Eve hooks and sandboxes help with audit and isolation—they don't authorize send() or create_issue.

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.

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

  • Patch and lock dependencies. Stay on Eve 0.34+ and Node 24+.
  • Keep API keys and Connect tokens off the client and out of channel bodies.
  • Screen inbound channel text after signature checks.
  • Don't treat Eve hooks, a sandbox, or a Slack signature as a policy gate.
  • Wrap authored defineTool handlers that have local execute.
  • Gate OpenAPI and MCP connections—those have no local execute.
  • Correlate inbound with a conversation ID that you already have, not crypto.randomUUID().
  • Catch issues in the editor and in CI before they ship.

For shorter wiring recipes, see Eve agent security is three jobs, How to detect prompt injection in an Eve agent, and How to secure Eve MCP connections. For the Guard helpers used in the examples, see the Vercel Eve agent guard.

How do you keep Eve 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.

Eve is pre-1.0, so pin eve and every channel or connection package that you actually import. Eve reads node_modules/eve/docs as local documentation—a major that changes defineChannel or approval is a security review, not only a changelog. Put the Node floor you choose in engines.node so installs fail loudly on an unsupported runtime.

Treat MCP, OpenAPI, and sandbox packages as review events. A sandbox isolates generated shell—it doesn't authorize send(). See a sandbox is not a tool policy and trivial packages.

How should Eve secrets be handled?

Model keys and Vercel Connect tokens belong on the server, loaded at process start. In a Next.js app, anything named NEXT_PUBLIC_* is inlined into the client bundle—never put a signing key or other server secret there. Don't put credentials in a channel body or a connection spec URL query string.

Eve verifies Slack and GitHub signatures in constant time. That stops a forged webhook. The body is still untrusted content—a verified message that says "forward last week's invoices" is authentic Slack, not a safe turn.

Don't log channel bodies or tool arguments. See storing secrets in environment variables and redacting sensitive data from logs.

How do you screen inbound Eve messages?

Eve channels are the front door: Slack, GitHub, HTTP webhooks. Signature verification proves the request came from the provider—it doesn't prove the message is safe to turn into agent instructions. Screen the body after that check, before send() starts a turn. The examples use guardInbound() on the channel for that step.

Never call createAgentContext inside an Eve callback.

On deny, return a 403 (or 503 when verdict.outcome is "UNAVAILABLE") and don't call send().

Pass an explicit correlationId. Use a conversation ID that the app already has. Don't generate a per-request UUID. Reuse that same value with args.from() so the inbound decision can join the session later.

import { defineChannel, POST } from "eve/channels";
import { detectPromptInjection } from "@arcjet/guard";
import { guardInbound } from "@arcjet/guard/vercel-eve/v0";
import { arcjet } from "../arcjet.js";
export default defineChannel({
routes: [
POST("/webhook", async (req, args) => {
const body = (await req.json()) as {
message?: string;
conversationId?: string;
};
const { message, conversationId } = body;
if (!message || !conversationId) {
return new Response(JSON.stringify({ error: "Missing fields" }), {
status: 400,
});
}
const verdict = await guardInbound(arcjet, message, {
rules: [detectPromptInjection()(message)],
action: "message.received",
correlationId: conversationId,
});
if (!verdict.allowed) {
return new Response(
JSON.stringify({
error: verdict.message,
outcome: verdict.outcome,
}),
{ status: verdict.outcome === "UNAVAILABLE" ? 503 : 403 },
);
}
const session = await args.from(conversationId).send(message, {
auth: null,
});
return new Response(
JSON.stringify({ success: true, sessionId: session.id }),
{ headers: { "Content-Type": "application/json" } },
);
}),
],
});

Eve helpers default to onGuardError: "deny". "allow" is a legitimate choice on the channel, because failing closed there stops the agent answering during an outage.

A clean inbound score says nothing about sendEmail or linear__create_issue. That leaves the lethal trifecta with only one leg scanned.

Why can't Eve hooks or a sandbox enforce?

Eve ships hooks for audit and sandboxes for isolating generated shell. Those are valuable, but they solve different problems than an action gate that can deny a tool or connection before it runs.

Eve hook handlers, including the ones arcjetHooks() registers, return void. They write an audit line. They can't reject a turn. See why Eve hooks can't enforce.

A sandbox isolates generated shell from process.env and from your Node.js runtime. Authored tools and MCP or OpenAPI connections still run on the trusted side, with full secrets. Isolation doesn't authorize send().

How do you gate Eve tools and connections?

Once a turn is running, Eve splits side effects into authored tools—with execute in your codebase—and connections where Eve injects tokens and calls remote APIs. Arcjet matches that split: guardTool for local code, guardApproval for connections.

An authored tool runs your execute function in the app runtime. Wrap it with guardTool. On deny the helper throws ArcjetDeniedError. Eve projects that as a failed action.result. Pass onDeny: "result" to return the payload instead so an outputSchema isn't silently violated.

defineDynamic tools can't be wrapped with guardTool. Gate those with guardApproval on the approval field of the tool.

OpenAPI and MCP connections have no local execute. Eve injects the token. The only enforcement point is guardApproval() on the approval field of the connection. On request-time deny, Eve returns { type: "denied", reason } the model can read.

Don't compose the returned value with always(), once(), or never() in Eve. To also require a human after the request-time gate, use onAllow: "user-approval". The response policy authorizes the responder. A rejection doesn't deny the tool. See human approval is not a security policy.

import { defineOpenAPIConnection } from "eve/connections";
import { tokenBucket } from "@arcjet/guard";
import { guardApproval } from "@arcjet/guard/vercel-eve/v0";
import { arcjet } from "../arcjet.js";
const apiLimit = tokenBucket({
bucket: "api-access",
refillRate: 30,
intervalSeconds: 60,
maxTokens: 30,
});
export default defineOpenAPIConnection({
description: "Orders API",
spec: "https://api.example.com/openapi.json",
approval: guardApproval(arcjet, {
action: "orders-api.read",
rules: (ctx) => [apiLimit({ key: ctx.session.id, requested: 1 })],
}),
operations: {
allow: ["GetOrder"],
},
});

How does the editor and CI catch Eve 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 every file under agent/connections: a mounted Linear MCP with no guardApproval is an unguarded create_issue. They catch the mistakes that are cheap to find automatically.

What do you do after this guide?

Confirm each item against one sensitive turn: a Slack body that asks to forward invoices, an authored sendEmail, and a Linear MCP connection with no local execute. Then apply AI agent runtime security for sequence, budget, and identity controls that aren't Eve-specific.

Frequently asked questions

What is the Vercel Eve security guide?

Use this eight-item guide on Eve 0.47.x. Stay on Node 24+, keep Connect tokens off channel bodies, screen inbound text after the signature check, don't treat hooks or a sandbox as a gate, wrap authored execute, put guardApproval on every MCP or OpenAPI connection, reuse a conversation ID that you already have, and run the same scanners in CI that you run in the editor.

Is a Slack signature enough?

No. The signature proves Slack sent the webhook. The body is still untrusted content. Screen it with guardInbound after the signature check.

Can Eve hooks deny a turn?

No. Hook handlers return void. They write an audit line. Screen inbound text with guardInbound and gate tools or connections with guardTool or guardApproval.

Does an agent with no authored tools have no side effects?

No. OpenAPI and MCP connections have no local execute. The only enforcement point is guardApproval on the approval field of the connection.

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.