C.W.K.
Stream
Lesson 02 of 05 · published

Unpacked vs Packed — Dev Loop and the .crx File

~10 min · unpacked, crx, developer-mode, distribution

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Unpacked is for you, packed is for everyone else. Lesson 2 is the difference between the developer-mode 'Load unpacked' loop you've been using all quest and the signed .crx that goes to other machines."

Unpacked

The whole quest, you've been working unpacked: a folder on disk loaded via chrome://extensions → Developer mode → Load unpacked. The extension reads its files live from the directory; editing content.js + clicking reload is the entire dev loop. No bundling required, no signing, no upload anywhere.

Unpacked extensions get a generated extension ID per Chrome install — different on each machine. That's fine for development; it becomes a problem if you ship something that hardcodes a specific extension ID (some Native Messaging hosts do this).

Packed

A packed extension is a .crx file: a ZIP of the extension contents plus a signature (RSA-signed by a key you control or by Chrome itself if you publish through the Web Store). The .crx is what installs onto someone else's Chrome.

Two ways to produce one:

  • chrome://extensions → Pack extension — local tool. Asks for the extension directory and (optionally) a .pem key file. First run produces both the .crx and a fresh .pem; keep the .pem somewhere safe so future updates sign with the same key (Chrome treats same-key as same extension for updates).
  • Chrome Web Store — upload the ZIP of the extension directory, Chrome signs it, the .crx lives in the store. This is the path 99% of public extensions take.

The Same-Key Update Contract

The .pem (private key) controls the extension's identity. As long as you sign each update with the same .pem, Chrome treats the new .crx as an upgrade of the existing extension — users get the new version, their storage persists, the extension ID stays the same. Lose the key and you've effectively orphaned that extension; you'd have to publish a new extension under a new ID and migrate users.

Web Store-published extensions don't need a local .pem because Google manages the key. The extension ID is generated at first publish and never changes.

The Dev Workflow That Survives Distribution

Recommendation: develop unpacked, publish through the Web Store as the primary distribution path. The hybrid:

  • One git repo, one source-of-truth manifest.json with the production permissions.
  • Local dev: npm run build outputs to dist/, chrome://extensions points 'Load unpacked' at dist/.
  • To ship: npm run build && cd dist && zip -r ../clipdeck.zip ., upload the zip to the Web Store developer dashboard.
  • The Web Store handles signing, hosting, and auto-updates; users install with one click.

Sideloading and Why It's Hard

Chrome heavily restricts installing .crx files from outside the Web Store. Desktop Chrome:

  • Drag-and-drop install on chrome://extensions used to work; now restricted to extensions installed via enterprise policy or developer-mode unpacked.
  • Direct .crx URL → Chrome prompts "This extension can't be added from this website" or similar.
  • Group policy install (Chrome Enterprise) is the supported path for sideloaded production extensions.

Workaround for dev teams that don't have Chrome Enterprise: publish unlisted to the Web Store. Each person installs from the unlisted URL; the rest is normal Web Store flow.

The Unlisted Loophole

'Unlisted' on the Web Store means the extension exists but doesn't appear in search or category browsing. People can install if you give them the URL. The submission flow is the same as public (privacy policy, review, the works), but the marketing surface is invisible. This is the right default for private-team extensions that don't justify the Chrome Enterprise overhead.

Unpacked for development, packed via Web Store for distribution. Sideloaded .crx files are a dead end for v1; either use the Web Store (public or unlisted) or commit to enterprise group policy.
Don't commit your .pem to git. The .pem private key signs your extension; anyone with it can publish updates as you. Treat it like a credential: keep it out of the repo, store a copy in a password manager, hand-encrypt a backup. Losing it isn't catastrophic for Web Store extensions (Google has its own key) but it's catastrophic for any extension you signed locally.

Code

Shell — produce the Web Store upload artifact·bash
# Build + zip for Web Store upload
npm run build                       # outputs dist/
cd dist && zip -r ../clipdeck.zip .  # exclude any source maps, .gitignore, etc.
cd ..
ls -la clipdeck.zip                  # this is what you upload
manifest.json — keep version semver-friendly for Chrome's update detection·json
{
  "name": "ClipDeck",
  "version": "1.0.0",
  "description": "Save text clips from any page. Browse, search, and copy from a persistent side panel.",
  "manifest_version": 3
}

External links

Exercise

Verify your current dev loop: chrome://extensions → Developer mode toggle ON → Load unpacked → pick your clipdeck/ directory. Make a small visible change (rename the popup heading), click the reload button on the card, confirm the popup updates. Then try the Pack extension button on the same page — pick the clipdeck/ directory, leave the key field empty, click Pack. You get a .crx and a .pem in the parent directory. Try to install the .crx by dragging it into chrome://extensions — note that Chrome refuses with a message about untrusted sources. That's the sideload restriction at work. The .crx and .pem are still useful for self-hosted enterprise distribution; for the Web Store, you upload the unsigned zip instead.
Hint
If 'Load unpacked' silently does nothing, the manifest.json might have a JSON syntax error — open chrome://extensions, look for an 'Errors' button on your extension's card. If reload doesn't pick up your popup.html change, you might need to also reload the popup (close + re-open the popup). Background.js changes always require the reload button; popup/panel HTML edits sometimes don't if Chrome cached them. The .pem you generated is for your eyes only — don't share it, don't commit it, treat it like an SSH key.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.