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"
"