Loading...
Loading...
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 script, build an RPC layer over chrome messaging, or publish to the Chrome Web Store. Covers both new extension projects and adding features to existing ones. Do NOT use for framework-specific questions.
npx skill4agent add samber/cc-skills chrome-extension| File | When to read |
|---|---|
| Setting up or modifying manifest.json, configuring icons, versioning |
| Background logic, lifecycle, state persistence, alarms, events |
| Injecting code into pages, isolated/main world, dynamic injection, SPA handling, orphaning |
| Communication between any contexts, typed protocols, RPC layer, async handler patterns |
| Popup, options page, side panel, context menus, commands, notifications, omnibox, devtools panel |
| chrome.storage (local/sync/session), quotas, reactive patterns, framework hooks |
| HTTP requests from content scripts, CSP bypass relay, declarativeNetRequest, offscreen docs, CORS |
| Required/optional permissions, host permissions, activeTab, runtime request flow |
| Exposing extension files to web pages, security implications |
| TypeScript setup, project structure, build tools comparison, bundling |
| Chrome Web Store submission, review process, rejection reasons, updates, privacy policy |
| Communication flow diagrams, per-context capabilities/limits, choosing the right messaging method |
| DevTools for extensions, testing SW termination, common gotchas, error patterns |
┌──────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────────────────────────────────────────┘┌───────────────────────────────────────────────────────────────────────────┐
│ 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) │ │
│ └──────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘references/execution-contexts.md| Method | Direction | Best for |
|---|---|---|
| Any ext context → SW | One-shot request/response (90% of cases) |
| SW → content script (by tabId) | Pushing data to a specific tab |
| Bidirectional | Streaming, progress, SW ↔ popup |
| Between worlds on same page | Page JS ↔ content script bridge |
| Broadcast to all contexts | Settings sync, no messaging needed |
references/execution-contexts.mdreferences/messaging-rpc.mdreferences/service-worker.mdreferences/network-csp.mdreturn truereferences/messaging-rpc.mdreferences/permissions.mdreferences/ui-surfaces.mdreferences/content-scripts.mdreferences/network-csp.mdreferences/storage.mdreferences/network-csp.mdreferences/messaging-rpc.mdreferences/execution-contexts.mdreferences/service-worker.mdreferences/network-csp.mdreferences/service-worker.mdactiveTabscriptingreferences/manifest-v3.mdreferences/typescript-build.mdreferences/service-worker.mdreferences/content-scripts.mdreferences/ui-surfaces.mdreferences/messaging-rpc.mdreferences/debugging-mistakes.mdreferences/publishing.mdreferences/permissions.mdreferences/content-scripts.md{
"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"
}
}references/manifest-v3.md// Wrap async handlers to avoid the return-true trap
function 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() };
}
}),
);// content-script.ts
async function apiCall(endpoint: string, options?: RequestInit) {
return chrome.runtime.sendMessage({ type: "API_RELAY", endpoint, options });
}
// background.ts
const 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() };
}),
);// Use chrome.storage.session for ephemeral state
chrome.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 });
}function isExtensionContextValid(): boolean {
try {
return !!chrome.runtime?.id;
} catch {
return false;
}
}
// Before any chrome.runtime call
if (!isExtensionContextValid()) {
showRefreshBanner();
return;
}eval()new Function()setTimeoutsetInterval<all_urls>return truelocalStoragesessionStoragewebRequestdeclarativeNetRequestchrome.extension.getBackgroundPage()