genkit with its model plugins, keep keys off flow traces, screen text before generate(), don't treat interrupt() as a gate, wrap the returned ToolAction, pass guardMiddleware on generate({ use }), omit outputSchema on guarded tools, and run the same scanners in CI that you run in the editor.What is the Genkit security guide?
Use this guide on Genkit JS 1.41.x (genkit >=1.0.0 <2). Stay on Genkit 1.33 or later if you use middleware that hooks tool calls.
Genkit is Google's framework for building AI flows: model calls, tools, and multi-step generation with tracing built in. When you call ai.generate() or chat.send(), the model may invoke tools you defined or tools injected from the filesystem or MCP. Untrusted prompt text can steer those calls the same way it can in any agent stack.
You need two separate controls. Screen user text before generation starts, then gate each tool invocation with rules you own—rate limits, PII detection, allowlists—before the side effect runs. Genkit also has interrupts and tool-approval middleware for human confirmation. Those 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 Genkit defineTool actions—not Go or Python Genkit, and not Vercel AI SDK tool() handlers.
HTTP request protection on a route doesn't see generate()—you still need runtime controls inside the flow.
Work through these eight topics in order. Each one is a control you can verify, not a slogan.
- Patch and lock dependencies. Stay on Genkit 1.33 or later if you use middleware.
- Keep API keys off the client and out of flow traces.
- Screen user text before
ai.generate()orchat.send(). - Don't treat
interrupt(),defineInterrupt, ortoolApprovalas a policy gate. - Wrap the returned
ToolAction, not the innerdefineToolhandler. - Gate tools that you didn't wrap with generate middleware.
- Validate arguments. Prefer omitting
outputSchemaon guarded tools. - Catch issues in the editor and in CI before they ship.
For the Guard helpers used in the examples, see the Genkit agent guard.
How do you keep Genkit 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 genkit and every model plugin (@genkit-ai/googleai, @genkit-ai/vertexai) together—a plugin major that doesn't match the core package is a silent runtime miss. Treat install scripts and added network access as review events. Prefer a short function that you maintain over a trivial dependency. See trivial packages.
How should Genkit secrets be handled?
Model and gateway keys such as GEMINI_API_KEY and GOOGLE_APPLICATION_CREDENTIALS belong on the server, loaded at process start. Don't put them in a client bundle, a flow input that later lands in a trace, or a context object that you serialize for debugging.
Genkit traces can include tool input and output. Redact before export. See redacting sensitive data from logs and storing secrets in environment variables.
How do you screen inbound Genkit text?
Genkit doesn't ship an inbound hook, so screen in your route handler, job, or webhook before you call ai.generate() or chat.send(). That is where user text is still plain—not yet part of the model's context. Middleware model hooks intercept the model call, not user text.
With Guard, call arcjet.guard() with prompt-injection rules on that string. 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 call generate().
import { detectPromptInjection } from "@arcjet/guard";import { genkitContext } from "@arcjet/guard/genkit/v1";import { arcjet } from "./arcjet.js";
const inbound = detectPromptInjection();const appContext = { sessionId: conversationId };
const decision = await arcjet.guard({ label: "message.received", rules: [inbound(userText)], ...genkitContext({ context: appContext }),});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) { throw new Error("message blocked");}genkitContext takes { context: appContext }, not a bare { sessionId }. It never mints an ID. It never reads traceId. It never treats interrupt / resumed as correlation.
Why isn't interrupt() a security policy?
Genkit can pause a flow and ask a person to approve a tool call. That is human-in-the-loop: valuable when a reviewer needs context, but not equivalent to a policy that evaluates every invocation automatically.
interrupt(), defineInterrupt, @genkit-ai/middleware toolApproval, and restartTool pause for a person. They are confirmation, not a remote allow or deny. If nobody is watching, or if the UI doesn't surface the arguments, the pause doesn't protect you.
The same pattern appears as Mastra requireApproval, Claude canUseTool, LangGraph interrupt(), and OpenAI Agents needsApproval. See human approval is not a security policy.
Don't turn a deny into interrupt(), ToolInterruptError, or finishReason: "interrupted"—those are pauses for a person, not a policy result.
How do you gate Genkit tools?
After inbound screening, tools are where refunds, emails, and API writes happen. Genkit wraps tool definitions in a ToolAction object. Arcjet hooks that outer callable so denials return structured results the model can read, instead of crashing the flow.
guardTool wraps the ToolAction that ai.defineTool returns. It replaces that callable and .run. It doesn't wrap the inner handler. outputSchema validation runs inside action(), so wrapping the handler would throw on a schema-mismatched denial and fail generate(). Prefer omitting outputSchema on guarded tools.
The helper overwrites the original registry key because generate() converts tools to name and schema defs and looks the live action up. On deny the original action never runs. The model receives an ArcjetDenialResult as a completed toolResponse.output. Don't throw.
guardMiddleware is a { name, instantiate } object with a tool hook. Pass it on ai.generate({ use }). A raw function becomes a model hook only and can't deny. The hook skips branded (guardTool) tools when it can look them up. Tools that can't be looked up are still gated.
MCP and filesystem-injected tools skip an unwrapped handler. guardMiddleware still gates a call that generate() executes. returnToolRequests: true still hits guardTool if the caller invokes the wrapped action.
import { genkit, z } from "genkit";import { localDetectSensitiveInfo, tokenBucket } from "@arcjet/guard";import { guardMiddleware, guardTool } from "@arcjet/guard/genkit/v1";import { arcjet } from "./arcjet.js";
const ai = genkit({/* configure your model plugin */});
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});const detectPii = localDetectSensitiveInfo();
const lookupOrder = guardTool( arcjet, ai.defineTool( { name: "lookup_order", description: "Look up an order by number", inputSchema: z.object({ orderNumber: z.string(), note: z.string(), }), }, async ({ orderNumber, note }) => ({ orderNumber, note, status: "shipped", }), ), { action: "order.looked-up", rules: (input) => [ lookupLimit({ key: input.orderNumber, requested: 1 }), detectPii(input.note), ], },);
await ai.generate({ prompt: userText, tools: [lookupOrder], use: [guardMiddleware(arcjet, { sessionId: conversationId })], context: { sessionId: conversationId },});Scan the free-text note. An opaque orderNumber doesn't trip email, phone, card, or IP detection.
The tool-hook ctx from toRunOptions is only { metadata, resumed }. Put the same ID on guardMiddleware({ sessionId }) when you need tool-time correlation.
How does the editor and CI catch Genkit 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 the tools that are registered and the tools that 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 flow: a prompt that asks for a refund, an authored lookup that includes a note, and a filesystem-injected tool that you didn't wrap. Then apply AI agent runtime security for sequence, budget, and identity controls that aren't Genkit-specific.
Frequently asked questions
What is the Genkit security guide?
Use this eight-item guide on Genkit JS 1.41.x. Pin genkit with its model plugins, keep keys off flow traces, screen text before generate(), don't treat interrupt() as a gate, wrap the returned ToolAction, pass guardMiddleware on generate({ use }), omit outputSchema on guarded tools, and run the same scanners in CI that you run in the editor.
Does this guide cover Go or Python Genkit?
No. The Arcjet adapter is Genkit JS only. Don't wrap Go or Python Genkit with @arcjet/guard/genkit/v1.
Why omit outputSchema on a guarded tool?
outputSchema validation runs inside action(). Wrapping the inner handler would throw on a schema-mismatched denial and fail generate(). guardTool wraps the returned ToolAction instead.
Can a raw generate middleware function deny a tool?
No. A raw function becomes a model hook only. Pass guardMiddleware as a { name, instantiate } object on generate({ use }).
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.