Detection-based defenses all try to answer the same impossible question: is this span an attack? The answer isn't in the text — “delete the branch and open a PR” is a normal instruction or a live injection depending only on where it came from and what the agent is about to do with it. So stop asking. Ask the question that actually has an answer: where did this value come from?
That question is provenance, and tracking it through a pipeline is taint tracking — a technique borrowed from decades of program security and pointed at LLM agents. It doesn't detect the attack. It arranges the system so that the one moment that matters — an untrusted-derived value reaching a sensitive action — is a decidable event you can gate.
Tag every value by origin
The idea is old and blunt. At the moment a value enters the pipeline, tag it by where it came from. Two labels are enough to start:
- Trusted — the system prompt, your own config, values your code computed from constants. Things an attacker cannot author.
- Untrusted — anything the user typed, the repository returned, a web page contained, or a tool emitted. Things an attacker can author.
The tag is metadata that rides with the value. When a value is derived from another — concatenated, parsed, summarized — the derived value inherits the taint of its inputs. Propagation is deliberately conservative: if any input was untrusted, the output is untrusted. You would rather over-tag than miss a path.
Gate the sensitive sinks
Taint on its own does nothing. The enforcement is a single rule at the point of action: a sensitive action invoked with untrusted-tainted arguments is a policy violation. Merging code, moving money, changing access, sending mail — these are sinks. When a call reaches a sink and any argument carries untrusted taint, the agent does not auto-commit. The call is diverted into a proposed / review lane.
In that lane, a human approves the write — or, better, a stricter policy re-derives the argument from a trusted source so it never needed trust in the first place. The dangerous action still happens. It just stops happening silently. That is the whole move: a confused-deputy write, where an injected instruction rides the agent's authority into a mutation, becomes a human-gated write.
How this ties to the trifecta
Taint tracking doesn't remove a leg of the lethal trifecta. The agent still has private data, still sees untrusted content, still has a channel out. What it does is insert a checkpoint on the exact edge that converts an injection into harm: the moment an untrusted-derived value arrives at a sensitive sink. Privilege separation isolates the model that reads untrusted text; taint tracking watches the value flowing toward the write. They stack.
The unsolved edge: taint doesn't survive the model
Here is where we stop selling and start being honest. Classical taint tracking works because a program's data flow is inspectable — you can follow a byte from source to sink. An LLM breaks that. The moment a tainted span and a trusted span both sit in a model's context, the output tokens are a blend. There is no reliable way to say which output characters were caused by the tainted input. Propagating taint across an LLM boundary is an open research problem — semantic taint tracking, Agent-Sentry, and execution-provenance surveys are all early work, not settled engineering.
So the honest default is coarse and conservative: any value produced by a model that saw untrusted content is treated as tainted, full stop, until it is re-validated against a trusted source. You are not tracking taint through the model. You are treating the model as an opaque tainting box. That is defensible, and it is a real limitation to state out loud.
The failure mode you will actually hit: review fatigue
If you treat the model as tainting and then gate everything, every action lands in the human lane and your reviewers start rubber-stamping. A gate that is always triggered is theater. The entire value of this pattern lives in scoping the word sensitive tightly — gate the writes that mutate high-value state or leave the perimeter, and let everything else flow untouched. Over-tainting is a self-inflicted denial of service, and it is the most common way teams quietly kill the control while believing they still have it.
Cost is medium-to-high: this is pipeline instrumentation, not a config flag. You are adding a tag to every value, propagation logic to every transform, and a policy gate in front of every sink. The payoff is a decidable security property on the one edge that matters.
What we tell clients
If your agent can mutate anything that matters, enumerate the sensitive sinks first — that list is the whole design. Tag origins at ingest, propagate conservatively, treat the model as a tainting box, and put a gate in front of each sink that refuses to auto-commit on untrusted taint. Prefer re-deriving an argument from a trusted source over asking a human to click approve; the best gate resolution needs no trust at all.
And keep it in proportion. Taint tracking is a promising but partial control — a checkpoint, not a wall. It cannot follow taint through the tokens, so it must be paired with the rest of the stack: privilege separation upstream, least-privilege tooling so an approved-by-mistake write can only do so much, and audit logging so every tag and every gate decision is reviewable after the fact.
This is one pattern in our Defense Architecture catalog. The pattern page for Taint Tracking & Provenance has the trust boundaries, threat surface, and architecture diagram in full.