GitHub README Prompt Injection: The File Every Coding Agent Reads First
Your coding agents run overnight on cloud dev boxes, with auto-approve on and an organization...
Aug 18, 2026
Bedrock Guardrails do not inspect your agent’s tool calls.
AWS says so, in its own guidance on securing Bedrock Agents against indirect prompt injection. Guardrails associated with an agent are applied to the user input and the final agent answer. The current implementation does not pass tool input and tool output through them.
That sentence sits mid-paragraph inside a section on content moderation, which is why reading past it is the default outcome. It is also the most operationally significant line in the document.
AWS names four injection vectors for a Bedrock agent: user input, tool input, tool output, and the agent’s final answer. Attaching a guardrail covers the first and the last. The two in the middle are where an instruction stops being text and becomes an effect.
You can close them. AWS documents how, and each method charges a price on every call.
A Bedrock agent runs a loop. It reads a request, reasons about it, constructs an argument set for an action group function, invokes that function, reads what comes back, and reasons again. The loop repeats until the agent has an answer worth returning.
Guardrails sit at the two ends of that loop. The request going in is screened. The answer coming out is screened. Every iteration in between runs on unscreened content.
That matters because the two ends carry text and the middle carries consequence. A hostile instruction sitting in the user’s request has done nothing yet. The same instruction expressed as an argument to a Lambda that writes to DynamoDB has already done everything it was going to do. Tool input is the moment intent becomes an API call. Tool output is the moment attacker-controlled content re-enters the model’s context wearing the agent’s own trust.
Tool output is the vector teams underestimate most. Your agent invoked the tool, so the response feels like the agent’s own work. It is a ticket body, a scraped page, a database row a customer populated through a form, an MCP server’s reply. Anything the tool reads that an attacker can write is a channel into the context window, and the model has no mechanism for telling retrieved content apart from instruction. That is the shape of indirect prompt injection, and on Bedrock it lands on the two vectors your guardrail is not attached to.
AWS gives you three ways to close the gap. Each one works. Each one takes something.
| Vector | Covered by an attached guardrail | What closes it | What it costs | What remains |
|---|---|---|---|---|
| User input | Yes | Attached guardrail, filter strength tuned | Latency per request | Payloads inside the classifier’s benign distribution |
| Tool input | No | User confirmation, or ApplyGuardrail from the action group Lambda | Human approval per action, or an inspection per invocation | Malicious arguments that read as ordinary parameters |
| Tool output | No | ApplyGuardrail from the action group Lambda, or verifier logic in custom orchestration | An inspection per invocation, code you maintain | Content that is adversarial in effect and clean in surface |
| Agent final answer | Yes | Attached guardrail | Latency per response | Exfiltration shaped like a normal response |
User confirmation protects tool input. You enable it per action inside an action group, and Bedrock requires end-user approval before the function is invoked. Decline it and the model takes the refusal as context and tries something else. This is the correct control for mutating actions, and it is the reason to enable it on anything that changes state. It also converts an autonomous agent into an assisted one. That is the right trade on a payments action and the wrong one on a triage agent whose entire value is that nobody has to look at it.
ApplyGuardrail inside the action group Lambda is the method AWS points to for full vector coverage. You call the guardrail API yourself, from your own function, on the arguments coming in and the payload going out. It closes both uncovered vectors properly. The bill is that it is your code in every action group you ship, it adds an inspection to every tool invocation on the hot path, and the coverage is only as complete as your least disciplined function. One action group written in a hurry is one unscreened vector.
Plan-verify-execute moves the control into orchestration. The model produces a plan upfront, custom orchestration parses it, and before any action fires the strategy checks that the action was in the original plan. Tool output cannot introduce a step, because a step that was not planned does not execute. AWS states the constraint plainly: the pattern suits agents working in a constrained space where the orchestration does not need a replanning step, and it does nothing when the user prompt itself is the payload used during planning.
Bedrock’s native controls are good controls with stated scopes. The objection is not that they fail. It is that full coverage is priced per call, per action group, and per human in the loop.
Every vector on that list lives inside one session.
Bedrock Agents and AgentCore Memory persist what happened in a session and feed it into future orchestration prompts. That is the feature. An agent that forgets the customer between Tuesday and Thursday is not much of an assistant, so the summarization step reads the session, extracts what looks important, and writes it to a long-term store that subsequent sessions load as context.
Unit 42 walked a payload through that step. Their proof of concept against Amazon Bedrock Agents used a travel-assistant bot with memory enabled and a malicious webpage the agent fetched. The injected content manipulated the session summarization process so that attacker instructions were written into the agent’s memory. Once stored, they persisted across sessions, arrived inside orchestration prompts as trusted memory, and drove silent exfiltration of conversation history in later interactions.
Read that against the vector list. The instruction entered through tool output, which the list names and the guardrail skips. It then moved somewhere the list has no name for: the summarization step, which is not user input, not tool input, not tool output, and not a final answer. It arrived in session two as system context, which is the one category of text the control model is built to trust.
Two properties make this worse than a session-scoped injection. The attack and the effect are separated in time, so the correlation a responder needs may be weeks wide. And the agent has no basis for distinguishing a memory it formed from a memory that was planted, because both are text in the same store with the same apparent provenance.
Unit 42 was explicit that this is not a Bedrock defect. It is what happens when untrusted content flows into a prompt that later becomes a high-priority instruction. The point for a Bedrock team is narrower. A control model organized around four in-session vectors has nowhere to attach a control for it.
Sort the seven controls AWS recommends and they fall into three groups.
Screening asks what the text says. Guardrails, secure prompt engineering with nonce delimiters, verifier logic inside custom orchestration. Every one of them is a judgment about a string.
Permission asks what the agent is allowed to do. Least privilege on the agent’s role, sandboxing around external content, user confirmation on action groups. None of them read the text. They read a request against a fixed policy.
Record produces evidence afterward. Model invocation logging, CloudTrail, AgentCore Observability traces. They tell you what the agent did once someone goes looking, and nothing in that group is positioned to interrupt anything.
Coercion passes both live families by construction. A poisoned instruction redirects an agent’s authorized capability toward the attacker’s goal, and the agent then does its job with its own credentials, calling tools it is registered to call, reaching endpoints its IRSA-bound role permits. Screening cleared the text because the text was already inside. Permission cleared the action because the action was on the list. Every step is legal and the sequence is the attack. This is the same gap that makes control-plane logging on AWS show a healthy bedrock:InvokeModel event while the workload behind it is being driven by someone else.
The permission family has a second ceiling worth naming, because an isolation boundary is easy to over-trust. BeyondTrust’s Phantom Labs found that AgentCore Code Interpreter’s sandbox network mode permitted DNS resolution, which was enough to move data out of an environment documented as isolated. AWS determined the behavior reflected intended functionality rather than a defect, updated the documentation, and later removed the DNS exfiltration path. The finding is instructive regardless of how it was classified. A permission boundary encodes what someone intended the workload to be able to do. It does not encode what the workload did.
The attacker cannot complete a coercion without producing behavior, and behavior is the one part of the attack that is expensive to fake.
Capture it per agent. A namespace-wide profile cannot separate two agents with different normal behavior, and a pod-level one loses the agent when the pod restarts. What you want on record is the tool invocation sequence, because the single call is authorized and the order is the signal. You want AWS API calls attributed through the IRSA or Pod Identity binding, so a dynamodb:Query resolves to the invoice agent rather than to a node. Network destinations, with the process that opened them. Process execution and syscall activity inside the container. And writes into the memory store, treated as events with their own pattern rather than as storage.
On EKS, kernel-level collection through eBPF gets most of this without touching the agent’s code, with no sidecar, at 1 to 2.5% CPU and around 1% memory. That property matters more for agents than it did for microservices, because instrumentation requiring a code change gets skipped on the agent someone shipped last quarter, and the agent someone shipped last quarter is the one holding the interesting permissions. IRSA scoping and pod-level network policy already do part of this job, and the runtime layer should extend them rather than repeat them.
That telemetry becomes a per-agent behavioral baseline. ARMO builds it as Application Profile DNA (APD™): which tools this agent invokes, which APIs it reaches, which destinations it contacts, which syscalls are routine for it. The baseline is derived from observed execution rather than declared in a manifest, and that distinction decides whether the control can catch anything at all. A declared baseline describes what someone thought the agent would do, which puts it in the same class of artifact as the IAM policy that already cleared the attack.
Against a baseline, the Unit 42 chain produces signal at three points where the vector model produced none. The agent fetches from a source absent from six weeks of history. It writes a memory record whose shape sits outside the pattern of records it normally persists. It reaches a destination that appears nowhere in its profile. None of those is a string to classify. Each is a fact about execution, which is why deviation from an agent’s own runtime behavior survives when prompt-plane scoring does not.
Enforcement runs from the same signal. Progressive sandboxing starts in Audit, where deviations are recorded and the policy is proven against real production traffic, then moves to Enforce, where deviations are stopped. Credential swapping closes the loop from the other side: the agent holds substituted keys, so a call that clears every other check still fails at the point where it tries to use something real.
The loop itself is untouched. Nothing is added to orchestration, nothing sits between the model and its action groups as a classifier, and the agent’s plan quality and tool selection are exactly what they were. What narrows is the set of actions the agent may execute, measured against its own record.
Keep the screening layer. It removes the opportunistic volume before it reaches your agent, and removing that volume is real work. Treat it as a rate reducer and assume every payload written specifically for your stack passes it.
Then budget honestly. Two vectors are covered when you attach a guardrail. Two more cost you a call, a Lambda, or a human. One more has no cell in the model at all, and it is the one that outlives the session.
The instruction is a string in a document, and strings are cheap to vary and cheap to retry until one lands. The agent’s execution is not. To finish the job, the attacker has to make your agent do something it does not do, against a record the attacker never saw and cannot query.
ARMO’s runtime security for AI workloads builds per-agent behavioral baselines from kernel-level observation and enforces against them with no code changes. Watch a demo to see what a coerced Bedrock agent looks like against its own baseline.
No. AWS documents that guardrails associated with an agent apply to the user input and the final agent answer, and that the current implementation does not pass tool input and output through them. To cover those vectors you call the ApplyGuardrail API yourself from inside the action group Lambda, on the arguments arriving and the payload returning. That is your code to write and maintain in every action group you ship.
For any tool that reads content an attacker can write, yes. Ticket bodies, scraped pages, inbound email, customer-populated database rows, and MCP server responses all qualify. For tools that only read data your own systems produce, you are adding latency to every invocation for a narrow class of payload, so scope the inspection by whether the tool touches attacker-reachable content rather than applying it uniformly.
It protects one vector. Requiring approval before an action group function fires means a coerced agent cannot invoke a mutating action without a person saying yes, which is the right control for writes, deletes, and transfers. It does nothing for tool output, and it converts autonomous operation into assisted operation, so it fits agents where a human was going to be in the loop anyway.
Treat writes into the memory store as instrumented events with their own baseline rather than as storage. A poisoned record gets written during a session that otherwise looks routine, so the signal is the shape and frequency of the write against what that agent normally persists. Detection also has to span sessions, because the write and the resulting behavior can sit weeks apart, and a per-session view will show two unrelated normal days.
Four things. Per-agent tool invocation records with ordering preserved. AWS API calls attributed through the IRSA or Pod Identity binding to a specific agent. Process and network activity inside the container. And an identity model that resolves all of it to an agent rather than to a pod or a namespace. Kernel-level eBPF collection supplies most of it without code changes, while CloudTrail and model invocation logs supply the control-plane half and cannot supply the rest.
Your coding agents run overnight on cloud dev boxes, with auto-approve on and an organization...
Chrome’s agentic mode ships with the most complete public architecture for securing one class of...
The prompt injection 101 most engineers were taught is the chatbot version. It has a...