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.
| Route | Install | Uninstall | Input events | Admin rights | Counts in store | Round trip |
|---|---|---|---|---|---|---|
| Marionette (Firefox) | yes | yes | 0 | no | no | 4.1 s |
| CDP (Chrome) | nur entpackt | yes | 0 | no | no | — |
| Enterprise policy | no | no | 0 | yes | not measured | — |
| Store UI + real input | no | no | 4 | no | yes | 179.4 s |
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.
| Step | Seconds | Outcome | Evidence |
|---|---|---|---|
| XPI aus dem Store | 0.21 | succeeded | aus dem lokalen Zwischenspeicher, bereits geholt |
| Firefox headless starten und Sitzung aufbauen | 3.65 | succeeded | Marionette antwortet, WebDriver:NewSession quittiert |
| Addon:Uninstall | 0.08 | succeeded | Befehl ohne Fehler quittiert |
| Addon:Install | 0.16 | succeeded | Profil 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.
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 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.
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:
C:\Program Files\Mozilla FirefoxESR\distribution — not writable,
UnauthorizedAccessExceptionHKCU\Software\Policies\Google\Chrome — ACL-protected,
UnauthorizedAccessException
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.
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.
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.
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.
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.
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.
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.