Build Chrome Extension
Build, debug, package, and ship Chrome Manifest V3 extensions. Optimize for restart-safe service workers, isolated content scripts, least-privilege permissions, and Web Store review readiness.
When To Use
Trigger when the request matches any of:
- creating a new Chrome MV3 extension or scaffolding from a framework (WXT, Plasmo, CRXJS, Vite)
- editing or generating
manifest.json with manifest_version: 3, service_worker, content_scripts, host_permissions, action, or side_panel fields
- implementing or debugging
chrome.runtime, chrome.storage, chrome.alarms, chrome.scripting, chrome.tabs, chrome.declarativeNetRequest, chrome.sidePanel, or chrome.offscreen APIs
- fixing MV3 service-worker lifecycle issues: idle termination, top-level listener registration, async
onMessage returns, chrome.alarms vs setInterval
- bridging content scripts between ISOLATED and MAIN worlds, or routing fetches through the service worker for cross-origin host permissions
- migrating an MV2 extension to MV3 (background page → service worker, blocking
webRequest → declarativeNetRequest, browser_action → action)
- validating a built
dist/ or .output/chrome-mv3/ folder, packaging the Web Store zip, writing privacy/permission justifications, or preparing review notes
- choosing or comparing Chrome MV3 frameworks for a new project
Do NOT use this skill when:
- the target is Firefox, Safari, or a cross-browser polyfill (
browser.* namespace, web-ext tooling, Safari App Extensions)
- the request is generic browser automation outside an extension context — route to
run-agent-browser
- publishing a regular npm package or library — route to
publish-npm-package
- building a different platform's extension surface (Raycast script command, VS Code extension, Edge enterprise-policy package) — route to that platform's skill
Pinned Defaults — Apply Before Generating Code
| Key |
Default |
| Manifest target |
manifest_version: 3 only; never generate MV2 |
| Greenfield framework |
WXT unless the repo already chose another MV3 tool |
| Persistent state |
chrome.storage.local; chrome.storage.session for ephemeral restart-safe state |
| Background model |
Event-driven service worker, listeners registered synchronously at top level |
| Periodic work |
chrome.alarms — never setTimeout/setInterval for background scheduling |
| Network modification |
chrome.declarativeNetRequest — never blocking webRequest |
| Permissions |
Least privilege; prefer optional_permissions and optional_host_permissions granted via chrome.permissions.request |
| Loaded folder |
Built output only: WXT .output/chrome-mv3-dev/ or .output/chrome-mv3/; Plasmo build/chrome-mv3-*; CRXJS/Vite dist/ |
| Package preflight |
Run scripts/check-mv3-manifest.sh and scripts/preflight-extension.sh against the production build before zipping |
MV3 Footguns — Keep In Working Memory
These are the failures that recur across every MV3 build. Internalize before writing code:
- Service workers idle out (~30s default). Global module-scope state disappears between events. Persist before each await and rehydrate at event entry.
- Register
chrome.runtime.onMessage, onInstalled, onStartup, alarm, and tab listeners synchronously at top level. Late-registered listeners miss wake-up events.
setTimeout/setInterval cannot keep a service worker alive and will not fire reliably across idle cycles. Use chrome.alarms.create with periodInMinutes >= 0.5.
chrome.runtime.onMessage async handlers must return true synchronously to keep the message channel open; otherwise sendResponse throws.
- Content scripts run in an isolated world by default. The page's JS, frameworks, and
window.* globals are invisible. Use world: "MAIN" only for page-JS access, then bridge with postMessage plus a same-origin token.
- Extension-origin
fetch requires matching host_permissions. Content-script fetch is bound by the page's origin and CORS rules — route privileged requests through the service worker via chrome.runtime.sendMessage.
- MV3 CSP forbids inline
<script>, eval(), new Function(), and remote executable code. Bundle everything; no CDN-loaded scripts.
- Hand-written
manifest.json paths must point at built artifacts (e.g. background.js, content.js, popup.html), never src/*.ts or unbuilt source.
- Requesting
<all_urls> or broad host permissions at install time triggers Web Store review friction. Prefer activeTab plus optional host grants.
Decision Rules
| Decision |
Default |
Escalate when |
| Background state |
chrome.storage.session/local |
Transactions, large indexes, binary blobs → IndexedDB |
| Periodic work |
chrome.alarms |
Real-time stream → design reconnect + queue |
| Network modification |
declarativeNetRequest |
Read-only observation → non-blocking webRequest |
| Page data fetch |
Service worker fetch with host permissions |
Page origin is sufficient → content-script fetch |
| Page JS access |
world: "MAIN" bridge with payload validation |
DOM-only access → stay in ISOLATED world |
| UI state persistence |
chrome.storage.session |
Must survive browser restart → local or sync |
| Host access |
activeTab or optional host grants |
Extension is non-functional without install-time host access |
| Side panel vs popup |
Side panel for persistent companion UI |
Quick action or short form → popup |
| Offscreen document |
DOM/canvas/clipboard/audio/Worker from service worker |
Popup/options/content script can own it → skip |
Routing Boundary
| Request shape |
Action |
| "Build a Chrome extension" |
Use this skill; target MV3 |
| "Build for Chrome and Firefox" |
Keep Chrome MV3 here; cross-browser layer is out of scope |
| "Automate a website in a browser" |
Route to run-agent-browser |
| "Write Playwright tests for an extension" |
Extension launch/load notes here; broader Playwright authoring is out of scope for this pack |
| "Publish to npm" |
Route to publish-npm-package |
| "Submit to Chrome Web Store" |
Use this skill; read references/publishing/web-store.md |
| "Deploy via enterprise policy" |
Out of scope unless a dedicated enterprise skill exists |
Workflow
1. Scope The Extension
Ask: "Which extension surfaces exist, which Chrome APIs are required, which permissions can be deferred to runtime?"
- List required surfaces: popup, options, side panel, content script, service worker, offscreen document, devtools.
- List required Chrome APIs and resulting permissions.
- Split install-time vs
optional_permissions/optional_host_permissions.
- Choose the framework — see
references/frameworks/comparison.md.
- Record the built-output folder Chrome will load.
2. Scaffold Or Adapt
Ask: "Greenfield, existing framework build, or hand-written manifest?"
- Greenfield default: WXT.
- Existing Vite app becoming an extension: CRXJS.
- Custom build pipeline / explicit control: vanilla + Vite.
- Existing repo: follow its framework and output conventions.
Chrome must load the built output folder, never src/ or unbundled TypeScript.
3. Implement Extension Contexts
Ask: "Which context owns the state, which owns the DOM, which messages cross the boundary?"
Read the matching reference before writing code:
- Service worker lifecycle, persistence, alarms, offscreen →
references/patterns/service-worker.md
- Content scripts, world isolation, idempotency, cross-origin routing →
references/patterns/content-scripts.md
- Popup, options, side panel, devtools, new-tab surfaces →
references/patterns/ui-surfaces.md
- One-time messages, ports, external/native messaging, page bridges →
references/apis/messaging.md
chrome.storage areas, quotas, typed wrappers, migrations → references/apis/storage.md
- Tabs, scripting, alarms, DNR, side panel, offscreen, runtime →
references/apis/core-apis.md
- Manifest, permission, and MV2 migration decisions are covered by the pinned defaults and guardrails in this
SKILL.md.
Validate every cross-boundary payload: messages, storage reads, external API responses, MAIN-world bridge data.
4. Validate Built Output
Ask: "Can Chrome load this exact folder, and do all manifest paths exist there?"
Run from the skill directory against your build folder:
scripts/check-mv3-manifest.sh dist
scripts/preflight-extension.sh dist
Adjust the path per framework:
| Framework |
Dev output |
Production output |
| WXT |
.output/chrome-mv3-dev/ |
.output/chrome-mv3/ |
| Plasmo |
build/chrome-mv3-dev/ |
build/chrome-mv3-prod/ |
| CRXJS |
dist/ |
dist/ |
| Vanilla Vite |
dist/ |
dist/ |
Read scripts/check-mv3-manifest.md and scripts/preflight-extension.md before modifying either script.
5. Test Extension Behavior
Ask: "What failure only appears once Chrome loads the extension?"
- Unit-test pure logic and message/storage helpers; mock
chrome.* only at boundaries.
- Manually load the built output via
chrome://extensions → "Load unpacked".
- Inspect the service worker from the extensions page; trigger an idle/wake cycle.
- Test content-script injection on allowed and disallowed URLs.
- Verify the runtime permission request UX (
chrome.permissions.request) before the host call.
Read references/testing/testing-guide.md for extension-specific tests and references/testing/debugging.md for service-worker, popup, content-script, permission, and storage debugging.
6. Package For Web Store Review
Ask: "Could a reviewer state the single purpose, each permission's need, the data use, and the remote-code posture in one sentence each?"
- Build production output.
- Run both bundled scripts against that production build.
- Strip junk before zipping: source maps (unless intentionally shipped), tests,
.DS_Store, __MACOSX, framework caches.
- Create the zip from inside the built-output folder (so
manifest.json sits at the zip root).
- Prepare: privacy practices, per-permission justification, remote-code disclosure, data-use certification, reviewer test instructions.
- Read
references/publishing/web-store.md before submission.
Minimal Build Evidence
Match evidence to the task before stopping:
| Task |
Evidence |
| New scaffold |
Built-output path + check-mv3-manifest.sh result |
| Feature change |
Relevant unit/integration test + manual-load note when Chrome behavior changed |
| Manifest/permission change |
check-mv3-manifest.sh result + permission justification |
| Content-script change |
Allowed-URL injection result + disallowed-URL injection result |
| Service-worker change |
Restart-resilience note OR explicit "not exercised" caveat |
| Package/Web Store work |
preflight-extension.sh result + zip path + reviewer notes |
Output Contract
Final reports include:
- loaded build directory
- zip path (when packaged)
- Chrome version used for manual load (when manually tested)
- summary of permissions and host-permissions justification
- Web Store policy posture: single purpose, data use, remote code, MV3 compliance
- scripts/tests run, including failures
- reviewer notes for remaining manual checks
Guardrails
- Never generate or preserve Manifest V2 for new work.
- Never load
src/ in Chrome unless the framework explicitly emits loadable code there.
- Never hold durable state in module-scope globals inside the service worker.
- Never rely on
setTimeout/setInterval to keep the service worker alive.
- Never use blocking
webRequest for normal MV3 network modification.
- Never request
<all_urls> without a feature-level justification and a documented narrower alternative.
- Never expose arbitrary background
fetch through unauthenticated content-script messages.
- Never ship remote executable code or undeclared remote-code behavior.
- Always validate built-output paths against
manifest.json before claiming done.
- Always make each permission explainable in one sentence.
Bottom Line
MV3 punishes the same mistakes repeatedly: lost service-worker state, late-registered listeners, timer-based scheduling, isolated-world surprises, over-broad permissions, and shipping src/ instead of built output. Apply the pinned defaults, route to references for depth, run the bundled scripts against the built folder, and produce review-ready evidence.
1---2name: build-chrome-extension3description: Use if building or debugging a Chrome MV3 extension — manifest v3, service_worker, content_scripts.4---56# Build Chrome Extension78Build, debug, package, and ship Chrome Manifest V3 extensions. Optimize for restart-safe service workers, isolated content scripts, least-privilege permissions, and Web Store review readiness.910## When To Use1112Trigger when the request matches any of:1314- *creating a new Chrome MV3 extension or scaffolding from a framework (WXT, Plasmo, CRXJS, Vite)*15- *editing or generating `manifest.json` with `manifest_version: 3`, `service_worker`, `content_scripts`, `host_permissions`, `action`, or `side_panel` fields*16- *implementing or debugging `chrome.runtime`, `chrome.storage`, `chrome.alarms`, `chrome.scripting`, `chrome.tabs`, `chrome.declarativeNetRequest`, `chrome.sidePanel`, or `chrome.offscreen` APIs*17- *fixing MV3 service-worker lifecycle issues: idle termination, top-level listener registration, async `onMessage` returns, `chrome.alarms` vs `setInterval`*18- *bridging content scripts between ISOLATED and MAIN worlds, or routing fetches through the service worker for cross-origin host permissions*19- *migrating an MV2 extension to MV3 (background page → service worker, blocking `webRequest` → `declarativeNetRequest`, browser_action → action)*20- *validating a built `dist/` or `.output/chrome-mv3/` folder, packaging the Web Store zip, writing privacy/permission justifications, or preparing review notes*21- *choosing or comparing Chrome MV3 frameworks for a new project*2223Do NOT use this skill when:2425- *the target is Firefox, Safari, or a cross-browser polyfill (`browser.*` namespace, `web-ext` tooling, Safari App Extensions)*26- *the request is generic browser automation outside an extension context — route to `run-agent-browser`*27- *publishing a regular npm package or library — route to `publish-npm-package`*28- *building a different platform's extension surface (Raycast script command, VS Code extension, Edge enterprise-policy package) — route to that platform's skill*2930## Pinned Defaults — Apply Before Generating Code3132| Key | Default |33|---|---|34| Manifest target | `manifest_version: 3` only; never generate MV2 |35| Greenfield framework | WXT unless the repo already chose another MV3 tool |36| Persistent state | `chrome.storage.local`; `chrome.storage.session` for ephemeral restart-safe state |37| Background model | Event-driven service worker, listeners registered synchronously at top level |38| Periodic work | `chrome.alarms` — never `setTimeout`/`setInterval` for background scheduling |39| Network modification | `chrome.declarativeNetRequest` — never blocking `webRequest` |40| Permissions | Least privilege; prefer `optional_permissions` and `optional_host_permissions` granted via `chrome.permissions.request` |41| Loaded folder | Built output only: WXT `.output/chrome-mv3-dev/` or `.output/chrome-mv3/`; Plasmo `build/chrome-mv3-*`; CRXJS/Vite `dist/` |42| Package preflight | Run `scripts/check-mv3-manifest.sh` and `scripts/preflight-extension.sh` against the production build before zipping |4344## MV3 Footguns — Keep In Working Memory4546These are the failures that recur across every MV3 build. Internalize before writing code:4748- Service workers idle out (~30s default). Global module-scope state disappears between events. Persist before each await and rehydrate at event entry.49- Register `chrome.runtime.onMessage`, `onInstalled`, `onStartup`, alarm, and tab listeners **synchronously at top level**. Late-registered listeners miss wake-up events.50- `setTimeout`/`setInterval` cannot keep a service worker alive and will not fire reliably across idle cycles. Use `chrome.alarms.create` with `periodInMinutes >= 0.5`.51- `chrome.runtime.onMessage` async handlers must `return true` synchronously to keep the message channel open; otherwise `sendResponse` throws.52- Content scripts run in an isolated world by default. The page's JS, frameworks, and `window.*` globals are invisible. Use `world: "MAIN"` only for page-JS access, then bridge with `postMessage` plus a same-origin token.53- Extension-origin `fetch` requires matching `host_permissions`. Content-script `fetch` is bound by the page's origin and CORS rules — route privileged requests through the service worker via `chrome.runtime.sendMessage`.54- MV3 CSP forbids inline `<script>`, `eval()`, `new Function()`, and remote executable code. Bundle everything; no CDN-loaded scripts.55- Hand-written `manifest.json` paths must point at built artifacts (e.g. `background.js`, `content.js`, `popup.html`), never `src/*.ts` or unbuilt source.56- Requesting `<all_urls>` or broad host permissions at install time triggers Web Store review friction. Prefer `activeTab` plus optional host grants.5758## Decision Rules5960| Decision | Default | Escalate when |61|---|---|---|62| Background state | `chrome.storage.session`/`local` | Transactions, large indexes, binary blobs → IndexedDB |63| Periodic work | `chrome.alarms` | Real-time stream → design reconnect + queue |64| Network modification | `declarativeNetRequest` | Read-only observation → non-blocking `webRequest` |65| Page data fetch | Service worker `fetch` with host permissions | Page origin is sufficient → content-script `fetch` |66| Page JS access | `world: "MAIN"` bridge with payload validation | DOM-only access → stay in ISOLATED world |67| UI state persistence | `chrome.storage.session` | Must survive browser restart → `local` or `sync` |68| Host access | `activeTab` or optional host grants | Extension is non-functional without install-time host access |69| Side panel vs popup | Side panel for persistent companion UI | Quick action or short form → popup |70| Offscreen document | DOM/canvas/clipboard/audio/Worker from service worker | Popup/options/content script can own it → skip |7172## Routing Boundary7374| Request shape | Action |75|---|---|76| "Build a Chrome extension" | Use this skill; target MV3 |77| "Build for Chrome and Firefox" | Keep Chrome MV3 here; cross-browser layer is out of scope |78| "Automate a website in a browser" | Route to `run-agent-browser` |79| "Write Playwright tests for an extension" | Extension launch/load notes here; broader Playwright authoring is out of scope for this pack |80| "Publish to npm" | Route to `publish-npm-package` |81| "Submit to Chrome Web Store" | Use this skill; read `references/publishing/web-store.md` |82| "Deploy via enterprise policy" | Out of scope unless a dedicated enterprise skill exists |8384## Workflow8586### 1. Scope The Extension8788Ask: *"Which extension surfaces exist, which Chrome APIs are required, which permissions can be deferred to runtime?"*89901. List required surfaces: popup, options, side panel, content script, service worker, offscreen document, devtools.912. List required Chrome APIs and resulting permissions.923. Split install-time vs `optional_permissions`/`optional_host_permissions`.934. Choose the framework — see `references/frameworks/comparison.md`.945. Record the built-output folder Chrome will load.9596### 2. Scaffold Or Adapt9798Ask: *"Greenfield, existing framework build, or hand-written manifest?"*99100- Greenfield default: WXT.101- Existing Vite app becoming an extension: CRXJS.102- Custom build pipeline / explicit control: vanilla + Vite.103- Existing repo: follow its framework and output conventions.104105Chrome must load the built output folder, never `src/` or unbundled TypeScript.106107### 3. Implement Extension Contexts108109Ask: *"Which context owns the state, which owns the DOM, which messages cross the boundary?"*110111Read the matching reference before writing code:112113- Service worker lifecycle, persistence, alarms, offscreen → `references/patterns/service-worker.md`114- Content scripts, world isolation, idempotency, cross-origin routing → `references/patterns/content-scripts.md`115- Popup, options, side panel, devtools, new-tab surfaces → `references/patterns/ui-surfaces.md`116- One-time messages, ports, external/native messaging, page bridges → `references/apis/messaging.md`117- `chrome.storage` areas, quotas, typed wrappers, migrations → `references/apis/storage.md`118- Tabs, scripting, alarms, DNR, side panel, offscreen, runtime → `references/apis/core-apis.md`119- Manifest, permission, and MV2 migration decisions are covered by the pinned defaults and guardrails in this `SKILL.md`.120121Validate every cross-boundary payload: messages, storage reads, external API responses, MAIN-world bridge data.122123### 4. Validate Built Output124125Ask: *"Can Chrome load this exact folder, and do all manifest paths exist there?"*126127Run from the skill directory against your build folder:128129```bash130scripts/check-mv3-manifest.sh dist131scripts/preflight-extension.sh dist132```133134Adjust the path per framework:135136| Framework | Dev output | Production output |137|---|---|---|138| WXT | `.output/chrome-mv3-dev/` | `.output/chrome-mv3/` |139| Plasmo | `build/chrome-mv3-dev/` | `build/chrome-mv3-prod/` |140| CRXJS | `dist/` | `dist/` |141| Vanilla Vite | `dist/` | `dist/` |142143Read `scripts/check-mv3-manifest.md` and `scripts/preflight-extension.md` before modifying either script.144145### 5. Test Extension Behavior146147Ask: *"What failure only appears once Chrome loads the extension?"*148149- Unit-test pure logic and message/storage helpers; mock `chrome.*` only at boundaries.150- Manually load the built output via `chrome://extensions` → "Load unpacked".151- Inspect the service worker from the extensions page; trigger an idle/wake cycle.152- Test content-script injection on allowed and disallowed URLs.153- Verify the runtime permission request UX (`chrome.permissions.request`) before the host call.154155Read `references/testing/testing-guide.md` for extension-specific tests and `references/testing/debugging.md` for service-worker, popup, content-script, permission, and storage debugging.156157### 6. Package For Web Store Review158159Ask: *"Could a reviewer state the single purpose, each permission's need, the data use, and the remote-code posture in one sentence each?"*1601611. Build production output.1622. Run both bundled scripts against that production build.1633. Strip junk before zipping: source maps (unless intentionally shipped), tests, `.DS_Store`, `__MACOSX`, framework caches.1644. Create the zip from inside the built-output folder (so `manifest.json` sits at the zip root).1655. Prepare: privacy practices, per-permission justification, remote-code disclosure, data-use certification, reviewer test instructions.1666. Read `references/publishing/web-store.md` before submission.167168## Minimal Build Evidence169170Match evidence to the task before stopping:171172| Task | Evidence |173|---|---|174| New scaffold | Built-output path + `check-mv3-manifest.sh` result |175| Feature change | Relevant unit/integration test + manual-load note when Chrome behavior changed |176| Manifest/permission change | `check-mv3-manifest.sh` result + permission justification |177| Content-script change | Allowed-URL injection result + disallowed-URL injection result |178| Service-worker change | Restart-resilience note OR explicit "not exercised" caveat |179| Package/Web Store work | `preflight-extension.sh` result + zip path + reviewer notes |180181## Output Contract182183Final reports include:184185- loaded build directory186- zip path (when packaged)187- Chrome version used for manual load (when manually tested)188- summary of permissions and host-permissions justification189- Web Store policy posture: single purpose, data use, remote code, MV3 compliance190- scripts/tests run, including failures191- reviewer notes for remaining manual checks192193## Guardrails194195- Never generate or preserve Manifest V2 for new work.196- Never load `src/` in Chrome unless the framework explicitly emits loadable code there.197- Never hold durable state in module-scope globals inside the service worker.198- Never rely on `setTimeout`/`setInterval` to keep the service worker alive.199- Never use blocking `webRequest` for normal MV3 network modification.200- Never request `<all_urls>` without a feature-level justification and a documented narrower alternative.201- Never expose arbitrary background `fetch` through unauthenticated content-script messages.202- Never ship remote executable code or undeclared remote-code behavior.203- Always validate built-output paths against `manifest.json` before claiming done.204- Always make each permission explainable in one sentence.205206## Bottom Line207208MV3 punishes the same mistakes repeatedly: lost service-worker state, late-registered listeners, timer-based scheduling, isolated-world surprises, over-broad permissions, and shipping `src/` instead of built output. Apply the pinned defaults, route to references for depth, run the bundled scripts against the built folder, and produce review-ready evidence.