Inspiration
Brave's security team asked Perplexity Comet to do the most boring thing you can ask a browser agent to do: "summarize this page." The page had instructions hidden in it in white-on-white text and an HTML comment. The agent read them, went to the user's email, pulled out a one-time passcode, and sent it to a stranger.
The user typed six words. They were robbed by the page.
We went looking for the defence and found an uncomfortable pattern instead. A 2026 study of leading agents on GPT-5 and Gemini found not a single attack scenario was consistently blocked. A survey of 12 published defences found adaptive attacks bypassed more than 90% of them, and most of those defences had originally reported near-zero bypass rates. In April 2026 the Cloud Security Alliance confirmed indirect injection had crossed from proof-of-concept into live exploitation. In May, the Five Eyes agencies issued joint guidance naming prompt injection as a core manipulation route for agentic AI.
Meanwhile 83% of organisations are deploying agentic AI and 29% feel ready to secure it.
So we stopped trying to build a better detector. Almost everyone in this space builds a classifier that tries to spot the injection, and the literature above is twelve consecutive obituaries for that approach. We took the opposite position:
We assume the injection succeeds at fooling the model. We make it fail at producing an effect, and we make it visible to the human.
That is the whole idea. Tracer does not stop prompt injection. Nobody has. It assumes the model gets fooled and breaks the step after that.
What it does
Tracer is a provenance firewall for AI agents. Every piece of untrusted content the agent reads is registered as a span with its origin, its DOM path, and whether a human could actually have seen it. Every tool call the agent proposes is checked against those spans before it executes.
The rule that does most of the work reads like this:
The agent is about to send something somewhere you never mentioned, and the only place that destination came from is a web page.
That conjunction is the exfiltration signature. It is narrow on purpose. Blocking every recipient you did not name would break every legitimate "email the author" request, so destinations we simply cannot trace are held for approval with the provenance chain shown. The hard block needs both halves: absent from your instruction and present in untrusted content.
There are three surfaces on one codebase:
- sandbox — a mock agent, a mock inbox and a local attack range, so you can watch the mechanism work without wiring anything up
- range — 16 known injection classes pointed at whatever agent you configure, emitting a scorecard for that agent
- proxy — an MCP proxy that evaluates every forwarded tool call, and an MV3 browser extension that does the same inside the page
How we built it
One host-agnostic core, two adapters. @mukeremshifa/tracer-core holds the tiers, the destination rule, the overlap scan, the plan-then-execute loop and the analyser. It knows nothing about Express, about mock inboxes, or about what a "tool" is. The host injects tool execution and configures tiers.
Two layers, and one of them does not trust the model.
Layer A, declared provenance. Every tier-1 and tier-2 tool takes a mandatory derived_from: the span IDs whose content informed its arguments. It is cheap and it produces good explanations. It is also bypassable, because an injection can simply instruct the model to lie, so it is never load-bearing on its own.
Layer B, enforced overlap. Independently of what the model declares, our own code scans tool arguments for overlap with untrusted content, after normalising that content so encoding the payload does not hide it. Zero-width steganography and base64 are both decoded first. An injection cannot talk its way past code it never sees.
In the shipped demo run, Layer A fails exactly as predicted: the injection tells the model not to mention the step, so it declares derived_from: []. Layer B catches it anyway, and the interface shows the contradiction rather than hiding it. When the two layers disagree, saying so is more informative than either one agreeing.
The key technical call: no server-side browser. Visibility is a rendering property. Whether a human could have seen some text depends on computed styles and real layout, so the analyser runs where the page renders, in a sandboxed iframe in the browser, using getComputedStyle against real layout.
This is not a shortcut around a headless browser. In a real browser agent, the agent is the browser, so running the analyser in the page is the architecturally correct place for it. It also means the X-ray visualisation is nearly free because we are already in the DOM we need to light up, the Arena works instantly on a stranger's submitted page in their own browser, and there is no Playwright image and no 400 MB cold start. The iframe is loaded without allow-scripts and range pages are served with script-src 'none', so untrusted markup renders and computes but cannot execute.
Challenges we ran into
The firewall was a delivery vehicle for the attack it existed to stop. The MCP proxy originally republished tool descriptions verbatim. A description is content: it comes from a server we did not write and it reaches the model before any call is made. We had built a clean pipe for the MCP-native injection class. Descriptions now go through the analyser at listTools time, one that reads as instructions is republished fenced and named as content with its hidden characters decoded in the open, and a destination appearing only in a tool description is refused outright.
We cannot force plan-then-execute on a client that will not cooperate. The proxy offers tracer_begin_task, and most clients will never call it. The tempting response is to escalate everything to a human prompt, but holding every read behind an approval dialog is precisely how a firewall gets switched off in week one. So Tracer degrades instead: the frozen-plan rules stand down and say so in every decision record, tier-1 reads are allowed, and a goal is inferred from the agent's first read and labelled inferred everywhere it is relied on. What does not relax is the destination rule, because it compares against the spans rather than against a plan.
An audit log that keeps the secret it was protecting is a new vulnerability with a reassuring name. Every proxy decision is written to a SQLite file, and secret values are masked before they are stored.
Deciding what not to build. We deliberately did not build an LLM-gateway adapter or per-framework middleware. The gateway is strictly weaker than the MCP proxy, with the same blindness and worse ergonomics, and framework middleware is N integrations for one capability.
Accomplishments that we're proud of
Against 16 injection classes, under the harder of two user goals, where send_email is on the frozen plan and the destination rule has to do the work alone:
16/16 attacks robbed the unprotected agent. 0 landed through Tracer. 14/16 of the user's actual tasks still completed.
That last number is the one we care about most. A firewall that stops the attack by stopping the agent is not a firewall, it is an off switch. The legitimate email still goes out after the hostile one is refused.
The scorecard is generated by npm run eval and published including anything that gets through. The Arena is a public page where anyone can write their own injection, watch it planted into a live page, and watch both agents run against it, with successful bypasses published on a Hall of Bypasses under the submitter's name. We do not fear the bypasses. A defence that displays its own failures reads as real engineering; a defence claiming a clean sweep reads as a demo that was rigged.
What we learned
The honest version of a security claim is more persuasive than the strong version, and it is also the only one that survives a competent question. Every time we wrote down a limitation instead of hiding it, the architecture got better, because a limitation you have written down is one you can design around.
We also learned where the ceiling is. Tracer does not cover exfiltration through a channel we do not model, such as timing or DNS or a tool nobody tiered. It does not cover injections that cause harm without moving data, like destructive actions rather than disclosure. And a semantically laundered destination that the model reconstructs rather than copies will get past the overlap scan. Those are real gaps, and we would rather list them here than have them discovered on camera.
The browser extension cannot sit inside a closed product like Comet, either. There is no supported way to intercept another extension's privileged calls, and claiming otherwise would be exactly the kind of unfalsifiable security claim Tracer exists to argue against.
What's next for Tracer
Extracting the core properly is what makes new adapters cheap, so the near-term work is breadth: more hosts, more tool surfaces, and a tier configuration people can share and diff rather than each writing their own.
The deeper work is the semantic gap. Our overlap scan is a string-level defence with normalisation in front of it, which is robust against encoding but not against a model that reads an address and rebuilds it from parts. Closing that without reintroducing a classifier, and without the false-positive rate that makes a security tool get uninstalled, is the interesting problem.
Tracer also sits adjacent to enterprise agent authorisation and capability scoping. derived_from and the capability tiers are, in effect, a provenance-scoped authorisation check on every single agent action, which is a natural fit for the MCP authentication work happening in that space.
We have used Anthropic's leading coding platform Claude Code throughout the development of this project.
Log in or sign up for Devpost to join the conversation.