Loading...
Loading...
Build and publish Chrome Extensions using Manifest V3 best practices. Use this skill whenever the user asks to create, modify, debug, or understand Chrome browser extensions, add-ons, or anything involving the Chrome Extensions API. Trigger on mentions of: 'Chrome extension', 'browser extension', 'manifest.json', 'content script', 'service worker' (in browser context), 'popup' (in browser extension context), 'side panel', 'chrome.* API', 'declarativeNetRequest', 'omnibox', 'context menu' (in extension context), or any request to build functionality that integrates with the Chrome browser UI. Also trigger for publishing to the Chrome Web Store: 'publish extension', preparing an extension for publishing, responding to a review rejection, writing permission justifications, or drafting a privacy policy.
npx skill4agent add googlechrome/modern-web-guidance chrome-extensions❌ BROKEN — referencing files that don't exist or reusing one file for all sizes:
"icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }
✅ CORRECT — each size is a separate file at the correct pixel dimensions:
"icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" }
(where icon-16.png is 16×16px, icon-48.png is 48×48px, icon-128.png is 128×128px)
✅ ALSO CORRECT — omit icons from manifest if you cannot generate real PNG files:
(just remove the "icons" and "default_icon" fields — Chrome uses a default icon)references/extensions/icons.md"side_panel": {"default_path": "..."}// In service-worker.js — open side panel on extension icon click
// IMPORTANT: chrome.action.onClicked ONLY fires when there is NO default_popup
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ windowId: tab.windowId });
});chrome.sidePanel.open()chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })openPanelOnActionClickopenPanelOnActionIconClickdefault_popupsetPanelBehaviorreferences/extensions/side-panel.mdeval()new Function()<script>// ❌ BROKEN — direct iframe DOM access throws SecurityError
iframe.contentDocument.write(html);
// ❌ BROKEN — eval in extension page
eval(userCode); // CSP blocks this
// ✅ OPTION A: Sandbox in manifest + postMessage
// manifest.json: { "sandbox": { "pages": ["sandbox.html"] } }
iframe.contentWindow.postMessage({ html, css, js }, '*');
// sandbox.html receives and runs:
window.addEventListener('message', (e) => { eval(e.data.js); /* allowed in sandbox */ });
// ✅ OPTION B: Blob URL (creates separate origin, bypasses extension CSP)
iframe.src = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));
// ✅ OPTION C: srcdoc
iframe.srcdoc = `<style>${css}</style>${html}<script>${js}<\/script>`;references/extensions/csp-sandbox.mdtab.urltabstab.urlundefined// manifest.json — REQUIRED if you read tab.url or tab.title anywhere:
{ "permissions": ["tabs"] }references/extensions/tab-management.md.then()// ❌ BAD
chrome.tabs.query({active: true, currentWindow: true}).then(tabs => {
chrome.scripting.executeScript({target: {tabId: tabs[0].id}, files: ['content.js']}).then(() => {});
});
// ✅ GOOD
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });runtime.onMessagechrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const data = await chrome.storage.local.get('key');
sendResponse({ data });
})();
return true; // keeps channel open
});requestAnimationFrameasync function highlightAll(elements) {
const BATCH = 20;
for (let i = 0; i < elements.length; i += BATCH) {
await new Promise(r => requestAnimationFrame(() => {
elements.slice(i, i + BATCH).forEach(el => el.style.backgroundColor = 'yellow');
r();
}));
if (globalThis.scheduler?.yield) await scheduler.yield();
}
}references/extensions/content-scripts.md// ❌ BROKEN — state lost when SW terminates (~30s of inactivity)
let count = 0;
chrome.tabs.onUpdated.addListener(() => { count++; });
// ✅ CORRECT — persist in chrome.storage, read on every event
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
if (changeInfo.status !== 'complete') return;
const { count = 0 } = await chrome.storage.local.get('count');
await chrome.storage.local.set({ count: count + 1 });
await chrome.action.setBadgeText({ text: String(count + 1) });
});chrome.alarmssetTimeoutsetIntervalreferences/extensions/service-worker.md"key""key": "MIIBIjANBgkqh..."references/extensions/auth-identity.mdreferences/extensions/context-menus.mdLanguageModelLanguageModel.params()const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }modern-web-guidancereferences/extensions/prompt-api.mdchrome.actionactionchrome.action.setBadgeTextchrome.action.setIconchrome.action.onClicked"action"chrome.actionundefined// ❌ BROKEN — manifest has no "action" key
await chrome.action.setBadgeText({ text: '5' });
// TypeError: Cannot read properties of undefined (reading 'setBadgeText')
// ✅ FIX — add "action" to manifest.json (at minimum an empty object)
{ "action": {} }
// or with a popup:
{ "action": { "default_popup": "popup/popup.html" } }activeTabactiveTabcommands// ❌ BROKEN — activeTab does NOT work from a side panel button click
document.getElementById('summarize').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerText });
});
// ✅ FIX — use "tabs" permission + specific host_permissions instead
// manifest.json: { "permissions": ["tabs", "scripting"], "host_permissions": ["<all_urls>"] }references/extensions/side-panel.mdchrome.devtools.panels.create()// ❌ BROKEN — path relative to devtools/ directory
chrome.devtools.panels.create("My Panel", "", "panel/panel.html");
// ✅ CORRECT — full path from extension root
chrome.devtools.panels.create("My Panel", "", "devtools/panel/panel.html");references/extensions/devtools.mdchrome.offscreenchrome.*chrome.downloadschrome.tabschrome.action// ❌ BROKEN — chrome.downloads is undefined in offscreen documents
chrome.downloads.download({ url, filename: 'recording.webm' }); // TypeError
// ❌ BROKEN — chrome.action is undefined in offscreen documents
chrome.action.setBadgeText({ text: 'REC' }); // TypeErrorchrome.runtime.sendMessagechrome.runtime.onMessagechrome.runtime.getURLchrome.runtime.sendMessagereferences/extensions/message-passing.mdchrome.notifications.create()iconUrl"Unable to download all specified images."// ❌ BROKEN — icon file doesn't exist
chrome.notifications.create('reminder', {
type: 'basic',
iconUrl: 'icons/icon-128.png', // File not in extension!
title: 'Reminder',
message: 'Time is up!'
});
// ✅ Generate a data URL at runtime via OffscreenCanvas — no file needed.
// See `references/extensions/icons.md` for a reusable implementation.
const iconUrl = await getIconDataUrl();
chrome.notifications.create('reminder', { type: 'basic', iconUrl, title: 'Reminder', message: 'Time is up!' });chrome.action.setIconchrome.tabCapture.getMediaStreamId()"Cannot capture a tab with an active stream"// ❌ BROKEN — no guard against rapid clicks
let isRecording = false;
chrome.action.onClicked.addListener(async (tab) => {
if (isRecording) { stopRecording(); isRecording = false; }
else { isRecording = true; startRecording(tab); } // Second click = "active stream" error
});
// ✅ CORRECT — use transitional states to lock out concurrent operations
// State machine: 'idle' → 'starting' → 'recording' → 'stopping' → 'idle'
// Store state in chrome.storage.session (survives SW restart, cleared on browser close)
chrome.action.onClicked.addListener(async (tab) => {
const { recordingState = 'idle' } = await chrome.storage.session.get('recordingState');
if (recordingState === 'starting' || recordingState === 'stopping') return;
if (recordingState === 'idle') {
await chrome.storage.session.set({ recordingState: 'starting' });
try {
await startRecording(tab);
await chrome.storage.session.set({ recordingState: 'recording' });
await chrome.action.setBadgeText({ text: 'REC' });
await chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
} catch (err) {
console.error('Failed to start recording:', err);
await chrome.storage.session.set({ recordingState: 'idle' });
}
} else if (recordingState === 'recording') {
await chrome.storage.session.set({ recordingState: 'stopping' });
try { await stopRecording(); }
finally {
await chrome.storage.session.set({ recordingState: 'idle' });
await chrome.action.setBadgeText({ text: '' });
}
}
});chrome.tabCapturechrome.desktopCapturechrome.offscreen.createDocumentreferences/extensions/media-capture.mdchrome.desktopCapturechrome.desktopCapture.chooseDesktopMedia()targetTaburl"tabs"// ❌ BROKEN — called without targetTab from service worker
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], (streamId) => { ... });
// Error: "A target tab is required when called from a service worker context."
// ❌ BROKEN — tab doesn't have url field (missing "tabs" permission)
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => { ... });
// Error: "targetTab doesn't have URL field set."
// ✅ CORRECT — "tabs" permission in manifest + pass tab object
// manifest.json: { "permissions": ["tabs", "desktopCapture"] }
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => {
if (!streamId) return; // User cancelled
});chrome.tabCapture.getMediaStreamId()chrome.desktopCapturereferences/extensions/media-capture.mdchrome.windows.query()getAllgetLastFocusedgetCurrentchrome.tabs.query()chrome.windows.query()// ❌ BROKEN — chrome.windows.query does not exist
const windows = await chrome.windows.query({ focused: true });
// TypeError: chrome.windows.query is not a function
// ✅ CORRECT — use the right method for your need
const focused = await chrome.windows.getLastFocused({ populate: true });
const current = await chrome.windows.getCurrent({ populate: true });
const all = await chrome.windows.getAll({ populate: true });chrome.windowsgetAllgetLastFocusedgetCurrentget(windowId)createupdateremovereferences/extensions/tab-management.mdbackground.service_workerbackground.scriptschrome.actionchrome.browserActionchrome.scripting.executeScriptchrome.tabs.executeScripthost_permissionspermissions<script src="file.js">addEventListenerCHROMEWEBSTORE.mdCHROMEWEBSTORE.mdreferences/webstore/chromewebstore-template.mdmanifest.jsonreferences/webstore/chromewebstore-template.mdreferences/webstore/privacy-policy.mdreferences/webstore/review-checklist.md.git/node_modules/.envCHROMEWEBSTORE.mdreferences/webstore/store-listing.md| Topic | Reference |
|---|---|
| Side panels | |
| Content scripts & DOM | |
| Popups | |
| Service worker lifetime | |
| Code execution & CSP | |
| API calls | |
| Declarative Net Request | |
| Chrome Prompt API | |
| DevTools panels | |
| Authentication | |
| Context menus | |
| Omnibox | |
| Storage | |
| Tab & window management | |
| Tab/desktop capture | |
| Message passing | |
| Icons | |
| CHROMEWEBSTORE.md template | |
| Privacy policy guidance | |
| Pre-publish review checklist | |
| Store listing tips & rejections | |
manifest_version: 3eval()tabstab.urltab.titleasyncawait.then()requestAnimationFramechrome.storage"action": {}chrome.action.*tabshost_permissionsactiveTabchrome.devtools.panels.create()chrome.runtimechrome.downloadschrome.actionchrome.notificationschrome.action.setIconchrome.desktopCapture.chooseDesktopMediatargetTabtabschrome.windowsgetAllgetLastFocusedgetCurrent.query()sidePanel.setPanelBehavioropenPanelOnActionClickopenPanelOnActionIconClickhost_permissions<all_urls>return trueonMessage