Sneppard Sniffer
A Chrome extension that flags prompt injection aimed at AI browsing agents, by looking for instruction-like text crossed with evidence someone took pains to hide it.

A Manifest V3 Chrome extension that reads a page the way an AI assistant reads it, meaning all of it, and flags text that seems to be addressing the assistant rather than you.
The gap it lives in
An AI browsing agent ingests the whole DOM. You read the part that gets painted. Those are not the same document, and prompt injection lives in the difference: text put on a page for the agent and positioned so that you’ll never come across it.
The uncomfortable part is who that exposes. People who have an agent read a page for them are, by and large, people who didn’t fancy reading it themselves. That is the entire proposition. So the one person placed to notice a paragraph of instructions addressed to a machine is exactly the person who isn’t looking.
How it decides
Two signals, and it wants both. Instruction-like language on its own is unremarkable, since plenty of ordinary pages discuss talking to models. Hidden text on its own is even less remarkable, since the web is full of it. Together they are worth a second look.
The first signal is a set of 27 weighted patterns, each worth 5 to 10
points. They cover the usual repertoire: “ignore previous instructions”,
persona hijacking (“you are now [role]”), system prompt overrides, obfuscation
markers such as DAN and fake chat tokens like <|im_start|>, credential
exfiltration attempts, negation-based manipulation (“do not reveal”), and
conditional injection (“when asked … say …”).
The second is evidence the text was concealed, worth +3 points per vector.
| Vector | What it looks for |
|---|---|
| Hidden CSS | display:none, visibility:hidden, opacity under 0.05, font-size under 2px, colour matched to the background, clip:rect(0,0,0,0), clip-path:inset(100%), zero-dimension boxes |
| Off-screen positioning | position:absolute or fixed with left/top beyond −500px, or text-indent at −1000px or less |
| Attributes | alt, aria-label, title, placeholder, data-tooltip |
| HTML comments | Comment bodies containing injection keywords |
| Meta tags | Meta content containing injection keywords |
Those combine into a single number:
finding score = sum of matched pattern weights + 3 per concealment flag
The page score is the sum of all findings, which lands it in one of four bands.
| Score | Level | Badge |
|---|---|---|
| 0 | safe | none |
| 1–5 | low | amber ! |
| 6–14 | medium | orange !! |
| 15+ | high | red !!! |
One detail is worth spelling out, because it’s the sort of thing that quietly
breaks a detector. Checking the element that holds the text isn’t enough. If a
wrapper three levels up carries display:none, the child’s own computed style
still reports whatever it declared. The child isn’t being painted, but nothing
in its own style says as much. So the scanner walks up to ten ancestors instead
of trusting the node in front of it.
Not crying wolf
This is the part that actually determines whether the thing is usable. A detector that fires on ordinary pages gets switched off within a day, and then it protects nobody.
Two guards do most of that work:
- Hidden elements alone never produce a finding. Dropdown menus, modal shells, off-screen navigation, screen-reader-only labels. Hidden text is ubiquitous and almost all of it is innocent. Concealment only starts counting once there is something instruction-shaped inside it.
- Visible text needs a high-weight pattern (≥9) to register at all. Prose about AI says “act as” and “do not reveal” constantly, and a page that says those things in the open isn’t hiding anything.
Both were checked against a pair of pages at opposite ends:
| Page | Result |
|---|---|
Adversarial fixture (test/injection-fixture.html) |
High risk, score 198, all 8 planted injections detected |
| Benign control page | Safe, score 0 |
Legitimate pages carrying “act as” or “do not reveal” in visible content also scored zero, which is the outcome the second guard exists to produce.
Working inside MV3
Manifest V3 shaped a fair amount of the code. No eval, no inline scripts, so
everything is a real file under the extension’s own CSP. Everything async,
built on await.
The service worker is the constraint that matters most: it isn’t somewhere you can keep things. It’s torn down when idle, so anything that needs to outlive a single scan has to sit in storage instead of in a variable, and the worker has to be written as though it just woke up, because it usually has.
The other one is rendering. Reading computed style is what makes this detector
work, and doing it in a tight loop over a large DOM is exactly how you make a
page stutter. Style reads are deferred and batched through
requestAnimationFrame so the scan doesn’t fight the browser for the main
thread. Dynamically injected content is picked up by a MutationObserver,
debounced at 1200ms, so a page that keeps rewriting itself doesn’t trigger a
rescan on every keystroke.
What it isn’t
It’s a heuristic detector, not a shield. It can’t block anything. It looks, and then it tells you. It can’t see inside cross-origin iframes or shadow DOM either. Novel phrasing walks straight past it and will keep doing so, because a fixed pattern list is a fixed pattern list.
Worth being plain about what a clean result means: nothing matched. That isn’t the same claim as “this page is safe”, and it shouldn’t be read as one.