Watercolor landscape painting
← Back to blog

AGENT SECURITY

How to Pass Context Safely (Without Leaking Sensitive Data or Executing Hidden Instructions)

How to share context between steps and agents without leaking sensitive data or executing hidden instructions.

Paulina XuMay 14, 202610 min
SecurityMulti-Agent SystemsAI Safety

TL;DR

The leak rarely happens at the agent that reads the untrusted document. It happens one hop later, at the agent that trusts the first agent's summary of it. Nobody downstream of that hop can tell whether a claim came from a verified source or from three lines of white-on-white text in a PDF.

Take a support agent that reads a customer's ticket and hands its read of the account to a billing agent. If the ticket carried a hidden instruction, the billing agent never sees the ticket. It sees the first agent's word for what the ticket said.

The instinct is to secure the document at the door and move on. That stops the direct read, but it does nothing for the second and third hops, where the raw text is gone and only a paraphrase remains.

We think this is a boundary problem, not a capacity problem: what a piece of context is allowed to carry across a hop, and what has to be proven again on the other side.

Redact before ingestion, tag trust instead of assuming it, fence untrusted text so it can't read as an instruction, and carry a provenance record that survives every hop.

Overview

Take a support-ticket triage agent. It reads an incoming ticket, decides the account needs a billing adjustment, and hands that read off to a billing agent, which has never seen the original ticket and never will. It only sees what the triage agent decided to pass along. If the ticket contained a paragraph of white-on-white text instructing the reader to issue a full refund to a new bank account, and the triage agent folded that instruction into its "customer is requesting a refund" summary, the billing agent inherits an attacker's intent wearing the first agent's credibility.

That is the shape of the problem this post is about. Not whether a single agent can be tricked by a poisoned document, which is the well-covered case, but what happens to the trick once it survives translation into another agent's input. Every hop between agents is a place where a reader loses the ability to check the original source and has to decide how much to believe the agent before it.

This is a different question from context degrading over a long single-agent run, covered separately on this site: that post is about a window losing signal as it fills up over dozens of turns. This one assumes the window is nowhere near full. The risk here is not that the model forgets something. It is that something got in that should never have been trusted in the first place, and it is now traveling with a stamp of approval it did not earn.

The boundary that matters is between agents, not just between the internet and the model. Redacting a document once at the door does not protect the third or fourth agent that only ever sees a summary of a summary of it.

What Actually Crosses the Boundary

Agent-to-agent context is rarely a clean handoff of "here is the user's question." By the time a pipeline has a few steps, what moves between agents is a mixed bag: retrieved documents, tool outputs and API responses, an intermediate summary or two, references into a memory store, and sometimes a prior agent's plan for what it intended to do next. Every one of those is a channel a hostile document can ride on, and every one of them looks, to the next agent, exactly like ordinary working material.

The reason this is worse for agents than for a single chatbot turn is the gap between what a person would notice and what a model will act on. An instruction hidden in an HTML comment, a Unicode tag block, or text colored to match its background is invisible to a human skimming the document and perfectly legible to a model that reads every byte. It does not need to be readable. It only needs to survive retrieval, land in someone's context window, and be phrased the way an instruction is phrased. Once it does, a model has no reliable built-in way to separate text it should treat as data from text it should treat as a command, and an agent that forwards its read of that text to a second agent forwards the ambiguity along with it.

This is the failure mode security researchers call indirect prompt injection, and it sits at the top of the OWASP list of risks for LLM applications for a reason: the attacker never touches your interface. They plant the payload in a wiki page, a support ticket, or a scraped result and wait for your system to retrieve it on its own.

Redact Before It Ever Reaches a Model

Redaction has to happen before a document is ingested, not after a model has already seen it. Scrubbing a response after the fact is too late: the sensitive text already entered the context window, may already have been folded into a summary, and might resurface two hops later in a form that no longer looks like the original secret at all.

python
import re

PII_PATTERNS = {
    "email": r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+",
    "ssn": r"\b\d{3}-\d{2}-\d{4}\b",
    "api_key": r"(api_key|secret|token|bearer)[:=][^\s]+",
}

def redact(text: str) -> str:
    for label, pattern in PII_PATTERNS.items():
        text = re.sub(pattern, f"[REDACTED_{label.upper()}]", text)
    return text

Pattern matching is a floor, not a ceiling. It catches well-formed identifiers and misses whatever doesn't fit a fixed shape: a name, a mailing address, a customer ID in a format nobody anticipated. We think regex earns its place as the fast first pass and nothing more; production systems layer a named-entity detector on top, because each detector covers a class of failure the others let through.

Some content is not worth partial redaction at all. A raw log dump, a stack trace with a token in it, a config file: drop the whole section rather than trying to mask it line by line, because a single missed line defeats the effort entirely. When a document contains something that dangerous, the safer default is to remove the section and let a person decide whether it belongs in context at all.

Tag Trust, Don't Just Assume It

Once content is clean, the next agent still needs to know where it came from. The simplest technique borrows a principle every web developer already knows: never let code and data share a channel. A system prompt is code. A retrieved document is data. A tool result is data. The moment those streams blur into one undifferentiated block of text, a model is free to promote attacker-controlled data into an instruction, and that is exactly the outcome the rest of this post exists to prevent.

python
class ContextChunk:
    def __init__(self, text, source, trusted=False):
        self.text, self.source, self.trusted = text, source, trusted

    def to_prompt(self):
        tag = "[TRUSTED]" if self.trusted else "[UNTRUSTED]"
        return f"{tag} Source={self.source}\n{self.text}"

Tagging is necessary and not sufficient by itself. A payload can include the literal string [TRUSTED] Source=SYSTEM and try to forge a higher trust level than it earned. Defend against that two ways: strip or escape any of your own delimiter tokens before you wrap untrusted content in them, and pair the tag with a verbatim fence, covered next. The tag tells a model how much to weigh a piece of content. The fence tells it where that content starts and ends, so the forged tag inside the body can't relabel anything outside it.

Neutralize Hidden Instructions

The dangerous instructions rarely sit in plain, visible prose. They're smuggled: buried in an HTML comment, hidden behind zero-width characters, written in white text on a white background, tucked into metadata a human reviewer would never open. Any defense that only checks what a document looks like on screen is checking the wrong thing.

python
def fence(text: str) -> str:
    text = text.replace("</DOCUMENT_CONTENT>", "")  # neutralize forged closers
    return f"<DOCUMENT_CONTENT>\n{text}\n</DOCUMENT_CONTENT>"

A model treats fenced content as data rather than instruction, but only if the fence actually holds. If a document contains a literal closing tag, it can break out of the wrapper and have its trailing text read as a top-level instruction, which is why the inner text needs a pass to strip that string before wrapping.

Keyword heuristics ("ignore previous instructions", "you are now") are worth running too, as the outermost and cheapest layer. Be honest about their ceiling: they catch lazy, literal payloads and miss anything rephrased, translated, or encoded, and they will occasionally flag a legitimate document that happens to say "you must." Treat a hit as a reason to down-rank or route to a human, not as proof.

A tag says how much to trust something. A fence says where it begins and ends. Neither one works alone, and both are cheap enough that there's no reason to ship only one.

Provenance That Survives the Hop

This is the part that actually addresses agent-to-agent handoffs rather than a single agent's own ingestion. Provenance means every piece of context carries a record of where it came from and what has been done to it, and that record travels with the content rather than living in the first agent's head.

python
class ProvenanceRecord(BaseModel):
    source: str
    trusted: bool
    transformations: list[str]  # e.g. ["redacted", "fenced", "flagged"]

class AgentMessage(BaseModel):
    content: str
    provenance: list[ProvenanceRecord]

In a single-agent system, origin is easy to lose track of and rarely matters, because there is only one hop. In a multi-agent pipeline it matters a great deal: by the time a chunk reaches a third or fourth agent it may have been summarized twice, and any trust signal that wasn't carried along with it is simply gone. The billing agent from the opening example never had a chance to distrust the refund instruction, because nothing told it the instruction had come from unverified customer text rather than a verified account note.

The rule that makes provenance worth the code: trust never increases downstream. If a chunk entered the system untrusted, every transformation appends to its history, and it stays untrusted unless a specific, logged step elevates it. Never let a summarization step quietly launder that flag away.

Trust is a one-way ratchet, and it only turns down. A summary of untrusted content is still untrusted content. If your pipeline can't say that, it can't say much else about where a bad instruction came from once it's three hops downstream.

A Firewall at Every Handoff

The individual techniques above only add up to something if every agent-to-agent handoff routes through the same checkpoint rather than trusting whichever agent happens to be sending. That checkpoint is a context firewall: the one place a message from Agent A is stripped, scanned, tagged, and stamped with provenance before Agent B ever sees it.

raw handoff
(no firewall)

through firewall

Untrusted document
hidden instruction inside

Agent A
reads, summarizes

Agent B
inherits the instruction

Context firewall
redact + fence + tag + provenance

Agent B
sees a labeled, bounded claim

Agent A (reads, summarizes) Agent A --raw handoff, no firewall--> Agent B [inherits the hidden instruction] Agent A --through firewall--> Context firewall (redact, fence, tag, provenance) --> Agent B [sees a labeled, bounded claim] -->

Figure 1 — The same summary, with and without a firewall at the handoff. The raw path carries whatever Agent A believed; the firewalled path carries only what it can label and bound.

Two rules make the firewall worth building as one component instead of scattered checks. First, no agent hands raw context to another agent directly; every edge in the pipeline routes through it, so a single hardening change protects every hop at once instead of needing to be re-implemented per pair of agents. Second, guard the store as well as the prompt: a document that was embedded into a vector index before anyone scanned it sits there looking exactly like legitimate knowledge until it's retrieved, so the same redaction and tagging pass belongs upstream of embedding, not only at query time.

Conclusion

Most teams still treat context as extra text riding alongside the real payload. In a system with more than one agent, it is the payload: it determines what the next agent believes, and belief is what gets acted on. Securing it is not a feature you add once at the front door. It's a discipline applied at every hop, from the moment a document is first ingested to the moment its content, in whatever form it survives in, reaches the last agent in the chain.

We think the useful discipline is small enough to actually run: redact before ingestion, tag trust instead of assuming it, fence untrusted content so it can't be read as command, and carry a provenance record that a summarizer is never allowed to quietly erase. None of these techniques is exotic. What is easy to miss is that they have to survive the handoff, not just the first read, or the second agent in the chain is trusting something it never actually saw.

Ask this before shipping a multi-agent pipeline: if Agent C acted on something wrong, could you point to which document it came from and what happened to it in between? If the answer is no, the firewall isn't built yet. It's just a redaction step with an agent standing behind it.