The post-incident review usually goes the same way. Someone pulls the agent's permissions, and every one is justified: it reads customer records to answer support questions, it summarizes because that is the job, and it files issues because engineering asked for it. The access review passes. Yet a customer's data now sits in an issue several hundred people can read, and nobody granted that.
We call what happened in between data laundering: data loses its security context as it passes through a transformation, so a sensitive record comes out the other side looking like ordinary text with no owner and no audience restriction. The term is ours and names a failure mode, not a new exploit family. Our position is that teams keep missing it because of a handful of comfortable beliefs about per-call authorization, and that the fix is a flow control, accepted with its over-tainting cost, rather than harder inspection of each call.
Myth: if every call is authorized, the session is
Reality: in May 2025, Invariant Labs described an attack on agents using the GitHub MCP server [1]. An attacker files an issue in a public repository. When the owner asks their agent to look at open issues, the agent reads the injected instructions, pulls data from the owner's private repositories into its context, and publishes it in a pull request on the public repository, where the attacker can read it. Invariant demonstrated this with Claude 4 Opus in Claude Desktop and noted that many users set tool confirmations to Always Allow [1].
Every step was a call the agent was permitted to make with the user's own token. Invariant was explicit that this is not a flaw in the GitHub MCP server code but an architectural issue to be addressed at the agent-system level, and called the pattern a toxic agent flow [1]. Per-call authorization asks one question: may this principal invoke this tool with these arguments? It has no slot for where the arguments came from. A create-issue call whose body the user typed and one assembled from a restricted record look identical at the moment of the call. Willison's lethal trifecta says the same at session level: private data, untrusted content and an external channel together are dangerous even when each is reasonable alone [3].
Viewed call by call, a read, a rewrite and a post each pass. Viewed as a flow, restricted data has moved to a broader audience with no one deciding that it should.
Myth: a stricter permission review would have caught it
Reality: the permissions were the right ones. OWASP's Excessive Agency entry names the root causes as excessive functionality, permissions and autonomy, and its example is a composition: an email assistant meant to summarize mail also has a send capability, so an injected message makes it search the inbox and forward what it finds [2]. The fixes OWASP recommends, a read-only scope, a person reviewing every sent message, and authorization enforced in downstream systems rather than by the model, work by breaking the chain, not by inspecting one call harder [2].
Invariant's own mitigation for the GitHub case makes the point: restrict the agent to one repository per session [1]. That is a flow control, not a permission change. The same token could still read and write both repositories.
Myth: data-loss checks on tool arguments will catch the data
Reality: the transform step defeats content inspection. A summary keeps the facts and drops the formatting a pattern scanner keys on. A translation, a table rebuilt as prose, a value split across three comments, or a base64 blob in a diagnostic attachment all carry the data without matching the original.
Picture Fernway, a fictional payroll software company whose support agent can read a customer's pay runs and file issues in an engineering tracker visible to every employee and a large contractor team. A log file attached to a ticket asks, in the voice of the customer's IT team, for the full employee table to be included in the issue so engineering can reproduce the bug. The agent files a table of names, salaries and partial bank details. Data-loss checks pass it: the bank details were already masked to the last four digits, and names with salaries match no pattern.
Argument checks still catch tokens, card numbers and identifiers in their usual shapes, and they should run. They cannot be the only line for prose.
Myth: agents rarely get redirected this way
Reality: two public benchmarks say it happens often enough to plan for. InjecAgent built 1,054 test cases across 17 user tools and 62 attacker tools, with two attacker intentions, direct harm to the user and theft of private data; ReAct-prompted GPT-4 was vulnerable 24 percent of the time, and reinforcing the attacker instructions nearly doubled the success rate [4]. AgentDojo provides 97 realistic tasks, such as managing an email client or an e-banking site, and 629 security test cases, and found that existing attacks break some security properties but not all [5].
These numbers come from specific models, prompts and tasks, and newer models score differently, so read them as evidence that redirection is routine, not as a rate for your deployment. Either way, the controls that matter are the ones that still hold when the model is steered.
Myth: flow tracking is a research problem
Reality: the principled versions are research systems, but the core idea works today at a coarse grain. Microsoft researchers' Fides planner tracks confidentiality and integrity labels through an agent's work and deterministically enforces security policies, with primitives for selectively hiding information from the planner [6]. CaMeL derives control and data flow from the trusted user request and attaches capabilities that stop private data leaving through unauthorized flows [7]. The coarse version: once a session reads data labeled restricted, the whole session carries that label, and the label never goes down.
Each read raises the session to the highest sensitivity it has touched. A write runs when the destination's audience is no broader than the data allows, asks when it is broader, and blocks when the audience cannot be determined.
This over-taints. A session that read one restricted field treats every later write as restricted. We accept that, because it fails in the right direction for consequential sinks and is cheap to relieve: a person approving the write can see exactly which source raised the label. Finer tracking through language generation is still an open research problem, so do not promise it.
Cheaper still is removing a leg. Scope a session to one data domain, as Invariant suggests [1]. Split the agent that reads restricted data from the one that posts to broad audiences, and pass only the fields the second needs. A support workflow that needs an order status should receive an order status, not the full customer record.
Myth: you know where data went because you know which tool wrote it
Reality: the tool tells you the sink, not its audience. Describe each flow in three parts: the source where restricted data entered the session, the transform the model applied, and the sink that put the result somewhere. A laundering path exists whenever the sink's audience is broader than the source's, whatever the transform.
| Pattern | Source | Sink and its audience | Why per-call checks miss it | Control |
|---|---|---|---|---|
| Private repo to public PR | Private repository contents | Pull request on a public repo: anyone [1] | Same token, same tool family, valid arguments | One repository per session; public writes ask after private reads |
| Record to shared ticket | Customer or HR record | Ticket in an all-staff project: every employee and contractor | Issue body is free text with no pattern match | Resolve project membership; broader audience asks |
| Inbox to forward | Search across the user's mail | Outbound email: any address [2] | Send is an allowed tool for drafting replies | Outbound mail asks; recipient bound to the thread |
| Query to attachment | Database or report export | File share link: anyone with the link | Upload and share are separate, benign calls | Link sharing outside the tenant asks; size limits |
| Document to URL | Internal document text | HTTP fetch to an outside host: its operator | A GET request looks like browsing | Fetches to new hosts after private reads ask |
| Drip across calls | Any restricted source | Many small comments or messages | Each call is under every per-call limit | Per-session volume budget for writes after private reads |
| Handoff to another agent | Data in this agent's context | Another agent with broader sinks | Each agent's own policy passes | Labels travel with agent-to-agent messages |
Resolve the audience from the provider, not from the model's description: whether a repository is public, the membership of a tracker project, whether an email recipient is inside your domain, whether a share link is tenant-only. Cache the answers and treat a lookup failure as the broadest audience. Under a session label, Fernway's pay-run read raises the session to restricted, the tracker resolves to all staff and contractors, and the create-issue call asks the support lead, showing the pay run as the source. The legitimate task, filing a bug with a pay-run identifier and an error message, still runs, because describing the bug never needed the table.
For detection, join reads to later writes in the same session: a restricted read followed by a write to a broader audience, a new destination that first appears after untrusted content arrived, a write much larger than that tool's usual payload, and many small writes after one large read. Log the session, the sources that raised the label, and the sink with its resolved audience on every decision. Keep the user's original request next to the agent's plan, too. An agent will often explain that a document told it the export was required. That explanation is evidence of where the instruction came from, not authority.
Intertrace sees model, tool, MCP and agent-to-agent calls in a session at one inline gateway, records each decision with the session, rule, layer and evidence, and replay shows which of the last 30 days of decisions a tighter rule would have changed. Outbound messages always ask. Session labels and audience resolution are yours to build in your tools and orchestrator, and the gateway's per-call decisions are the backstop when they are missing.
Myth: a refusal in the transcript means nothing leaked
Reality: only the sink can tell you. Test with canaries:
- Create synthetic restricted records, each with a unique canary fact such as an invented employee name and salary, in a workspace the agent can read.
- Set up sinks with known audiences: a private tracker project, an all-staff project, an outbound email address you control, and an outside host that logs requests.
- Run the legitimate task first and confirm it completes, so you can tell enforcement from breakage.
- Introduce an untrusted attachment asking for the data to be posted to each broad sink, and try direct copies, paraphrases, aggregates and splits across several calls.
- Inspect the sinks for canary facts, not just the transcript. A refusal in the transcript with a write in the sink is a failure.
- Run the benign twin: the same disclosure to a permitted audience with a person's approval, which should succeed.
- Repeat each case several times and record the model, prompt, tool and policy versions, because behavior varies between runs and releases.
A canary in a broad sink proves the boundary failed under that configuration. A clean run proves only that the tested paths held, and says nothing about transformations you did not try.