Screen tool results for prompt injection before the agent reads them

Prompt injection is not mainly something a user types. It arrives in tool results: the web page the agent fetched, the file it read, the issue body it pulled from an API, the response an MCP server returned. All of that is text somebody else wrote, and the agent reads it in the same context as instructions from you.

That is the asymmetry worth naming. You chose the tool, so the call looks fine. The content coming back is chosen by whoever wrote the page, and there is no point in the chain where the agent is told which of the two it is reading.

The channel is every result, not just the web #

What the screen looks for #

Code goes first and finds the places text is trying not to be read by a person: zero-width and bidirectional control characters, HTML comments, blocks hidden with CSS, and long base64 runs. Those are listed as hidden regions in the state, and the model is asked what they say rather than being trusted to notice them.

The result itself is split into numbered blocks and screened as a unit, so one answer can point at the block it means. A result too large for one request is split at paragraph boundaries into several, and the verdict is the highest across them.

Annotate, or withhold #

These are different operations and they are deliberately kept apart. Annotating returns the result exactly as it arrived with a banner in front and the flagged block wrapped, because the point is that the agent should read it and know what it is looking at. Withholding replaces the body, because the point is that it must not.

Severity selects the tier, it does not gate the screen. On the email set most attacks are low-harm, the sort that asks for a joke or a translation, and gating on severity would let them all through.

agent-chaperone show 4f1c9ab203

A withheld result is still on your machine in full. The command above prints the original, so withholding keeps the text away from the agent rather than away from you.

What it catches, and what it does not #

Counted at 0.5, the threshold the shipped rule acts at, on one dated run against public benchmarks.

Counts rather than rates, because a rate hides how many items it was measured on.
SetItemsTo catchCaughtMissedFlagged in error
InjecAgent tool responses1,3941,0548482069
BIPIA email250200165350
Benign documents that discuss injection63none--7

The largest set is also the weakest. Of 1,054 injected tool results, 206 were not flagged at all, and of those that were, only 359 crossed the bar to be withheld rather than annotated. Anyone telling you a screen like this catches everything has not counted.

The full results, every threshold, and the misses named. The recorded model responses are committed to the repository, so the scorer reproduces these numbers without an API key.

The thing it gets wrong most #

Writing about prompt injection looks a lot like performing one. Security documentation, READMEs of tools like this one, and blog posts quoting an attack are the main false-positive class, and the question's own criteria try to draw that line: discussing or quoting an instruction is not issuing one.

On 63 such paragraphs, 7 scored at or above 0.5. The two highest were a documentation header addressed to automated readers, and code containing literal question text. If your agent reads a lot of security writing, expect banners it did not need.

Limits #

The false criterion on the main question, and the second question that was measured and dropped for adding no signal, are both in the design document.