What it is
The school's Chrome policy removed 131 extensions by ID — and an extension ID is also the address the browser uses to ask Google for updates, so 129 of the 131 still resolve to signed bundles. All 129 are now downloadable as folders: unzip one, press Load unpacked in chrome://extensions, done. 28,887 files, every asset digest-checked against GitHub's own copy, the untouched .crx beside each folder, and the two that answer HTTP 204 named rather than quietly dropped.
Version history
Straight from the project’s own card data — what each version added.
129 of 131 recovered · 1.46 GiB · 0 corrupt · 2 delisted (HTTP 204 on every version tried)
Published for download: GitHub release, 129 .crx assets, 1,567,361,468 bytes, server digests matched.
Republished as folders: 129 .zip assets · 28,887 files · 1.46 GiB · tree SHA-256 per folder; a downloaded 1,578-file folder re-hashes identical.
What broke, and what I’d change
Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/chrome-crx-archive.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.
Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.
Build spec
The project’s own build notes, rendered. Open the raw .md ↗
Show the spec
Every blocked extension, recovered — 129 of 131 CRX bundles
Goal. The school's Chrome policy removed 131 extensions by ID. An extension ID is also the token the browser uses to ask Google for updates, so most of those "removed" extensions are still downloadable binaries. Fetch them all, verify them, and publish the evidence — while being explicit about the two that no longer exist anywhere.
Companion project: Island College Chrome policy — Audit (/analysis/island-chrome-policy/) decoded the policy these IDs came from. This project asks a different question — can the blocked things still be obtained? — and is a separate card, page and artefact. Not merged, cross-linked both ways.
Constraints
- Read-only with respect to the managed profile: no account, no cookie, no policy change, no attempt to
- No API key, no login. The Web Store CRX update service is open; that is the whole method.
- Every number in the report is computed from the capture manifest at build time, never typed.
- The page must be self-contained (no external CSS/JS/fonts) so it prints identically to PDF.
- Never round 129 up to 131. Non-recovery is stated in the deck, a stat tile, a section, the table and
make anything installable on a school machine. The deliverable is an archive + a report.
the footer.
Method
- Parse the 131 IDs out of the cached
ExtensionSettingspolicy (see the companion prompt for the - Ask the update service, once per ID, as a macOS arm64 Chrome 126 client:
protobuf decode). Keep the policy's own category groups.
https://clients2.google.com/service/update2/crx?response=redirect
&os=mac&arch=arm64&os_arch=arm64&nacl_arch=arm64
&prod=chromiumcrx&prodchannel=unknown&prodversion=126.0.0.0
&acceptformat=crx2,crx3&x=id%3D<ID>%26uc
8 workers, 4 retries with backoff, 90 s timeout.
- Classify the reply:
204/ 0 bytes = delisted (terminal — do not retry, do not report as a - Validate three ways: magic bytes
43 72 32 34, header version 3, then strip12 + header_length - SHA-256 the delivered bytes (signature header included) into
crx-manifest.json. - For anything that 204s, probe every mirror before declaring it gone: legacy endpoint without
network error). 200 with Cr24 magic = a real CRX3.
and run zipfile.testzip() over the payload — a full CRC sweep, not a header peek. Parse the version out of the redirect filename (/ext_126.0.0.0_<version>.crx) and cross-check it against the manifest.json inside the archive.
acceptformat, crx4chrome, crxsoso, extpose, chrome-stats, Wayback CDX + availability API. Record the result of each route in the report's evidence table.
Numbers this run produced
Design decisions
- Ship folders, not just
.crx. Isaac's correction, and it is the right one: a.crxis a zip - Verify the folder with a tree hash. A directory has no SHA-256, so record
- The binaries are not served from this domain — they are published elsewhere. 19 of 129 exceed
- Verify downloads against the server's digest, not your own arithmetic. After upload, pull
- Two stat tiles carry the bad news (
2 delisted, and later19 over cap) so the gap is visible - Section order: two-projects table → the protocol → what came back → the full 131-row list →
with a CRX3 signature prefix, so a normal person who downloads one gets an "the file may be damaged" dialog and no obvious next step. So each extension is published as <Name>__<id>.zip whose single top-level entry is the extension's directory — unzip once, get a folder with manifest.json at its root, then chrome://extensions → Developer mode → Load unpacked. Keep the untouched .crx beside it for anyone who prefers drag-install.
sha256(sorted "relpath\0filehash\n") per extension. Proof it works: a downloaded 1,578-file folder re-hashes to the recorded value after round-tripping through the release.
the 25 MiB single-file limit of a static host, and the uploadable remainder is 483 MB across ~20,000 files. So the archive lives on a GitHub release (testisaac1804-byte/crx-archive, tag v1.0): 258 assets (129 folders + 129 bundles), 2 GiB per asset, no account to download, CDN-served, and it does not depend on this Mac being awake. The page stays the index: every one of the 131 rows carries its own folder link plus .crx, there is one button for the whole set, and downloads.json maps ID → zip → crx → digests so a script can mirror the lot.
releases/tags/v1.0 from the API and compare each asset's digest with the locally computed SHA-256: 258/258 matched. That is the end-to-end proof that what a stranger downloads is byte-identical to what was captured.
before the reader scrolls.
the two that don't exist → integrity → why not hosted → reproduce.
Traps
204is not an error. urllib raises nothing; you get an empty body and would happily write a- The redirect filename carries the version — parse it there rather than trusting the archive.
- mirrors 404 on a delisted ID, they do not 500. chrome-stats answers 403 (Cloudflare), extpose's
- Watch the disk: 1.5 GiB of CRX and 2.3 GiB unpacked. Unpacking is optional; if Isaac only wants
- A
__MSG_name in an unpacked manifest is not a broken unzip — it means the extension uses /analysis/chrome-crx-archiveis registered in~/scripts/theme-inject.pySKIP_PREFIX: the report
0-byte file. Check the body, not the status alone.
viewer proxies the same dead Google URL, so it looks alive and returns nothing.
packed files, delete unpacked/ afterwards (he asked for that here).
i18n and the display name lives in _locales/*/messages.json.
is a print-precise artifact and the skin/beacon injection would disturb the measured layout.
Deliverable
portfolio-deploy/
analysis/chrome-crx-archive/
report.html 8 sections, 7 tables, 131-row catalogue, self-contained
crx-catalogue.csv 131 rows — category, name, ID, version, size, files, sha256, status
crx-manifest.json 129 SHA-256 entries + the 2 delisted IDs marked status:delisted
prompts/chrome-crx-archive-prompt.md (this file)
Card actions: Open the report · Catalogue (CSV) · Hash manifest (JSON) · Companion: policy audit.