picket: an injection firewall for the agentic browser
An agentic browser has the lethal trifecta by construction: the page is untrusted content, the logged-in session is private data, and navigation is the outbound channel. picket is a governed browser: a deterministic injection firewall, an action gate, and a credential-isolating login layer wrapping any CDP browser. Here's how it's built, how it was validated against a real Chrome, and the real bugs that live testing surfaced, each found, fixed, and merged in public.
Update, July 16, 2026: picket has since been renamed fieldpass for its npm release (@askalf/fieldpass): same governed browser, same guarantees; the picket_* MCP tool names and PICKET_* variables are unchanged, and the repository links below redirect. (The rest of the stack renamed alongside it: warden is now redstamp, canon is now truecopy, keeper is now strongroom.)
The engineering problem
Agentic browsers arrived fast: an agent now drives a real, logged-in browser on your behalf, clicking and typing across the open web. The capability is genuinely useful, and it hands every web page a new attack surface that is structural to the category, not incidental to any one product.
Simon Willison named the precondition the lethal trifecta: an agent that (1) reads untrusted content, (2) holds private data, and (3) has a way to send data out. A browser agent has all three the moment it opens a page. So a hostile page can carry an instruction aimed not at the human reader but at the agent (white-on-white text, a hidden div, an alt attribute), invisible to a person, plainly readable to anything that scrapes the DOM. That is indirect prompt injection.
The tempting fix is labeling: tell the model "the following is untrusted, don't obey it." In practice that doesn't hold against a determined payload: the model still sees the imperative, and a sufficiently convincing one wins. The robust move is provenance + quarantine: anything that reads as an instruction is not delivered at all. It's replaced with an opaque placeholder before it reaches the model.
That is the engineering problem picket was built to solve.
What picket is
picket is a thin governance layer that wraps a CDP browser (Chrome, Edge, anything speaking the DevTools protocol). The agent talks to picket, never to the browser directly, across three planes:
- Perception: every page is normalized into nodes tagged with provenance (body text vs. comment vs.
alt/aria-labelvs.meta) and visibility (display:none, low-contrast, off-screen, tiny-font, zero-width). A deterministic detector scores each node for the trifecta legs; co-location of instruction + private-data reference + exfil sink is the lethal case and blocks the page. Quarantined payloads are replaced with opaque placeholders before the model sees anything. - Action: navigation is allowlist-checked, high-authority verbs (
wire,approve,delete) step up for approval, and the agent is never allowed to type into a credential field. - Identity: logins are leased from a secret broker and filled at the CDP layer. The agent gets an opaque handle; the secret never enters its context.
The design decision that matters most: the deterministic layer is fast, free, and conservative; the LLM judge is escalate-only and fail-safe. The judge reviews only the ambiguous residue the patterns can't rule on, can upgrade a verdict but never downgrade one, and if the model call errors, the deterministic verdict simply stands. Defense doesn't depend on the network being up or a classifier being perfect.
- Repository: github.com/askalf/picket, public, MIT
- Release: v0.2.0 (2026-07-01), CI green, 65 tests
- Ships as a library, a CLI (
picket scan), and an MCP server any agent framework can call
Does it work? The demo is the claim
The repo ships a self-contained, offline demo: a booby-trapped vendor-invoice page carrying eight planted payloads (two full-trifecta attacks plus hidden-div, HTML-comment, alt-attribute, tiny-font, zero-width, and meta variants) alongside two benign controls that must survive untouched.
NAIVE AGENT 8 attacker directives reached the model ❌
GOVERNED AGENT 8 quarantined, 0 reached the model ✅
The invoice text the user actually asked about survives intact; every payload's exfil URL is withheld. This is what the model receives instead of the traps (npm run demo regenerates the full report from the booby-trapped page in the repo):
[picket:BLOCK #n1 — 167 chars withheld; signals: instruction-to-ai,
tool-call, sensitive-data, hidden-text]
Invoice #4471 totals $2,300.00, due July 1, 2026.
[picket:QUARANTINE #n16 — 88 chars withheld; signals: instruction-to-ai,
authority-spoof]
The placeholder tells the model something was there and why it was withheld, auditable, without ever delivering the imperative.
Validated against a real browser, not just fixtures
Static-HTML tests are necessary but insufficient, because the attacks live in computed style: a class-based white-on-white payload looks "visible" to a static parser that can't resolve the stylesheet. So picket has two capture backends, a browserless static parser for CI, and a live CDP backend that reads real computed styles, and the end-to-end suite drives both through a real Chrome 149.
The live run: a real Wikipedia article, 6,709 nodes, comes back ALLOW with zero findings (no false positives on dense, benign, link-heavy content), and the hostile page comes back BLOCK with zero leaks, with the white-on-white payload correctly attributed as hidden only under computed styles. Same verdict on both backends; strictly better attribution on the live one.
What real bugs look like in this system
A detector that only ever meets crafted fixtures accumulates blind spots silently. The most valuable engineering in picket's history is what real pages and adversarial review surfaced; every item below is a merged, linkable fix.
Real pages punish naive heuristics
The first live run against Wikipedia produced three false positives, fixed in one commit: an exfil-verb regex matched the word upload inside the hostname upload.wikimedia.org (fix: strip URLs and emails before verb-matching); meta and attribute nodes were auto-marked "hidden," so benign og:image URLs and title-attribute citations tripped the hidden-with-substance rule; and a hidden node containing only a bare URL flagged without any actual intent signal. The lesson generalizes: verb-in-URL and metadata-as-hidden are the classic over-flags, and you only find them on real pages.
Adversarial review of your own gate
PR #2 fixed five defects that a live end-to-end pass and an adversarial review found together, each pinned by a regression test. The two worst: observe() silently preferred the static parser even when a live browser endpoint was configured, meaning the production path wasn't the one being exercised, and the action gate failed open on unknown action types. It now default-denies. The same PR added cross-node detection for trifecta legs scattered across sibling nodes, credential-typing inference (a password-shaped field is treated as credentials even without the flag), and data:/javascript:/blob: URLs as exfil sinks.
The live path is the one that drifts
PR #8 is the subtlest: the live CDP backend had quietly diverged from the static backend. Low-contrast detection compared foreground and background for exact equality, so a #fffffe-on-#fff payload, one bit away from white-on-white, slipped the production path while the static tests stayed green; and the CDP attribute loop omitted value, so an injection planted in <input value="…"> was caught offline but missed live. Both fixed to parity. When a system has a cheap test backend and an expensive real one, parity between them is itself a property that needs tests.
Beyond the firewall
The same governed core acquired, PR by PR: a content-keyed verdict cache so repeat pages don't re-pay the judge; an MCP server exposing picket_observe / picket_gate / picket_login to any MCP client (observe returns the verdict and finding categories only, so withheld payloads never cross the wire); a broker of isolated, secret-leased persona contexts on one shared browser; a deterministic replay oracle that culls fabricated "the page shows X" claims without an LLM; and a session recorder that emits a governed browsing session as a signed, pinnable, deterministically replayable skill. The full arc is in the changelog.
picket is the browser leg of Own Your Stack: warden gates actions, keeper brokers secrets, canon vets skills, picket governs the browser. Each is independently useful; picket forwards its action decisions to warden and leases its logins from keeper when they're present, and runs standalone when they're not. We run this composition in production on our own agent fleet: untrusted web content our agents research passes through picket's observe path before any model sees it.
What this demonstrates
A security posture, not a heuristic pile. Deterministic-first, escalate-only, fail-safe: the properties that matter under failure are design decisions, stated and tested. The gate default-denies, the judge can only tighten a verdict, and a dead model call leaves the deterministic verdict standing.
Validation against reality, and the humility that follows. The false positives, the fail-open gate, and the parity drift were all found by pointing the system at real pages and reviewing it adversarially, then fixed with regression tests in single, auditable PRs. A detector's history of fixed over-flags is evidence it has actually met the web.
Fully public and runnable. The repository, the release, the demo, the end-to-end harness, and the PR history are all publicly accessible. Every claim in this piece has a link you can click, and npm run demo reproduces the headline result offline.
Scope and limits
The heuristics are a strong first line, not a complete one; the LLM judge covers novel phrasing, but a model classifier is itself fallible. This is defense in depth, not a silver bullet: picket reduces blast radius; it doesn't replace least-privilege secrets or egress controls, which is exactly why it composes with the rest of the stack instead of claiming their jobs. And it's a young project: one studio's production use, not a standard. If you can get a payload past it, the repo's security policy tells you how to report it. We'd genuinely like to know.
All claims are verifiable at github.com/askalf/picket (as of 2026-07-01, v0.2.0).
We build the boundaries that make agents safe to hand real capability: browsers, tools, secrets, skills. If your agents read content you don't control, that's the kind of problem we go deep on.
Start a conversation →