The Lethal Trifecta: Why Your Coding Agent Is a Loaded Gun
Between January 7 and January 15, 2026, the security firm PromptArmor publicly disclosed indirect-prompt-injection data exfiltration in four shipping AI products: Notion AI, IBM’s “Bob”, Superhuman AI, and Anthropic’s Claude Cowork. Eight days. No CVEs assigned. The same root cause every single time.
Every one was a textbook instance of what Simon Willison named the “lethal trifecta” on 2025-06-16: private data, exposure to untrusted content, and a way to communicate externally (simonwillison.net, 2025). Put all three in one agent surface and exfiltration stops being a risk. It becomes a certainty waiting for the right input.
Here’s the thesis. The trifecta is not a vibe, and it’s not a threat-model poster for the security channel. It’s a buildable design constraint. You don’t solve it at the model; you break it at the tool boundary. This post shows how, specifically for a coding-agent-plus-MCP setup: the place most engineers now run an LLM with file access, shell access, and a fistful of third-party tools.
I surveyed the broader 2026 CVE surge in the dual-angle security post. That CVE survey is one altitude. This is the focused cut: the exact boundary where the injection actually lands, and what you change in your own config to stop it.
Key Takeaways
- A 2026 systematization of knowledge over 78 studies found prompt-injection attack success rates exceeding 85% against state-of-the-art defenses under adaptive attack, with most of 18 analyzed defenses below 50% mitigation (arXiv:2601.17548, 2026). You will not filter your way out.
- The lethal trifecta (private data, untrusted content, an exfiltration vector) is exploitable whenever all three combine in one surface; prompt injection has been OWASP’s #1 LLM risk since the list began.
- The fix is architectural: the trifecta is a property of your tool graph, which you own. Break one leg at the tool boundary so the three never assemble in one capability.
- When the exfiltration leg is “run a command”, data leakage becomes remote code execution: CVE-2026-30615 (Windsurf) is a zero-click MCP RCE, CVSS 8.0 (NVD, 2026).
What is the lethal trifecta in AI agents?
The lethal trifecta is the combination of three agent capabilities that, present together, make data exfiltration unconditionally exploitable: (A) access to private data, (B) exposure to untrusted content, and (C) the ability to communicate externally. Willison coined the term on 2025-06-16. The canonical example was a GitHub MCP server that mixed all three in one tool: it read attacker-filed public issues (untrusted content), reached private repositories (private data), and opened pull requests (an exfiltration vector) (simonwillison.net, 2025).
Read that as an engineer, not a threat modeler. Each leg maps to a tool capability you grant. Private data is a token, a repo mount, a database connection. Untrusted content is any input you didn’t author: a fetched web page, a PR diff, an issue body, an uploaded file. The exfiltration vector is any outbound channel: a network call, a rendered image, a PR, a webhook. The trifecta is a property of your tool graph. And the tool graph is something you configure, version, and own.
Why can’t the model just refuse the malicious instruction? Because it can’t reliably tell your instruction from the attacker’s. Prompt injection has sat at OWASP LLM01, the number-one risk for LLM applications, since the list began (OWASP). The reason it never moves down the list is structural: an LLM reads trusted instructions and untrusted content in the same context window, with no reliable boundary between them. There is no “system voice” the model can verify. To the model, the poisoned issue body is just more text.
The definition is the whole engineering payload: the lethal trifecta is private data plus untrusted content plus an exfiltration vector, and it is fatal whenever all three combine in one agent surface. It has been OWASP’s LLM01 since the list began, because models cannot reliably separate trusted instructions from untrusted input in a shared context window.
Eight days, four products, one root cause
In January 2026 the rule stopped being theoretical. PromptArmor walked four shipping products into the same trap inside a single week, and each is the same trifecta with a different exfiltration leg. The pattern is the lesson, so hold the legs in view as you read.
- Notion AI (disclosed ~2026-01-07): exfiltration through a Markdown image that rendered before the approval prompt fired. The egress happened pre-consent (PromptArmor).
- IBM “Bob” (~2026-01-07): a malware download and execution via a process-substitution bypass of an “always allow” rule. The permission wall was the leg that failed.
- Superhuman AI (coordinated disclosure ~2026-01-12): exfiltration through a whitelisted Google Docs domain plus image rendering. The content-security allowlist was the unguarded egress (PromptArmor; Willison).
- Claude Cowork (launched ~2026-01-13, exploited ~2026-01-15): file exfiltration via a malicious uploaded skill using 1-point white-on-white hidden text. The untrusted content arrived dressed as a “skill”, which is to say as loadable code.
Notice what the exfiltration leg was in three of the four: a rendered image, a CSP-allowlisted domain, an approval that fired too late. Each felt like a trusted channel. None of them were. That’s the trap that makes the trifecta lethal rather than merely risky. The vector doesn’t have to look like an exfiltration vector. It just has to carry bytes to somewhere the attacker can read.
The detail that should change how you think about this: no CVEs were assigned. These weren’t memory-corruption bugs with a patch on the way; they were the products working as designed, where the design happened to assemble all three legs. A CVE implies a fix you can wait for. A design flaw implies a fix you have to build. When the vendor’s position is “the user permitted it”, the security boundary is wherever you decide to put it, which is the whole argument of this post.
A note on sourcing, because it matters here. Aggregator write-ups of this cluster circulated fast, and the aggregators are Tier 3. The load-bearing sources are PromptArmor’s own incident posts and Willison’s notes; cite those, not the secondhand roundups. Where I couldn’t pin a per-product date to a primary post, I’ve kept the framing to the verified window of January 2026 rather than overclaim a specific hour.
Four shipping AI products (Notion AI, IBM Bob, Superhuman, and Claude Cowork) exfiltrated private data through indirect prompt injection in roughly eight days in January 2026, with no CVEs and one root cause. Each granted private data, untrusted content, and an exfiltration vector in a single surface (PromptArmor; Willison, 2026).
Can you filter your way out of prompt injection?
No, and that’s the uncomfortable part: defenses do not hold. A 2026 systematization of knowledge synthesizing 78 studies and cataloging 42 attack techniques reports prompt-injection attack success rates exceeding 85% against state-of-the-art defenses under adaptive attack, and finds most of 18 analyzed defenses achieve under 50% mitigation (arXiv:2601.17548, 2026). The paper names the tools in scope, including Claude Code, GitHub Copilot, and Cursor. This is not a long tail of exotic payloads. It’s the median result against the best filters we have.
The empirical client comparison points the same direction. A March 2026 study tested seven AI development tools against injection and found a wide spread of robustness: Claude Desktop showed strong guardrails, while Cursor showed high susceptibility to cross-tool poisoning (arXiv:2603.21642, 2026). Treat that as a qualitative posture, not a leaderboard. The honest read isn’t “pick the client with the best score”; it’s “even the careful ones get beaten, so don’t bet the system on the filter.”
What does “adaptive attack” mean, and why does it matter so much? A static filter is tested against a fixed list of known payloads, and it can look great in that setting. An adaptive attacker reads your filter and writes the next payload around it: encode the instruction, split it across turns, hide it in a code comment, phrase it as a citation. Every new defense layer becomes one more thing the attacker shapes their input against. That’s the structural reason the success rate stays above 85% no matter how many classifiers you stack. You’re playing whack-a-mole against an opponent who gets to see the board.
Input filtering, instruction hierarchy, and trained classifiers are all incremental, and adaptive attackers route around each new layer. That’s why the “wait for the labs to solve alignment” posture is a trap: there is no patch coming that flips LLM01 off the list. The model is a probabilistic component that will, often enough, follow instructions hidden in the data you feed it. Prompt-injection attacks succeed more than 85% of the time against state-of-the-art defenses under adaptive attack, and most defenses block under half of attempts; the only reliable move is to deny the combination, not to detect every payload (arXiv:2601.17548, 2026).
The boundary is the tool, not the model
Because you can’t fix the model, you fix the surface the model acts through. Restating the engineering claim: the trifecta is a property of your tool graph, and the tool graph is code you own. The fix is to break one leg at the tool boundary so the three never assemble in a single capability. This is the SOPHOS “blast-radius reduction” idea, generalized to a coding agent (SOPHOS).
Three concrete break-points, in order of how cleanly they sever a leg:
- Remove the exfiltration leg. Put a broker between the agent and the outside world. A broker process holds the credentials, calls a fixed schema, returns only the parsed response, and enforces a per-tool egress allowlist. If the agent has no general outbound capability, there’s no channel to exfiltrate through.
- Seal tools with narrow parameters. A “send email” tool with a fixed sender and a domain allowlist is a smaller surface than a free-form “make an HTTP request” tool. Give the model a request-body schema, not a blank authoring canvas. The tool’s shape is itself a constraint.
- Per-agent tool allowlist. Each agent role runs only with explicitly approved MCP servers. Adding a tool is a reviewed change, not an ambient default.
The move that makes this actionable: map your coding agent’s actual tools (Read, WebFetch, Bash, each MCP server, each loadable skill) onto the three circles, and find the single capability that touches all three. That capability is your loaded gun. A WebFetch tool that an agent can aim at an attacker-controlled URL after it has read your private repo is the trifecta in one call: it pulls untrusted content, it sits next to private data, and the fetch itself is the egress. You don’t need to audit everything. You need to find the overlap and cut it.
This is why tool design is a security discipline, not just an ergonomics one. The tool’s schema is the prompt the agent reads, and a narrow, well-shaped tool is also a smaller trifecta surface. The same argument runs through why an MCP server should be tool-shaped rather than an API mirror: every extra raw capability you expose is another way for the three circles to overlap. The boundary fix and the design fix are the same fix.
The durable rule: break one leg at the tool boundary. Because the trifecta is a property of the tool graph and not the model, the defensible control is architectural; deny the overlapping capability with code rather than hoping the model declines the attacker’s instruction.
What is the Agents Rule of Two, applied to a coding agent?
Meta’s Agents Rule of Two gives the trifecta an operational upper bound: within a single session, an agent should satisfy at most two of three properties. Those properties are processing untrustworthy input, accessing sensitive systems or private data, and the ability to change state or communicate externally (Meta, 2025). The trifecta tells you that all three together is fatal. The Rule of Two tells you to pick which two to keep and design the third out of that session.
Applied to coding agents, it gets concrete fast:
- A code-review agent reads untrusted PR content (untrustworthy input) and has repo access (sensitive data). So deny the third leg: no egress. No
WebFetch, no outbound MCP, no webhook. It can read and comment; it cannot phone home. - A research or fetch agent reads untrusted web pages (untrustworthy input) and can communicate out (external action). So deny the second leg: no secrets, no private repos. Run it in an isolated context where the worst it can leak is the public page it just fetched.
Pick the two the role actually needs, and the attacker loses the channel that completes the trifecta. Within a session, grant an agent at most two of: processing untrusted input, accessing private data, and communicating externally; the Rule of Two is the operational dosage for the trifecta diagnosis (Meta, 2025).
One honest caveat, because the security folks will raise it and they’re right to. Critics including Noma and Ken Huang argue the Rule of Two is necessary but not sufficient, especially for multi-agent and cross-tool flows where capabilities recombine across a handoff. Picture the failure: a fetch agent reads an attacker’s web page and writes a summary to a shared scratchpad, dropping its own egress as the rule demands. Then a deploy agent with private credentials and outbound access reads that scratchpad and acts on the smuggled instruction. Neither agent held all three legs. The pipeline did. The trifecta reassembled in the seam between two compliant agents, which is exactly the seam a single-agent rule can’t see.
So treat the Rule of Two as the best practical first cut, not a proof of safety. It shrinks the surface; it does not certify it. Compose it with the boundary controls above, and audit the handoffs between agents as carefully as the agents themselves.
How does zero-click MCP RCE actually happen?
When the exfiltration leg is “run an arbitrary command”, the trifecta stops being about leaked data and becomes remote code execution with no click. The cleanest single case is CVE-2026-30615 in Windsurf 1.9544.26: attacker-controlled HTML silently rewrites the local MCP configuration, registers a malicious STDIO server, and executes commands with no approval dialog and no interaction beyond opening the project. CVSS 8.0 (NVD, 2026; GHSA-wj2m-jvpr-64cq). OX flagged it as the only truly zero-click IDE in its review; Cursor, Claude Code, and Gemini-CLI each required at least one user step, and Google, Microsoft, and Anthropic declined CVEs on the grounds that user permission is required.
The single case is not the story. The systemic backdrop is. OX Security’s “Mother of all AI supply chains” advisory (2026-04-15) described a command-injection pattern propagating from MCP’s STDIO design across more than 15 packages, 7,000-plus exposed servers, and 10-plus CVEs (OX Security; The Hacker News, 2026). Anthropic confirmed the STDIO behavior is by design and declined to change it, putting sanitization on the developer. Spot-checked confirmations from that cluster: CVE-2026-40933 (Flowise, authenticated RCE via an MCP STDIO adapter, CVSS 9.9) and CVE-2025-65720 (GPT Researcher). One more, CVE-2026-30623 (LiteLLM command injection via the Anthropic MCP SDK), is RESERVED on NVD with no public enrichment; I treat it as vendor-confirmed from LiteLLM’s own advisory and won’t quote an NVD CVSS for it.
This is exactly why my code-intelligence MCP runs as a persistent daemon rather than spawning a STDIO subprocess per call. The original reason was performance: a long-lived broker amortizes the index and turns a “who-calls-X” query from tens of thousands of input tokens into a few hundred. But the same architecture is a security property. The daemon answers code questions over a fixed query schema and has no outbound network capability at all. There’s no STDIO command surface to inject into, because nothing spawns a shell per call, and there’s no egress leg to exfiltrate through, because the process can’t open a socket to the internet. The design that solved the scaling problem also closed the RCE class by construction. I didn’t set out to harden it; the boundary just happened to be the right place to stand.
CVE-2026-30615 lets attacker-supplied HTML rewrite Windsurf’s MCP config and run commands with zero clicks, and OX’s April 2026 advisory found the pattern is systemic to MCP’s STDIO design across more than 7,000 exposed servers and 10-plus CVEs (NVD; OX Security, 2026).
A boundary checklist you can ship this week
Five moves, ordered by leverage, that break a leg of the trifecta at the tool boundary for a coding-agent-plus-MCP setup. The egress hook below is a trimmed version of one I run. None of them require a vendor or a model upgrade. All of them are config you control.
- Map your tool graph onto the three circles. List every tool the agent can call and tag each with which legs it touches. Find the capability that touches all three. That overlap is the priority fix, and everything else is secondary.
- Run MCP as a brokered daemon, not STDIO-per-call. A long-lived broker is the natural chokepoint for an egress allowlist and a request-body schema, and it removes the per-call shell that the STDIO RCE class exploits.
- Enforce egress with a hook, not a prompt. Prompt walls failed as governance; a hook runs as deterministic code on every matched event. A
PreToolUsehook can deny any tool call whose arguments carry an outbound URL to a non-allowlisted host, or block a render to an unknown domain after the session has touched private files.
// .claude/settings.json - block egress to non-allowlisted hosts
{
"hooks": {
"PreToolUse": [
{
"matcher": "WebFetch|Bash",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/deny-egress.mjs" }
]
}
]
}
}
// .claude/hooks/deny-egress.mjs - exit 2 blocks the call
const ALLOW = new Set(["docs.internal.example", "api.github.com"]);
const input = JSON.parse(await new Response(process.stdin).text());
const args = JSON.stringify(input.tool_input ?? {});
const hosts = [...args.matchAll(/https?:\/\/([^/"\s]+)/g)].map((m) => m[1]);
const blocked = hosts.filter((h) => !ALLOW.has(h));
if (blocked.length) {
console.error(`egress blocked: ${blocked.join(", ")}`);
process.exit(2); // non-zero tells Claude Code to deny the tool call
}
- Apply the Rule of Two per agent role. The review agent drops egress. The fetch agent drops secrets. Never all three legs in one session, and audit the handoffs where two agents might recombine them.
- Treat MCP config and skills as supply-chain artifacts. Your
.cursor/mcp.json,~/.codex,.claude/, and any uploaded skill are executable code, not configuration. Review them in PRs. Both the Windsurf RCE and the Claude Cowork skill exploit landed precisely because these were treated as settings rather than as code. This is the install-time supply-chain threat model, and it is a separate problem from the runtime injection the rest of this post breaks.
The checklist is the whole post in five lines: map the tool graph, broker MCP as a daemon, enforce egress with a hook, apply the Rule of Two per role, and treat MCP config and skills as code. Each move breaks a leg of the lethal trifecta at the tool boundary, where you have control, rather than at the model, where you don’t.
FAQ
What is the lethal trifecta in AI agents?
It’s the combination of three agent capabilities that, together, make data exfiltration unconditionally exploitable: access to private data, exposure to untrusted content, and the ability to communicate externally (simonwillison.net, 2025). It’s fatal because an LLM can’t reliably tell trusted instructions from untrusted content in the same context window, so any one tool that spans all three legs is a standing exploit.
Can prompt injection be solved?
No, not at the model layer. A 2026 SoK of 78 studies found attack success rates exceeding 85% against state-of-the-art defenses under adaptive attack, with most of 18 analyzed defenses below 50% mitigation (arXiv:2601.17548, 2026). Prompt injection has been OWASP LLM01 since the list began. The only reliable move is to deny the trifecta combination at the tool boundary, not to detect every payload.
What is the Agents Rule of Two?
Meta’s Agents Rule of Two says an agent should satisfy at most two of three properties within a session: processing untrustworthy input, accessing sensitive data, and changing state or communicating externally (Meta, 2025). It’s the operational dosage for the trifecta: pick the two a role needs and design the third out. Critics note it’s necessary but not sufficient for multi-agent flows.
How do I stop a poisoned MCP server from compromising my coding agent?
Run MCP as a brokered daemon with a fixed schema and an egress allowlist instead of STDIO-per-call, enforce outbound limits with a PreToolUse hook, and treat MCP config and skills as reviewed code. The threat is real: CVE-2026-30615 (Windsurf) was a zero-click MCP RCE, CVSS 8.0, and OX found the STDIO injection pattern is systemic across 7,000-plus servers (NVD, 2026).
The trifecta is the diagnosis; the boundary is the cure
The labs are not going to hand you a fixed model. OWASP LLM01 is permanent, and the 85% number is the proof: prompt injection beats the best filters more often than not, and it always will, because the model reads your instructions and the attacker’s in the same breath. What you control is the tool graph, and the tool graph is where the fix lives.
So hold the three pieces together. The trifecta is the diagnosis, the Agents Rule of Two is the dosage, and the egress hook and the brokered daemon are the delivery. Map your agent’s tools onto the three circles, find the capability that touches all of them, and break that leg with code at the boundary. Do that and the January cluster (four products, eight days, one root cause) cannot happen to you, because the gun is never loaded with all three rounds at once.
The boundary is also where governance lives, not just security. The same place you enforce an egress allowlist is the place you decided the permission prompt was dying and moved control into deterministic code. Your coding agent is a loaded gun by default. The safety isn’t a smarter model. It’s a narrower tool.
If it was useful, pass it along.