chrome-extensions

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', 'm

By googlechrome · 5,080 installs

npx skills add googlechrome/modern-web-guidance --skill chrome-extensions

Source repository · Upstream listing

Chrome Extensions Build production quality Chrome extensions using Manifest V3 and publish them to the Chrome Web Store. Part 1 — Building Extensions Mandatory Rules These address the most common causes of broken extensions. Violating any produces a non functional build. 1. Icons: only reference files you create — or omit icons entirely If you include icon references, you MUST create the actual image files. Generate them with a script (see references/extensions/icons.md ) or leave them out. Never reference non existent files. 2. Side panel: you MUST provide a way to open it Defining "side panel": {"default path": "..."} does NOT make it openable. Add a trigger: If the extension has both a popup AND side panel, add a button in the popup that calls chrome.sidePanel.open() . Alternatively, use chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) — but the property is openPanelOnActionClick , NOT openPanelOnActionIconClick ; the "Icon" variant causes a synchronous TypeError that silently aborts the service worker. Do NOT also define default popup when using setPanelBehavior . See references/extensions/side panel.md . 3. Code execution: sandboxed iframes ONLY Extension CSP blocks eval() , new Function() , inline <script in all extension pages. See references/extensions/csp sandbox.md for full details. 4. tab.url requires the tabs permission Without it, tab.url silently returns undefined — no error thrown. See references/extensions/permissions.md . 5. Always use async/await — never .then() chains For runtime.onMessage listeners that do async work: 6. Content scripts: don't block the main thread When modifying many DOM elements, batch with requestAnimationFrame and yield between batches: See references/extensions/content scripts.md . 7. Service workers are ephemeral — never store state in variables Use chrome.alarms instead of setTimeout / setInterval . See references/extensions/service worker.md . 8. chrome.identity: extension ID differs between dev and production When using Google sign in, the OAuth client id is tied to a specific extension ID. The ID changes between unpacked development and the Chrome Web Store. To stabilize the ID during development, add a "key" field to manifest.json: 1. Pack the extension once (chrome://extensions → Pack) 2. Extract the public key from the .crx 3. Add "key": "MIIBIjANBgkqh..." to manifest.json Always document: "After publishing to the Chrome Web Store, update the OAuth client with the store assigned extension ID." See references/extensions/auth identity.md . 9. Context menus: show user feedback after action When a context menu item performs an action (save, copy, etc.), confirm it to the user. Use a notification, badge flash, or injected toast — don't let actions happen silently. See references/extensions/context menus.md for a complete toast implementation. 10. Prompt API: available in service workers, popup, and side panel The LanguageModel API works in all extension contexts — service worker, popup, and side panel — with no additional manifest permissions required. Extensions also get LanguageModel.params() , which is unavailable on the web: For general Prompt API patterns (availability checks, session creation, streaming), use the modern web guidance skill. See references/extensions/prompt api.md for the extension specific wiring example. 11. chrome.action API requires action in manifest Using chrome.action.setBadgeText , chrome.action.setIcon , or chrome.action.onClicked requires an "action" key in manifest.json — even if it's empty. Without it, chrome.action is undefined . 12. activeTab only works on direct user gestures — not from side panels activeTab grants temporary access to the current tab ONLY on a direct user gesture (action icon click, context menu item, keyboard shortcut, omnibox suggestion) — NOT from a button click inside a side panel or popup. Use tabs + host permissions instead. See references/extensions/permissions.md and references/extensions/side panel.md . 13. DevTools panel URLs are relative to the extension root When creating a DevTools panel, the panel HTML path is relative to the extension root , NOT relative to the devtools page that calls chrome.devtools.panels.create() . See references/extensions/devtools.md . 14. Offscreen documents have NO access to most chrome. APIs Offscreen documents ( chrome.offscreen ) are severely restricted . Most chrome. APIs are unavailable, including chrome.downloads , chrome.tabs , chrome.action , and others. The only APIs available in offscreen documents are: chrome.runtime.sendMessage / chrome.runtime.onMessage chrome.runtime.getURL Standard Web APIs (DOM, fetch, MediaRecorder, Canvas, Web Audio, etc.) Rule of thumb: Offscreen documents do the Web API work (recording, parsing, audio). The service worker does all chrome. API work (downloads, badge updates, notifications). Use chrome.runtime.sendMessage to bridge between them. See references/extensions/message passing.md . 15. Notifications and badge icons must reference real image files chrome.notifications.create() requires a valid iconUrl pointing to an actual image file. If the file doesn't exist or the path is wrong, the call fails with "Unable to download all specified images." This applies to ALL image references in chrome. APIs — notifications, chrome.action.setIcon , context menu icons, etc. If you reference a file, it must exist. 16. Tab capture: guard against double start with state locking chrome.tabCapture.getMediaStreamId() fails with "Cannot capture a tab with an active stream" if called while a previous capture is still active. Fast double clicks on the extension icon easily trigger this. Use explicit state locking: This pattern applies to any chrome API that manages exclusive resources: chrome.tabCapture , chrome.desktopCapture , chrome.offscreen.createDocument (only one offscreen document allowed at a time). See references/extensions/media capture.md . 17. chrome.desktopCapture requires a target tab with URL access When calling chrome.desktopCapture.chooseDesktopMedia() from a service worker, you must pass the active tab as the targetTab parameter. The tab object must have its url field populated, which requires the "tabs" permission. Note: Prefer chrome.tabCapture.getMediaStreamId() for tab only recording. Use chrome.desktopCapture only when the user should choose which screen/window to capture. See references/extensions/media capture.md . 18. User scripts: four non obvious pitfalls chrome.userScripts runs user provided code at runtime. Use it for script managers and user automation — not for extension bundled scripts. API throws on property access if not enabled. Chrome 138+ requires the user to toggle "Allow User Scripts" on the extension's details page; Chrome < 138 requires Developer mode. Always call isUserScriptsAvailable() before any chrome.userScripts. call and show an error UI when it returns false. Registered scripts are cleared on extension update. Persist configs in chrome.storage ; re register them in runtime.onInstalled for the "update" reason. Messaging requires explicit opt in. Call configureWorld({ messaging: true }) first; listen on runtime.onUserScriptMessage , not runtime.onMessage . ScriptSource constraint: each js entry must have exactly one of code or file . id constraint: cannot start with . See references/extensions/user scripts.md . 19. chrome.windows has NO .query() method — use getAll , getLastFocused , or getCurrent Unlike chrome.tabs.query() , the chrome.windows API does NOT have a .query() method. chrome.windows methods: getAll , getLastFocused , getCurrent , get(windowId) , create , update , remove . See references/extensions/tab management.md . 20. chrome.permissions.request() in the service worker must be called with no await before it in the message listener A user gesture from a UI context (side panel, popup) does propagate across chrome.runtime.sendMessage to the service worker's onMessage listener — but only for that one synchronous turn. If the listener does an await (even a short delay) before calling chrome.permissions.request() , the gesture is gone and the call throws "This function must be called during a user gesture" . Call it as the first thing in the listener, with nothing awaited before it — see references/extensions/permissions.md . Always Manifest V3 Never generate Manifest V2 code. background.service worker not background.scripts chrome.action not chrome.browserAction chrome.scripting.executeScript not chrome.tabs.executeScript host permissions is separate from permissions No inline scripts in HTML — use <script src="file.js" No inline event handlers — use addEventListener Part 2 — Publishing to the Chrome Web Store Manage CHROMEWEBSTORE.md — the single source of truth for all Chrome Web Store listing metadata, permissions justifications, privacy disclosures, version history, and publishing readiness for a Chrome extension project. Core Workflow Every time you touch a Chrome extension project in a way that affects its store presence, update (or create) CHROMEWEBSTORE.md in the project root. The file tracks everything the developer needs to fill out in the Chrome Developer Dashboard, so they can copy paste from a single doc instead of scrambling at publish time. When to create CHROMEWEBSTORE.md Create it the moment any of these happen: The user says they want to publish an extension The user asks to "prepare for the store" or "get ready to publish" You're building a new extension that will clearly end up on the store The user asks about store listing requirements Use the template in references/webstore/chromewebstore template.md as your starting point. Read it before generating the file. When to update CHROMEWEBSTORE.md Update it whenever: User facing changes : Bump the "Last Updated" date, update the feature list in descriptions, and add an entry to Version History manifest.json changes : If permissions, host permissions, or content scripts changed, update the Permissions Justification section — every permission needs a plain English reason the review team can understand New release : Add a Version History entry with version number, date, and summary Privacy relevant changes : If data collection, storage, or transmission changed, update the Privacy & Data Use section and the privacy policy Asset changes : If icons or UI changed, note which screenshots need refreshing Rejection response : If the user reports a CWS rejection, update the file with the fix and add a note to Version History How to fill it out For each section, pull information from the actual project files: 1. Read manifest.json to extract name, version, description, permissions, host permissions 2. Scan the codebase for data collection (storage, fetch calls, analytics) 3. Check for icon files and their dimensions 4. Look at the extension's UI to understand features for the description Write store facing copy in a tone that is specific, honest, and benefit oriented. The Chrome Web Store review team rejects vague descriptions. "Makes your life easier" will be rejected. "Highlights search results on any webpage and lets you save highlights to a local list" will pass. Never mention implementation details. Users care what the extension does for them, not how it was built. Strip any mention of APIs, libraries, frameworks, or code patterns: ❌ Implementation detail (cut it) ✅ User benefit (keep it) "Uses a MutationObserver to detect page changes" "Automatically detects new content as you browse" "