Wiring our citation endpoint to a capture extension raised the obvious question: can an agent just use the extension itself? We loaded it into an agent-driven browser and tried. Two steps pass, two fail — and the line between them is not a bug. It is the permission the extension asks for, and it decides which kind of AI system can drive it.
A Playwright-driven Chromium — the same shape of thing browser-use, Playwright MCP and Chrome-DevTools MCP put underneath an agent — started with the extension loaded from a local folder. Then four checks, each recorded separately so it stays visible which one holds.
| Step | Result | Detail |
|---|---|---|
| Extension loads | passes | Full Page PDF Snap 2.10.0, ENABLED |
| Service worker wakes | passes | background.js, manifest readable |
| Sees the page without a gesture | fails | 0 of 2 tabs carry a URL |
| Captures without a gesture | fails | "Kein Tab geladen." — no tab loaded |
Worth noting how the third one fails. chrome.tabs.query({})
returns the tabs — it just returns them with url and
title empty. The extension is not blocked from running; it is
blocked from seeing, and it reports that honestly instead of
capturing something wrong.
Both builds declare activeTab and no host permissions at
all — no <all_urls>, no site list:
| Build | Permissions | Host permissions |
|---|---|---|
| Firefox | activeTab, downloads, downloads.open, storage, menus,
notifications, scripting, clipboardWrite |
none |
| Chrome / Chromium | activeTab, downloads, downloads.open, storage, contextMenus,
notifications, scripting |
none |
activeTab grants access to the current tab only after a real
user gesture: a click on the toolbar icon, the keyboard command, or a
context-menu entry. Nothing else opens it — and a click dispatched by a script
into page content is not one of them. That is exactly what the measurement
shows, and it is the same design we argue for in
the permissions
measurement: an extension that cannot read every site cannot leak every
site.
So the answer to “can an AI use this extension” is not yes or no. It is: an AI that can produce a real gesture can; one that only manipulates the DOM cannot. The extension does not check whether a human or a machine clicked — the browser checks whether a gesture happened at all.
A permission that is granted by “a gesture” raises the obvious question — does
the browser check who made it? It does not. It checks whether one
reached the input layer at all. So the same test was repeated with the
extension untouched and one thing changed: instead of a scripted click inside
the document, the keyboard command was sent through the window system's own
input path (X11's XTEST, the mechanism xdotool uses), to a
visible browser window.
| Moment | Tabs the extension can see |
|---|---|
| After loading, service worker awake, no gesture | 0 |
After Alt+Shift+Y as a real input event | 1 |
That is the whole finding in two rows. The extension went from blind to seeing
the page, without a human in the room and without any change to the extension.
A synthetic input event at window-system level satisfies
activeTab. Which is exactly what a computer-use model
produces when it moves the mouse and presses keys, and what an
xdotool-driven agent produces when it clicks an extension's
toolbar icon.
One honest gap: in this run the capture itself did not finish inside the
measurement window — the driver closed the browser while the page was still
being assembled (TargetClosedError), which says something about
our timeout and nothing about the extension. The permission transition is the
claim being made here, and it is the one that was measured.
Sorted by whether the system's actions reach the browser as input events or as protocol commands. That, not the vendor, is what decides it.
| System | How it acts | Can trigger the extension |
|---|---|---|
| Claude in Chrome | extension-based agent clicking and typing in the live browser | yes — and it works in the profile where the extension is already installed |
| ChatGPT agent mode, the ChatGPT browser extension | agent operating a browser session | yes, same basis |
| Perplexity Comet and other agentic browsers | browser with a built-in agent | yes |
| Computer-use models driving a desktop | synthetic mouse and keyboard at OS level | yes — measured above |
Pixel-level MCP servers (screenshot plus xdotool) |
real input events, no CDP | yes — this is the category the measurement reproduces |
Bridge extensions to your own Chrome — chrome-use,
browser-agent-bridge, openchrome |
native messaging into the browser you are signed into | usually, depending on whether the bridge forwards real input or only DOM calls |
| Playwright / Puppeteer scripts, Playwright MCP, Chrome DevTools MCP | CDP commands and DOM events | no — measured: 0 tabs visible |
| Server-side readers and crawlers | no browser at all | no — and for half the work they do not need one |
Anything in the “yes” rows inherits the user's session, logins and permissions. That is why those products ship per-site approval and blocklists for sensitive categories, and why the same property that makes the extension usable by an agent is the one that should make anyone deploying such an agent think about scope.
Install once, then the agent uses it like you do. Nothing else is needed — the agent's clicks count.
Firefox, desktop and Android Chrome 116+, Edge, Brave, Vivaldi
Edge asks once to allow extensions from other stores; Opera needs its Install Chrome Extensions add-on first. On Android only Firefox applies — Chrome for Android installs no extensions at all.
Load the unpacked build. This part is verified — it is steps one and two of the measurement:
ctx = p.chromium.launch_persistent_context(
profile, headless=False,
args=["--headless=new",
f"--disable-extensions-except={ext}",
f"--load-extension={ext}"])
Two traps, both cost time if you meet them cold. First, the service worker
sleeps under Manifest V3: ctx.service_workers is empty until
something wakes it, and an empty list looks exactly like “the extension did
not load”. Read chrome://extensions instead —
developerPrivate.getExtensionsInfo answers regardless. Second, on
Chrome 150 --load-extension loads nothing and says
nothing, and the flag that used to re-enable it is gone; there,
Extensions.loadUnpacked over CDP is the working route (verified
on 150.0.7871.187). Playwright's bundled Chromium 145 still honours the
switch, which is what the measurement above used.
After that the agent still needs a gesture to trigger a capture. If it can only reach the DOM, it cannot produce one, and this route ends at “loaded but idle”.
Half the work needs no extension and no gesture. Ten of twenty sources in the reading-list measurement became complete citation records over plain HTTP, 0.4 s each, no account and no key. An agent should exhaust that first:
claude mcp add --transport http provinglab https://provinglab.dev/mcp
Where it cannot finish, the reply now carries a nextStep field
naming what has to happen in a browser, with both install links and the
disclosure that the tool is ours. Runnable recipes for Claude Code, Claude
Desktop, other MCP clients, Python and a shell loop are on
the recipes page.
activeTab, which the failing case
confirms from the other side.InstallTrigger from Firefox. That friction is the
security model, not an oversight.