@strands-agents/sdk 1.x. Stay on 1.x, keep keys off invocationState, screen text before invoke, don't treat event.interrupt() as a gate, wrap authored tool({ callback }), pass guardHooks as a Plugin, deny with BeforeToolCallEvent.cancel only, and run the same scanners in CI that you run in the editor.What is the Strands Agents security guide?
Use this guide on Strands Agents JS 1.x (@strands-agents/sdk >=1.1.0 <2).
Strands Agents is AWS's JavaScript agent framework. You define tools with callbacks, pass them to Agent, and call invoke() or stream(). The model reads user text and may call those tools—or MCP and vended tools that skip your callback. Prompt injection is when untrusted text tries to steer those calls toward side effects you never intended.
You need two separate controls. Screen inbound text before invoke, then gate tool calls on BeforeToolCallEvent for tools that aren't already wrapped. Strands also supports event.interrupt() for human confirmation—that pause isn't a policy gate on its own.
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 Strands tool({ callback }) plus BeforeToolCallEvent hooks—not LangChain, LangGraph, or Vercel AI SDK tool types.
HTTP request protection on a route doesn't see invoke()—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
@strands-agents/sdk1.x. - Keep API keys off the client and out of
invocationState. - Screen user text before
agent.invokeoragent.stream. - Don't treat
event.interrupt()as a policy gate. - Wrap authored
tool({ callback })handlers. - Register a plugin that gates tools on
BeforeToolCallEvent. - Deny with
BeforeToolCallEvent.cancel. Don't setBeforeToolsEvent.cancel. - Catch issues in the editor and in CI before they ship.
For the Guard helpers used in the examples, see the Strands Agents agent guard.
How do you keep Strands Agents 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 @strands-agents/sdk so a 2.x major can't land under an unversioned import. Treat MCP or vended-tool packages as review events: those tools skip an unwrapped callback. Prefer a short function that you maintain over a trivial one-liner package. See trivial packages.
How should Strands Agents secrets be handled?
Model keys such as OPENAI_API_KEY and ANTHROPIC_API_KEY belong on the server, loaded at process start. Don't put them on invocationState. That object is for caller-owned IDs (correlationId, sessionId, requestId), not credentials.
Don't log tool arguments or the raw prompt. See storing secrets in environment variables and redacting sensitive data from logs.
How do you screen inbound Strands Agents text?
Strands doesn't expose a first-class inbound hook, so screen in application code before agent.invoke or agent.stream. Evaluate untrusted text while it is still a string you control—not after it becomes part of the agent's turn history. With Guard, call arcjet.guard() with prompt-injection rules on that string.
Direct guard() fails open. Gate on decision.hasFailedOpen() when this call site must fail closed. On deny, don't call invoke().
import { detectPromptInjection } from "@arcjet/guard";import { strandsAgentContext } from "@arcjet/guard/strands-agents/v1";import { arcjet } from "./arcjet.js";
const inbound = detectPromptInjection();const invocationState = { sessionId: conversationId };
const decision = await arcjet.guard({ label: "message.received", rules: [inbound(userText)], ...strandsAgentContext({ invocationState }),});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) { throw new Error("message blocked");}strandsAgentContext reads invocationState.correlationId, then sessionId, then requestId. It never mints an ID. It never reads traceId or agent.id. If none of those keys is present, the call is uncorrelated.
Why isn't event.interrupt() a security policy?
Strands can pause a run and wait for a person to continue. That is human-in-the-loop: good for workflows where someone must review context, but not the same as a remote policy on every tool call.
event.interrupt() pauses for a person. It is human-in-the-loop confirmation, not a remote allow or deny. The same pattern appears as LangChain JS humanInTheLoopMiddleware, Mastra requireApproval, Claude canUseTool, LangGraph interrupt(), Genkit toolApproval, and OpenAI needsApproval.
Don't deny by calling interrupt(). Policy belongs on BeforeToolCallEvent.cancel only. See human approval is not a security policy.
How do you gate Strands Agents tools?
After inbound screening, tool calls are where side effects happen. Strands splits authored tools—which have a callback you can wrap—from MCP and vended tools that only appear at invoke time. Arcjet covers both: guardTool for callbacks, guardHooks as a plugin for everything else.
guardTool wraps an authored tool({ callback }) that you pass to new Agent({ tools }). On deny the original callback never runs. The helper returns a plain ArcjetDenialResult. Don't throw. Prefer omitting outputSchema on guarded tools.
guardHooks is a Plugin. Pass it on new Agent({ plugins }). Don't pass it to agent.addHook. initAgent registers BeforeToolCallEvent as the invoke-wide gate. On deny it sets event.cancel to a JSON string of ArcjetDenialResult. That string is the tool result error message.
Don't set BeforeToolsEvent.cancel. That skips per-tool hooks. The plugin skips branded (guardTool) tools. MCP and vended tools that aren't branded are still gated. AfterToolCallEvent is capture only.
import { Agent, tool } from "@strands-agents/sdk";import { z } from "zod";import { localDetectSensitiveInfo, tokenBucket } from "@arcjet/guard";import { guardHooks, guardTool } from "@arcjet/guard/strands-agents/v1";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});const detectPii = localDetectSensitiveInfo();
const lookupOrder = guardTool( arcjet, tool({ name: "lookup_order", description: "Look up an order by number", inputSchema: z.object({ orderNumber: z.string(), note: z.string(), }), callback: ({ orderNumber, note }) => ({ orderNumber, note, status: "shipped", }), }), { action: "order.looked-up", rules: (input) => [ lookupLimit({ key: input.orderNumber, requested: 1 }), detectPii(input.note), ], },);
const agent = new Agent({ tools: [lookupOrder], plugins: [guardHooks(arcjet, { sessionId: conversationId })],});
await agent.invoke(userText, { invocationState: { sessionId: conversationId },});Scan the free-text note. An opaque orderNumber doesn't trip email, phone, card, or IP detection.
A run that pauses on event.interrupt() resumes through a later invoke. Put the same invocationState on that resume call so later Guard decisions stay on the Sequence that started it.
How does the editor and CI catch Strands Agents 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 MCP or vended tools skip callback and whether guardHooks is on the agent. They catch the mistakes that are cheap to find automatically.
What do you do after this guide?
Confirm each item against one sensitive invoke: a prompt that asks for a refund, 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 Strands-specific.
Frequently asked questions
What is the Strands Agents security guide?
Use this eight-item guide on @strands-agents/sdk 1.x. Stay on 1.x, keep keys off invocationState, screen text before invoke, don't treat event.interrupt() as a gate, wrap authored tool({ callback }), pass guardHooks as a Plugin, deny with BeforeToolCallEvent.cancel only, and run the same scanners in CI that you run in the editor.
Can I pass guardHooks to agent.addHook?
No. guardHooks is a Plugin. Pass it on new Agent({ plugins }).
Why not set BeforeToolsEvent.cancel?
That skips per-tool hooks. Policy sits on BeforeToolCallEvent.cancel only.
Does this cover Python Strands?
The Arcjet adapter documented here is @strands-agents/sdk JavaScript. Don't wrap these tools with the LangChain or Vercel AI adapters.
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.