Installing a browser extension without a click, and removing it again

An agent that can install an extension can also be asked to remove it. Both directions were measured across four routes on one machine, with an ordinary user account. One of them does both in 9.4 seconds without a window ever appearing — and it is the one route that does not count in the store's user statistics. That trade-off is the whole result.

Measured 2026-08-03 · Firefox ESR and Chrome stable on Windows 11 · raw data

The four routes

What each route can do, measured 3 August 2026
RouteInstallUninstall Input eventsAdmin rights Counts in storeRound trip
Marionette (Firefox)yesyes0nono4.1 s
CDP (Chrome)nur entpacktyes0nono
Enterprise policynono0yesnot measured
Store UI + real inputnono4noyes179.4 s

Marionette: the channel Firefox already ships

Firefox carries a remote control channel of its own — the one geckodriver speaks. Started with -headless -marionette, it answers Addon:Install and Addon:Uninstall over a TCP socket. No driver, no third-party package: a length-prefixed JSON protocol and about two dozen lines of client code.

Round trip: uninstall, then install, verified against the profile
StepSeconds OutcomeEvidence
XPI aus dem Store0.21succeededaus dem lokalen Zwischenspeicher, bereits geholt
Firefox headless starten und Sitzung aufbauen3.65succeededMarionette antwortet, WebDriver:NewSession quittiert
Addon:Uninstall0.08succeededBefehl ohne Fehler quittiert
Addon:Install0.16succeededProfil meldet 2.26.0, aktiv, location app-profile

The evidence column matters more than the outcome column. Every step is verified against extensions.json — the file Firefox itself maintains — not against the command's own reply. A command can report success and leave nothing behind, and on the route below, that is exactly what happened.

Why the click route is not a route

Driving the store page with real input events is the only measured way to be counted as a store user, and on this machine it produced nothing. The run took 179.4 seconds. Every click reported success, because a click into empty space is a valid click as far as Windows is concerned. The extension was not installed.

The cause is fixed coordinates. A different resolution, a different font size or a changed store layout moves every target, and nothing in the script can tell the difference between hitting a button and hitting the space beside it. For a person that is an annoyance. For an agent it is worse than useless: a route that reports success on failure poisons everything downstream of it.

It also needs the foreground. Real input events go to the focused window, so whoever sends them takes over the user's mouse and keyboard for the duration.

Chrome: half the answer

Chrome has no Marionette. Over CDP with --headless=new --enable-unsafe-extension-debugging, Extensions.loadUnpacked loads an unpacked folder and returns its id, and Extensions.uninstall removes it. But Extensions.install does not exist — -32601 'Extensions.install' wasn't found. There is no CDP command that installs a store build.

So for Chrome the honest answer is split: an agent can put an unpacked extension into a browser it controls, headless, and take it out again. Getting the store build in requires the user interface or an enterprise policy. Worth noting that the Extensions domain is not listed by Schema.getDomains at all, yet answers — so absence from the schema is not evidence that a command is missing. Only the error is.

The policy route needs rights this account does not have

Enterprise policy is the usual answer to “install silently”, and it is also the route that could remove an extension the same way — ExtensionSettings with installation_mode: blocked uninstalls and blocks in one step. On this machine it does neither:

Without administrator rights the policy route is closed in both directions. This corrects an earlier version of our own agent skill, which said the Chrome entries could go under HKCU without elevation. They cannot.

The trade-off, stated plainly

Marionette installs the file. It does not visit a store page, so it does not appear in the store's user count. The XPI is the signed store build — the same bytes, checksummed in the raw data — but the visit that would be counted never happens.

That is the correct outcome, not a limitation to design around. A user number is supposed to mean “someone chose to install this”. Anyone who wants to be counted has to go through the interface, as a person would. Both stores terminate the developer account for inflating installs, so the incentive here points in exactly one direction.

A side finding about WSL

Firefox binds Marionette to 127.0.0.1 of the Windows host, and from WSL2 that port is unreachable — neither over 127.0.0.1 nor over the host address, both refused. The client has to run on the same side as the browser. This is not specific to Marionette; it applies to any browser control channel bound to loopback.

Reproducing this

One command, and it writes its own raw data:

python3 tools/erweiterung-fernsteuern.py rundlauf --protokoll lauf.json

Firefox must not have the target profile open — the tool checks whether the lock file is actually held, not merely present, and stops rather than killing someone's browser. Source: github.com/Bubu89/full-page-pdf-snap.

Questions

Is doing this allowed?

Installing an extension into a browser on a machine you administer, or that the user asked you to set up, is ordinary configuration. What is not allowed under either store's terms is manufacturing installs to move a public number. Nothing here does that — this route is invisible to the counter, which is precisely why it is safe to publish.

Will this keep working?

Unknown. Marionette exists because Firefox's own test tooling needs it, and --enable-unsafe-extension-debugging is named the way it is for a reason. Both are subject to change. The measurement says what was true on 3 August 2026 on one machine; if it stops being true, the reproduction command above will say so.

Can this uninstall an extension a user installed themselves?

Yes — Addon:Uninstall does not care how the add-on arrived. That cuts both ways, and it is the reason this page states the profile check so prominently: a tool that removes extensions should be certain which profile it is pointed at. On this machine six profiles sit side by side.