How do you secure Strands Agents?
To secure Strands Agents, screen user text before invocation, wrap authored tools, and install the tool-call hook for unwrapped capabilities. Use trusted caller identity and recheck resource access in the handler. JavaScript uses a Plugin and cancel; Python uses a hook provider and cancel_tool to deny execution.
Use this guide on Strands Agents JS 1.x (@strands-agents/sdk >=1.1.0 <2).
Strands Agents is AWS's agent framework with JavaScript and Python runtimes. The first examples use JavaScript; the Python section covers its separate adapter. 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 Model Context Protocol (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.
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 following examples 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.
Use this checklist to review your application:
- 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 do you manage Strands Agents secrets?
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";
// Resolve authenticatedUser from the server session before constructing tools.const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});const detectPii = localDetectSensitiveInfo({ deny: ["EMAIL", "PHONE_NUMBER", "IP_ADDRESS", "CREDIT_CARD_NUMBER"],});
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: authenticatedUser.id, 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 do you secure Python Strands Agents?
The Python Strands adapter wraps authored tools with guard_tool and registers unwrapped-tool checks through Agent(hooks=[...]). Use arcjet.guard.strands_agents; Python hook providers and JavaScript plugins have different interfaces.
Install "arcjet[strands-agents]>=1.1.0" with CPython 3.10 or later and lock the resolved dependencies. The adapter supports strands-agents>=1.11,<2.
Use guard_tool around authored @tool functions and pass guard_hooks through Agent(hooks=[...]). Screen inbound text explicitly before invoking the agent. This synchronous example accepts a client from launch_arcjet_sync, a trusted caller identity, and an application order service that enforces record ownership:
from arcjet.guard import DetectPromptInjection, TokenBucketfrom arcjet.guard.strands_agents import guard_hooks, guard_toolfrom strands import Agent, tool
inbound_scan = DetectPromptInjection()lookup_limit = TokenBucket( bucket="order-lookups", refill_rate=10, interval_seconds=60, max_tokens=10,)tool_limit = TokenBucket( bucket="other-tools", refill_rate=10, interval_seconds=60, max_tokens=10,)
def run_agent(guard, caller_id, conversation_id, user_text, lookup): decision = guard.guard_sync( label="message.received", rules=[inbound_scan(user_text)], correlation_id=conversation_id, ) if decision.has_failed_open(): raise RuntimeError("Guard evaluation unavailable; message not processed") if decision.conclusion == "DENY": raise RuntimeError("message denied by security policy")
@tool def lookup_order(order_id: str) -> dict: """Look up an order the authenticated caller can access.""" return lookup(caller_id, order_id)
guarded_lookup = guard_tool( guard=guard, tool=lookup_order, action="order.looked-up", actor=caller_id, session_id=conversation_id, rules=lambda arguments: [lookup_limit(key=caller_id, requested=1)], ) agent = Agent( tools=[guarded_lookup], hooks=[guard_hooks( guard=guard, actor=caller_id, session_id=conversation_id, action="tool.requested", rules=lambda arguments: [tool_limit(key=caller_id, requested=1)], )], ) return agent(user_text)The Python guard_hooks provider skips tools that already carry the guard_tool marker. If you add MCP tools, the hook provider can gate their calls; still restrict which servers and capabilities the agent can access. The example's fallback rule imposes a budget, not resource authorization. Add action-specific policies and keep tenant checks inside the application service.
On denial, guard_tool returns a plain ArcjetDenialResult dictionary. The Python hook provider sets BeforeToolCallEvent.cancel_tool to a JSON denial string, whereas the JavaScript adapter uses event.cancel. Do not use BeforeToolsEvent.cancel to enforce policy: it skips the per-tool hooks. After-tool events can record what happened but cannot undo it.
For a persistent conversation, use your application's session lifecycle instead of creating a fresh agent and history on every turn. Supply the same caller-owned ID on subsequent invocations and resumes. strands_agent_context can derive it from invocation_state; it also accepts explicit correlation, session, or request IDs. Never let a request select another user's stored history merely by naming that ID.
Keep the default on_guard_error="deny" on tool helpers. Direct guard_sync needs the explicit failed-open check in the preceding example. Test an allowed authored tool, a denied MCP tool, an unavailable evaluation, and a resumed interrupt, checking both the denial result and the tool handler's call count. See the Python Strands integration reference for async clients and the full hook contract.
How do 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.
How do you verify enforcement before deployment?
Use deterministic Guard responses and a handler call counter. A DENY or unavailable evaluation must leave the counter at zero; an authorized ALLOW must call the handler once. Also test another tenant's resource identifier: application authorization must still refuse access even when Guard allows.
Test an authored wrapper and an unwrapped MCP tool in both runtimes. Assert the JavaScript cancel and Python cancel_tool paths stop the underlying handler. Repeat after interrupt resume and with the hook provider installed alongside a wrapper to catch skipped hooks or duplicate evaluations.
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?
To secure Strands Agents, screen user text before invocation, wrap authored tools, and install the tool-call hook for unwrapped capabilities. Use trusted caller identity and recheck resource access in the handler. JavaScript uses a Plugin and cancel; Python uses a hook provider and cancel_tool to deny execution.
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?
Yes. Use arcjet.guard.strands_agents in Python, with guard_tool and Agent(hooks=[guard_hooks(...)]). Python denies through cancel_tool; JavaScript uses a Plugin and cancel.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Arcjet guards run inside the tool, so the allow or deny arrives before the side effect rather than after it.