← Blog

Every way of installing a Chrome extension is closed but one

Hub 4 min read

The Hub was designed with a three-step wizard for putting an extension into a Chrome profile: close every Chrome, open the profiles you want, pick them from a list. The mechanism underneath was assumed to be "write into the profile while Chrome is not looking".

Every part of that is impossible, and finding out took an afternoon of measurement rather than an afternoon of reading. The documentation is not wrong so much as imprecise about what actually happens, and the difference matters.

The experiment#

Pack an extension, register it the per-user way that needs no administrator, start Chrome, stop it, and read Secure Preferences:

HKCU\Software\Google\Chrome\Extensions\<id>
    path     = C:\...\anti-blur.crx
    version  = 1.0.0

Chrome installs it. The extension is unpacked to Default\Extensions\<id>\1.0.0_0 and given a settings entry. Chrome's own documentation says that as of Chrome 33 no external installs are allowed from a path to a local CRX file on Windows, and that is not quite what happens: the install proceeds.

Then it disables it.

"lbaihgmmdnbceolocffoncjdnaidgihg": {
  "active_bit": false,
  "disable_reasons": [256],
  "from_webstore": false,
  "location": 3
}

256 is DISABLE_NOT_VERIFIED in extensions/browser/disable_reason.h"an extension installation could not be verified." Verification means the Web Store's install signature. There is no ack_external on it, unlike the store-installed item sitting beside it in the same file, so this is not a prompt waiting to be accepted. It is an extension on disk, registered, recognised, and dead.

That is worth dwelling on, because "installed but permanently disabled" is a much worse failure than "refused". A wizard built on this route would appear to work, every time, and produce nothing.

The other three#

RouteWhat happens
Self-hosted update_urlCRX_REQUIRED_PROOF_MISSING — Chrome wants a Web Store proof on the CRX3 regardless of where it was fetched from
Developer mode, unpackedChrome 149 disables extensions after an update when they were sideloaded or loaded unpacked. A tool that turns itself off is not a product
Enterprise policyNeeds an administrator and a managed device, and the download page promises per-user with no administrator
--load-extensionNo effect at all on Chrome 152. The switch is parsed and ignored; it was removed from branded builds in 137

That last row was wrong in our own notes for a while — written down as "works, it is what Perch does", which was true of the design and had not been true of the binary for fifteen versions. Perch had moved on long before; the note had not. Measuring the thing you already believe is how you find that.

The one that works#

Extensions.loadUnpacked, over --remote-debugging-pipe, into a Chrome the app started itself. Not a new mechanism — it is the one Perch already ships on both platforms, and the pipe is not a preference either: that method is refused over --remote-debugging-port, and the port would open a listener any local process could drive.

So the Hub starts a browser, on a profile it owns, and loads your extensions into it.

What that costs, said plainly#

Two things, and neither should be softened.

They run in the browser the Hub opened, not the Chrome you already have open. Chrome 136+ refuses --remote-debugging-pipe on the default profile. Anything that could attach there could read your cookies and your saved passwords, and refusing is correct. A separate profile is the only configuration Chrome allows, so this is not a limitation anyone chose and it cannot be engineered away from this side.

A CDP-loaded extension does not survive a restart, so the Hub has to be what opens that browser, every time. And because the two are joined by the connection the extensions arrive over, quitting the Hub closes the browser with it.

What we did not do#

Publish them to the Chrome Web Store.

That would work, and it was the obvious conclusion for about a day. It also moves the paywall. Anyone can install a Web Store extension for free, so "can you get it" stops being the gate and a runtime check inside an extension becomes it — which is the weakest place in the whole system to put one.

Loading over the pipe keeps the gate where it already was. The bytes live in private storage and come out through the same entitlement-checked route as the applications: a signed, single-use download URL, minted only for an account whose subscription is re-read from the database on that request, and checked again when the URL is redeemed. Nothing is published, nothing is public, and an extension that was never downloaded cannot be loaded.

The wizard is gone. So is the profile list, and "close all Chrome", and the per-profile attach model. What replaced them is a list of what your plan covers and one button that opens the browser they run in — which is less impressive to describe and is the only version that is true.