chrome-extension
samber/cc-skills
Comprehensive guide for building Chrome extensions with Manifest V3. Use this skill whenever the user mentions Chrome extension, browser extension, manifest.json, content script, service worker (in extension context), popup, side panel, chrome.runtime, chrome.tabs, chrome.storage, chrome.scripting, background script, MV3, Manifest V3, or any Chrome extension API. Also trigger when the user wants to inject scripts into web pages, communicate between page and background, bypass CSP from a content
...Expand allAbout chrome-extension
A comprehensive guide for building, debugging, and publishing Chrome extensions with Manifest V3, structured as a routing document. The main file is read first to understand the architecture and decision points, after which only the relevant, self-contained reference file is loaded for implementation detail. Reference files cover manifest.json setup and versioning, the service worker lifecycle and state persistence, content scripts and isolated versus main world injection, messaging and RPC layers, UI surfaces (popup, options page, side panel, context menus, commands, notifications, omnibox, devtools panel), chrome.storage usage and quotas, network and CSP handling, permissions, web-accessible resources, TypeScript build setup, publishing to the Chrome Web Store, execution-context flow diagrams, and common debugging mistakes. It is aimed at AI coding agents and requires git and node, with declared tools spanning file edits and git, gh, and npm commands.
The architecture overview explains that an extension has up to five execution contexts that communicate by message passing: the service worker (background, no DOM, ephemeral, with access to all chrome.* APIs), the popup, options page, and side panel (all with full DOM and APIs), and, on the web page itself, the content script in an isolated world and a main world script in the page context. Diagrams show that the content script shares the DOM but has its own JS scope and chrome.runtime and chrome.storage access while being subject to CSP for network only, whereas the main world script has full page access but no chrome.* APIs and is fully subject to CSP. The two communicate via window.postMessage through the shared DOM.
Communication patterns are summarized in tables: chrome.runtime.sendMessage for one-shot request/response from any extension context to the service worker (the most common case), chrome.tabs.sendMessage for pushing data from the service worker to a specific tab by tabId, chrome.runtime.connect ports for bidirectional streaming and progress (including service worker to popup), and window.postMessage for bridging worlds on the same page. Because the service worker is ephemeral and cannot push directly to extension pages, the guide directs readers toward ports or chrome.storage.onChanged, which fires across all contexts simultaneously, and points to the execution-contexts reference for deeper flow diagrams and per-context capability and limit breakdowns. The skill explicitly states it should not be used for framework-specific questions.
FAQ
Which Chrome extension manifest version does this cover?
Manifest V3 (MV3). It covers setting up and modifying manifest.json, configuring icons, and versioning, along with the broader MV3 architecture, messaging, and publishing process.
How is the skill organized?
As a routing document. You read the main SKILL.md first for the architecture and decision points, then load only the relevant, self-contained reference file (for example service-worker.md, content-scripts.md, or messaging-rpc.md) for implementation details.
What execution contexts does a Chrome extension have according to this guide?
Up to five contexts that communicate via message passing: the service worker (background), popup, options page, and side panel within the extension process, plus the content script (isolated world) and main world script on the web page.
Which messaging method should I use for a typical request/response?
chrome.runtime.sendMessage from any extension context to the service worker, which the guide notes handles about ninety percent of cases. For pushing to a specific tab use chrome.tabs.sendMessage, and for bidirectional streaming use chrome.runtime.connect ports.
What are the prerequisites for using this skill?
It is designed for Claude Code or similar AI coding agents and requires git and node (the metadata lists git, node, and npm). It is not intended for framework-specific questions.
All Files
14 filesreferences/content-scripts.md12.5 KBViewreferences/execution-contexts.md18.3 KBViewreferences/messaging-rpc.md19.4 KBViewreferences/permissions.md8.0 KBViewreferences/service-worker.md10.9 KBViewreferences/typescript-build.md7.9 KBViewreferences/debugging-mistakes.md9.2 KBViewreferences/manifest-v3.md7.2 KBViewreferences/network-csp.md9.9 KBViewreferences/publishing.md6.4 KBViewreferences/storage.md9.0 KBViewreferences/ui-surfaces.md9.7 KBViewreferences/web-accessible-resources.md3.1 KBViewSKILL.md16.0 KBViewThis skill covers everything needed to build, debug, and publish Chrome extensions with MV3. It is organized as a routing document: read this file first to understand the architecture and decision points, then load the relevant reference file for implementation details.
Reference files
Read only the reference files relevant to the current task. Each file is self-contained.
| File | When to read |
|---|---|
references/manifest-v3.md | Setting up or modifying manifest.json, configuring icons, versioning |
references/service-worker.md | Background logic, lifecycle, state persistence, alarms, events |
references/content-scripts.md | Injecting code into pages, isolated/main world, dynamic injection, SPA handling, orphaning |
references/messaging-rpc.md | Communication between any contexts, typed protocols, RPC layer, async handler patterns |
references/ui-surfaces.md | Popup, options page, side panel, context menus, commands, notifications, omnibox, devtools panel |
references/storage.md | chrome.storage (local/sync/session), quotas, reactive patterns, framework hooks |
references/network-csp.md | HTTP requests from content scripts, CSP bypass relay, declarativeNetRequest, offscreen docs, CORS |
references/permissions.md | Required/optional permissions, host permissions, activeTab, runtime request flow |
references/web-accessible-resources.md | Exposing extension files to web pages, security implications |
references/typescript-build.md | TypeScript setup, project structure, build tools comparison, bundling |
references/publishing.md | Chrome Web Store submission, review process, rejection reasons, updates, privacy policy |
references/execution-contexts.md | Communication flow diagrams, per-context capabilities/limits, choosing the right messaging method |
references/debugging-mistakes.md | DevTools for extensions, testing SW termination, common gotchas, error patterns |
Architecture overview
A Chrome extension has up to 5 execution contexts that communicate via message passing:
┌──────────────────────────────────────────────────────────┐│ Extension Process ││ ┌─────────────────┐ ┌───────┐ ┌─────────┐ ┌──────┐ ││ │ Service Worker │ │ Popup │ │ Options │ │ Side │ ││ │ (background) │ │ │ │ Page │ │Panel │ ││ │ - No DOM │ │ Full │ │ Full │ │ Full │ ││ │ - Ephemeral │ │ DOM │ │ DOM │ │ DOM │ ││ │ - All chrome.* │ │ All │ │ All │ │ All │ ││ │ APIs │ │ APIs │ │ APIs │ │ APIs │ ││ └────────┬─────────┘ └───┬───┘ └────┬────┘ └──┬───┘ ││ │ chrome.runtime.sendMessage / connect │ │└───────────┼────────────────┼───────────┼──────────┼──────┘ │ │ │ │ chrome.tabs.sendMessage │ │ │ │ │ │ │┌───────────┼────────────────┼───────────┼──────────┼──────┐│ Web Page ▼ ││ ┌──────────────────┐ ┌──────────────────┐ ││ │ Content Script │ │ Main World Script │ ││ │ (isolated world) │◄──►│ (page context) │ ││ │ - Shared DOM │ │ - Shared DOM │ ││ │ - Own JS scope │ │ - Page JS scope │ ││ │ - chrome.runtime │ │ - No chrome.* API │ ││ │ - chrome.storage │ │ - Full page access│ ││ │ - Subject to CSP │ │ - Subject to CSP │ ││ │ (network only) │ │ (fully) │ ││ └──────────────────┘ └──────────────────┘ ││ ▲ window.postMessage ││ │ (through shared DOM) │└──────────────────────────────────────────────────────────┘Communication flows (labeled channels)
┌───────────────────────────────────────────────────────────────────────────┐│ Extension Process ││ ││ ┌─────────────────┐ chrome.runtime ┌───────┐ ┌─────────┐ ┌──────┐ ││ │ Service Worker │◄─.sendMessage()──│ Popup │ │ Options │ │ Side │ ││ │ (background) │◄─.connect()──────│ │ │ Page │ │Panel │ ││ │ │ └───────┘ └─────────┘ └──────┘ ││ │ - No DOM │ ┌────────────────────────────────────────────┐ ││ │ - Ephemeral 30s │ │ SW cannot push to these pages. │ ││ │ - All chrome.* │ │ Use: ports (.connect) or storage.onChanged │ ││ └────────┬─────────┘ └────────────────────────────────────────────┘ ││ │ ││ chrome.storage.onChanged ◄── fires across ALL contexts simultaneously ││ │└───────────┼──────────────────────────────────────────────────────────────┘ │ chrome.tabs.sendMessage(tabId, ...) [SW must know tabId] │┌───────────┼──────────────────────────────────────────────────────────────┐│ Web Page ▼ ││ ┌──────────────────┐ window.postMessage ┌──────────────────┐ ││ │ Content Script │◄───────────────────►│ Main World Script │ ││ │ (isolated world) │ Custom DOM events │ (page context) │ ││ │ │ │ │ ││ │ chrome.runtime ───┼── to/from SW │ No chrome.* APIs │ ││ │ chrome.storage │ │ Full page JS │ ││ │ Shared DOM │ │ Shared DOM │ ││ │ Page CSP (network)│ │ Page CSP (full) │ ││ └──────────────────┘ └──────────────────┘ │└──────────────────────────────────────────────────────────────────────────┘For detailed flow diagrams (three-layer bridge, cross-extension, storage broadcast) and a per-context breakdown of permissions, limits, and workarounds: → Read references/execution-contexts.md
Communication methods at a glance
| Method | Direction | Best for |
|---|---|---|
chrome.runtime.sendMessage | Any ext context → SW | One-shot request/response (90% of cases) |
chrome.tabs.sendMessage | SW → content script (by tabId) | Pushing data to a specific tab |
chrome.runtime.connect (Port) | Bidirectional | Streaming, progress, SW ↔ popup |
window.postMessage | Between worlds on same page | Page JS ↔ content script bridge |
chrome.storage.onChanged | Broadcast to all contexts | Settings sync, no messaging needed |
→ Full matrix with limits and edge cases: references/execution-contexts.md → Implementation patterns, typed protocols, RPC layer: references/messaging-rpc.md
Key architectural rules
Service worker is ephemeral. It terminates after 30s of inactivity. All state must be persisted to chrome.storage. All event listeners must be registered synchronously at the top level. Never use setTimeout/setInterval for anything beyond a few seconds. → Read
references/service-worker.mdContent scripts run in the page's origin. Network requests from content scripts are subject to the page's CSP and CORS. To bypass, relay through the service worker. → Read
references/network-csp.mdMessaging is the backbone. Every cross-context interaction uses chrome.runtime messaging. The #1 bug: forgetting to
return truefrom async message listeners. → Readreferences/messaging-rpc.mdPermissions determine CWS review speed. Broad host_permissions trigger manual review (weeks). activeTab + optional permissions = fast automated review. → Read
references/permissions.mdPopup is destroyed on blur. Side panel persists. Choose based on interaction duration. → Read
references/ui-surfaces.md
Decision tree: which context handles what?
"I need to run code when the user visits a page"
→ Content script. Static (manifest) for known URL patterns, dynamic (chrome.scripting) for user-triggered injection. Default to isolated world unless you need page JS access. → Read references/content-scripts.md
"I need to make an HTTP request to my API"
- From popup/options/side panel: direct fetch() works (extension origin, no CSP issues)
- From content script on a page with restrictive CSP: relay through service worker
- From service worker: direct fetch() works (requires host_permissions for the target domain) → Read
references/network-csp.md
"I need to store user settings"
- Settings that sync across devices: chrome.storage.sync (100KB limit)
- Large data or caches: chrome.storage.local (10MB, or unlimited with permission)
- Ephemeral state surviving SW restarts: chrome.storage.session → Read
references/storage.md
"I need to modify HTTP headers or block requests"
→ declarativeNetRequest (NOT webRequest, which lost blocking in MV3) → Read references/network-csp.md
"I need the page's JavaScript to talk to my extension"
→ Three-layer bridge: page (window.postMessage) → content script → service worker → Read references/messaging-rpc.md
"I need to understand what each context can and cannot do"
→ Read references/execution-contexts.md — per-context cards listing chrome.* access, DOM, network, storage, lifetime, hard limits, and practical workarounds.
"I need periodic background tasks"
→ chrome.alarms (minimum 30s interval). NOT setTimeout. → Read references/service-worker.md
"I need DOM APIs in the background" (DOMParser, Canvas, Audio)
→ Offscreen document. One per extension, only chrome.runtime available. → Read references/network-csp.md
"I need to authenticate with OAuth"
→ chrome.identity.launchWebAuthFlow() or chrome.identity.getAuthToken() (Google only) → Read references/service-worker.md (identity section)
Workflow: new extension from scratch
Define the manifest with minimum permissions. Start with
activeTab+scripting. → Readreferences/manifest-v3.mdSet up TypeScript and build tooling (or use CRXJS for Vite-based dev). → Read
references/typescript-build.mdImplement the service worker with all event listeners at the top level. → Read
references/service-worker.mdAdd content scripts if you need page interaction. → Read
references/content-scripts.mdBuild UI surfaces (popup, options, side panel) as needed. → Read
references/ui-surfaces.mdWire up messaging between all contexts. → Read
references/messaging-rpc.mdTest with DevTools, specifically test service worker termination. → Read
references/debugging-mistakes.mdPublish to Chrome Web Store. → Read
references/publishing.md
Workflow: adding a feature to an existing extension
- Identify which context the feature belongs to (see decision tree above).
- Read the relevant reference file(s) for that context.
- Check if new permissions are needed. Prefer optional_permissions for new capabilities. → Read
references/permissions.md - Update the manifest if adding new content scripts, UI surfaces, or permissions.
- Handle extension updates gracefully (content script orphaning). → Read
references/content-scripts.md(orphaning section)
Minimal manifest.json template
{ "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "description": "What it does in one sentence", "permissions": ["storage", "activeTab", "scripting"], "action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "background": { "service_worker": "background.js", "type": "module" }, "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }}→ For the full manifest reference with all fields: references/manifest-v3.md
Code patterns quick reference
Async message handler (the safe pattern)
// Wrap async handlers to avoid the return-true trapfunction asyncHandler( fn: (msg: any, sender: chrome.runtime.MessageSender) => Promise<any>,) { return ( message: any, sender: chrome.runtime.MessageSender, sendResponse: (r: any) => void, ) => { fn(message, sender) .then(sendResponse) .catch((e) => sendResponse({ __error: true, message: e.message })); return true; // literal true, not Promise<true> };}chrome.runtime.onMessage.addListener( asyncHandler(async (msg, sender) => { if (msg.type === "FETCH") { const res = await fetch(msg.url); return { ok: res.ok, data: await res.text() }; } }),);
CSP bypass relay (content script → service worker → API)
// content-script.tsasync function apiCall(endpoint: string, options?: RequestInit) { return chrome.runtime.sendMessage({ type: "API_RELAY", endpoint, options });}// background.tsconst ALLOWED_ENDPOINTS = ["https://api.example.com"];chrome.runtime.onMessage.addListener( asyncHandler(async (msg) => { if (msg.type !== "API_RELAY") return; if (!ALLOWED_ENDPOINTS.some((e) => msg.endpoint.startsWith(e))) { throw new Error("Blocked endpoint"); } const res = await fetch(msg.endpoint, msg.options); return { ok: res.ok, status: res.status, data: await res.text() }; }),);
Persist state across SW restarts
// Use chrome.storage.session for ephemeral statechrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS",});async function getState<T>(key: string, fallback: T): Promise<T> { const result = await chrome.storage.session.get(key); return result[key] ?? fallback;}async function setState<T>(key: string, value: T): Promise<void> { await chrome.storage.session.set({ [key]: value });}
Orphaned content script detection
function isExtensionContextValid(): boolean { try { return !!chrome.runtime?.id; } catch { return false; }}// Before any chrome.runtime callif (!isExtensionContextValid()) { showRefreshBanner(); return;}
What NOT to do
- Do NOT use
eval(),new Function(), or load remote scripts. MV3 forbids it. - Do NOT use
setTimeout/setIntervalfor anything > 5s in service workers. - Do NOT register event listeners inside callbacks or async functions.
- Do NOT use
<all_urls>host permission unless absolutely necessary. - Do NOT rely on DevTools keeping the service worker alive during testing.
- Do NOT forget
return truein async message listeners. - Do NOT use
localStorageorsessionStoragein service workers (they don't exist there). - Do NOT assume content scripts survive extension updates.
- Do NOT use
webRequestblocking (removed in MV3). UsedeclarativeNetRequest. - Do NOT use
chrome.extension.getBackgroundPage()(removed in MV3).
All Files
14 filesInstall chrome-extension
Download and extract the skill files to your .claude/skills/ directory.
Download ZIPClone the repository and copy the skill files to your project.
git clone https://github.com/samber/cc-skills/blob/main/skills/chrome-extension/SKILL.md # Copy SKILL.md to your .claude/skills/ directory
Copy





Home
