Get the latest, first
arrowBlog
Prompt Injection Through Tool Output Is Two Events (Your Screens Read One)

Prompt Injection Through Tool Output Is Two Events (Your Screens Read One)

Sep 6, 2026

Ben Hirschberg
CTO & Co-founder

Key takeaways

  • Why does OWASP treat tool output as untrusted? Because an outsider can write to it. OWASP's prompt injection prevention cheat sheet groups tool output with RAG documents, web pages, and email bodies as content that has to be screened before the model sees it, and the thing those four share is that someone outside your trust boundary composed the text. A ticket body, a CRM note, and a PR description all qualify even when the system returning them is one you own.
  • Why do OWASP's screens pass a tool-result injection? Each screen holds one moment of the agent loop. Input screening reads the result as content, action screening reads the following call as an authorized operation, and both are correct in isolation. The injection is the relationship between the two moments, and no screen sees both.
  • What does a successful tool-output injection produce that you can observe? A tool call with no precedent in that agent's own history, issued in the same turn as the result. The tool is correct, the schema validates, the session is authorized, and the destination, path, or argument has never appeared in months of that agent's execution. That precedent gap is the signal, and only a system holding that agent's history can read it.

Tool output is untrusted because your own systems produce it.

That is the part of the OWASP guidance that never makes it into a deployment. The label goes on web pages and email bodies, where an outsider obviously wrote the text. It never goes on the ticket store, the CRM, or the repo, because those are yours. The attacker does not care whose system it is. He cares which field takes free text: the ticket body, the opportunity note, the PR description.

So the result arrives from a trusted source, passes input screening as content, and the call that follows it passes action screening as an authorized operation. Each screen reads one of them. The attack is both.

This piece is about the second read, and what it takes to see it.

Untrusted tool output is generated by the systems you trust

Tool output is any text a tool returns into the agent’s context after a call. It sits in the same screening bucket as RAG documents, web pages, and email bodies in OWASP’s prevention guidance, which also names forged tool output as an agent-specific attack pattern. It is untrusted for one reason: someone outside your trust boundary can put words into the field. The system that delivered it does not change that.

Tool output fails that test far more often than the tool list suggests. The tool is Jira, and the field is a ticket body a customer typed. The tool is GitHub, and the field is a pull request description an outside contributor wrote. The tool is Salesforce, and the field is an account note pasted from an inbound email. The tool is Postgres, and the row came from a web form.

Tool output carries a trust label that was assigned to the system, and assigning it there is the reasonable move: the system is what procurement reviewed and what the platform team integrated. The attacker never touches the system. He targets the one field inside it that accepts free text, and that field returns to your agent with the system’s reputation attached.

Tool output is the one injection channel that never crosses your perimeter. A poisoned web page arrives from outside and gets the treatment outside content gets. A poisoned ticket was already inside when the agent asked for it. OWASP’s LLM01 entry calls indirect injection a case of the model accepting input from an external source, and a ticket store is external to the model while sitting inside your network.

So the direct and indirect injection labels tell you which pipeline to inspect and nothing about what happens next. We have traced the full chain from poisoned ingestion through exfiltration; this piece stays on the one stage where the label decides everything downstream.

Re-label your tool inventory by field

Start with one agent. The same tool is trusted for one agent and untrusted for another, because what matters is which of its fields that agent actually reads.

For that agent, list every tool it can call. For each tool, list the return fields that carry free text. Numbers, enums, and structured identifiers are out of scope. For each free-text field, write down who can put words into it. The answer is the label. A field only your own service writes is trusted. A field a customer, a contributor, a vendor, or a web form can write is untrusted, and the tool that returns it is an injection channel for this agent regardless of what the tool is called.

The table for a typical support-triage agent looks like this.

SystemReturn fieldWho writes itLabel
Ticketingticket body, commentsany customeruntrusted
Ticketingstatus, priority, assigneeyour teamtrusted
Knowledge basearticle bodyyour team, contractorstrusted, review contractors
CRMaccount notes, activity logsales reps, inbound email parseruntrusted (parser)
Order databaseshipping notescustomer at checkoutuntrusted
Order databaseorder ID, SKU, amountyour servicetrusted

The label alone does not rank anything. Rank by pairing an untrusted field with a high-privilege tool inside the same agent. The support agent that reads ticket bodies and can only post replies is a disclosure risk. The support agent that reads ticket bodies and can query the customer table, issue a refund, or open an internal link is the one where a coerced call changes state. Sort your agents by the number of untrusted fields multiplied by the number of state-changing tools, and baseline from the top of that list.

Coding agents in remote development environments sit at the top of it almost everywhere. They read repository content that outside contributors can write, they hold repository and cloud credentials in the same runtime, and their tool list includes shell execution and outbound network. Every untrusted field they read is paired with every privileged tool they hold.

Each OWASP screen holds one moment of the loop

OWASP’s prompt injection prevention cheat sheet places three screens and one architecture around the model: input screening, output screening, action screening, and the dual-LLM pattern. Each reads one point in the agent loop.

Input screening reads the tool result before the model sees it. The cheat sheet says to run retrieved context and tool output through a classifier, and it says plainly that pattern filters miss indirect injection in untrusted content. So the screen is a model judging adversarial prose, and its position is fixed: it sees the result, alone, before any decision has been made.

Output screening reads the model’s response after the decision. It catches system-prompt leakage, exfiltration markup, and policy-violating text on the way out. Its position is after the reasoning and before the user, and it sees what the model said. What the model did is outside its position.

Action screening reads the proposed tool call against the user’s original intent. The cheat sheet’s design deliberately withholds the intermediate context from this screen, so that an action drifting because of an injected instruction can be refused on intent alone. Its position is the call, isolated from the result that caused it.

The dual-LLM pattern, as Simon Willison described it, makes the isolation structural. A quarantined model reads untrusted content and cannot act. A privileged model holds the tools and never reads untrusted content directly. Nothing the quarantined model produces reaches the privileged one unfiltered. A controller holds the tainted text in variables, passes references, and lets a value cross only when it is verifiable, such as a label from a fixed set. The pattern holds as long as nothing unverified crosses, and it costs the privileged model every detail it never sees.

Every one of these screens does real work and every one of them was measured against a static attacker. Detector models, delimiters, and sandwiching broke together under adaptive attackers, for the same positional reason. The sandwiching you added after tool output is a rate reducer, and it should stay.

So the four controls read four moments: the result, the response, the call, and whatever the controller lets across. One moment each.

The injection lives between the result and the next call

A tool result lands in the context window, and within the same turn the agent issues its next call. Those are two events with a few hundred milliseconds between them, and the injection is what happens across that gap.

The result arrives as the most recent thing the agent has read, and recency is why it works. A description is one line competing with every other tool’s line. A result is the last thing in the window when the agent decides what to do next, and it carries the standing of data the agent asked for. The split is measurable: researchers who benchmarked poisoned tool descriptions took payloads from InjecAgent, a benchmark where the malicious text arrives in a tool’s execution result, and moved them into static metadata. Attack success collapsed to near zero, while purpose-built description payloads reached 41.8% on the same model.

The call the agent then makes is unremarkable in every respect but one. It uses a tool the agent is registered to use. Its schema validates. Its session is authorized. What differs is a destination, a path, a table name, or an argument value that this agent has never used before. The agent that has read the ticket store and the knowledge base for three months queries the customer table. The agent that has always emailed internal addresses emails an external one. The agent that reads configuration files reads a key path.

Every check the call meets, it passes, because every check was built for an actor who should not be there, and this actor was provisioned. This is coercion: a trusted input redirecting an agent’s authorized capability toward someone else’s goal. Every step is allowed. The sequence is the attack.

The sequence is also the one object none of the screens hold. Input screening had the result and no call. Action screening has the call and no result, by design. Output screening arrives after the call has already run. The pair exists for a few hundred milliseconds inside one turn, and the only party that sees both halves is whatever watched the agent execute. Catching it fast enough to matter means catching it there, with the agent’s history attached, because the call’s only distinguishing feature is that this agent has never made it.

A coerced call has no precedent in the agent’s own history

Seeing the pair takes three things: a baseline, a deviation rule, and an enforcement path that starts before anything is blocked.

The baseline is a record of what one specific agent does when it is not being coerced. For this attack it has to carry four fields: which tools the agent invokes, which argument values it uses against them, the order it calls them in, and the file, process, and network activity each call produces underneath. The fourth field is what distinguishes an execution baseline from a tool-call log, because the attack’s consequence lands underneath the call. ARMO builds this as Application Profile DNA (APD™), a per-agent behavioral baseline assembled from kernel-level observation of what the workload does, so the record comes from execution. A declared tool list is what the attacker is writing against; an observed one is what he cannot query.

The deviation is a call in the turn after a tool result that has no precedent in that baseline. Either the tool itself is one this agent has never called, or the tool is familiar and the argument is new: a first-ever destination on the send tool, a first-ever table on the query tool, a first-ever path on the read tool. A baseline that records only that this agent sends email passes the argument change without registering anything. One that records which destinations it has sent to flags it on the first call. When the deviation fires, the result event that preceded it and the call that followed it land on one timeline as a single Attack Story, so the responder reads one incident with both halves on it. The same signal covers tool misuse that arrives without any injection at all.

The enforcement path runs from audit to enforce. In audit, deviations are recorded against the baseline and nothing is blocked, which is how the policy gets proven safe against production traffic before it can break anything. In enforce, the deviating call is stopped. Credential isolation closes the loop from the other side, so a coerced read of a credential path that clears every other check returns nothing an attacker can use. For coding agents in remote development environments, that is the difference between an injected instruction and an exfiltrated cloud login. Runtime behavioral security for AI workloads is what makes the per-agent view available without instrumenting each framework separately.

An attacker who knows a baseline exists can work inside it, widening an agent’s behavior gradually so that no single call clears a threshold. That attack is real and it is the hardest case here. What it costs him is where he has to do the work: a baseline is built from one agent’s history in one cluster, so it cannot be downloaded, studied offline, or practiced against before the attempt.

For a platform team, the cost is a sensor with no code changes and no sidecars. For a CISO, the output is one timeline per incident that names the result and the call.

Screen the result, then instrument the call that follows it

The screens stay. They remove the opportunistic volume, and every result they clear is one fewer the baseline has to adjudicate. What they cannot carry is the targeted payload written for your stack, because that payload passes input screening as content and produces a call that passes action screening as authorized.

The pair is what the attacker cannot avoid producing. To finish, the injected instruction has to make a real tool do something real, and doing something real leaves a call against a history the attacker never saw.

Three actions, in order. Re-label your tool inventory by field, and rank agents by untrusted fields paired with state-changing tools. Baseline the agents at the top of that list. Run in audit until the deviation rate settles, then enforce.

Watch a demo to see what a coerced call looks like against the agent’s own history.

Frequently Asked Questions

How do I find which tool outputs in my agents are attacker-writable?

Work one agent at a time and read the return schema of each tool it can call. The tool’s name tells you nothing about the fields. Any field typed as free text is a candidate, and the label depends on who can write to it: customers, outside contributors, inbound email parsers, and web forms all make a field untrusted. Ticket bodies, PR descriptions, CRM notes, calendar descriptions, and log lines are the fields that most often carry outsider text into a production agent. Record the label next to the field, and rank agents by how many untrusted fields they read against how many state-changing tools they hold.

Does wrapping tool output in delimiters or repeating the user instruction after it stop this?

It reduces the rate of opportunistic payloads and should stay in place for that reason. Under adaptive testing, delimiter conventions and instruction sandwiching moved from originally reported attack success in the low-to-mid twenties into the mid-to-high nineties once the attacker optimized against the design, because both rely on the model honoring a marking convention that an injected instruction can address directly. Treat them as filters on volume and assume a payload written for your stack passes them. The control that does not depend on the model’s cooperation is the one that reads the call the model produces.

Should tool results go through the same classifier as retrieved documents?

Yes. OWASP’s prevention guidance lists tool output alongside RAG documents and web pages as content that input screening should cover, and a classifier that only reads user prompts leaves the highest-salience channel unread. The classifier’s limit is the same for both: it sees the text alone, before any decision, and passes anything that reads as benign data. It cannot see the call the text will cause, so screening the result and instrumenting the call are two separate controls covering two separate moments.

How is tool-output injection different from MCP tool poisoning?

Entry point and timing. Tool poisoning places the instruction in a tool’s description at registration, before any prompt exists, where it competes with every other tool’s metadata and needs a trigger condition to fire. Tool-output injection places the instruction in a tool’s execution result, which is the most recent content in the context window when the agent decides its next action. Benchmarks that moved standard tool-output payloads into descriptions saw success collapse, which is why the two need different payload grammar and why the tool result is the higher-volume vector.

What telemetry do I need to see the result-to-call pair?

Four streams, each attributed to one specific agent. Tool invocation records that carry argument values, since the highest-yield coercion changes only an argument, and turn boundaries, so a call can be tied to the result that immediately preceded it. Kernel-level file, process, and network activity for the agent workload, which eBPF collection provides without code changes and which shows what the call did underneath. And identity attribution that resolves the workload to one agent, because two agents in one namespace have two different definitions of normal.

Close

Your Cloud Security Advantage Starts Here

Webinars
Data Sheets
Surveys and more
Group 1410190284
Ben Hirschberg CTO & Co-Founder
Rotem_sec_exp_200
Rotem Refael VP R&D
Group 1410191140
Amit Schendel Security researcher
slack_logos Continue to Slack

Get the information you need directly from our experts!

new-messageContinue as a guest