{"id":20769,"plugin_id":"plugins_6aa7f81337c481918a5ee55e13498c40","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:16:29.123Z","digest":"37dfceb854097e5e0c4d2883a465cbda409b544162016e5a071920a57a14d321","against":null,"payload":{"description":"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), 'userScripts', 'user script', 'script manager', 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.","included_files":[{"relative_path":"references/extensions/api-calling.md","size_in_bytes":2188},{"relative_path":"references/extensions/auth-identity.md","size_in_bytes":4036},{"relative_path":"references/extensions/content-scripts.md","size_in_bytes":3506},{"relative_path":"references/extensions/context-menus.md","size_in_bytes":1844},{"relative_path":"references/extensions/csp-sandbox.md","size_in_bytes":4810},{"relative_path":"references/extensions/declarative-net-request.md","size_in_bytes":2794},{"relative_path":"references/extensions/devtools.md","size_in_bytes":2889},{"relative_path":"references/extensions/icons.md","size_in_bytes":2756},{"relative_path":"references/extensions/media-capture.md","size_in_bytes":4805},{"relative_path":"references/extensions/message-passing.md","size_in_bytes":4214},{"relative_path":"references/extensions/omnibox.md","size_in_bytes":1819},{"relative_path":"references/extensions/permissions.md","size_in_bytes":4641},{"relative_path":"references/extensions/popup-ui.md","size_in_bytes":2378},{"relative_path":"references/extensions/prompt-api.md","size_in_bytes":3835},{"relative_path":"references/extensions/service-worker.md","size_in_bytes":4591},{"relative_path":"references/extensions/side-panel.md","size_in_bytes":4480},{"relative_path":"references/extensions/storage.md","size_in_bytes":1965},{"relative_path":"references/extensions/tab-management.md","size_in_bytes":3756},{"relative_path":"references/extensions/user-scripts.md","size_in_bytes":9414},{"relative_path":"references/webstore/chromewebstore-template.md","size_in_bytes":5786},{"relative_path":"references/webstore/privacy-policy.md","size_in_bytes":4247},{"relative_path":"references/webstore/review-checklist.md","size_in_bytes":7074},{"relative_path":"references/webstore/store-listing.md","size_in_bytes":8033}],"name":"chrome-extensions","skill_md_contents":"---\nname: chrome-extensions\ndescription: >\n  Build and publish Chrome Extensions using Manifest V3 best practices. Use this skill\n  whenever the user asks to create, modify, debug, or understand Chrome browser extensions,\n  add-ons, or anything involving the Chrome Extensions API. Trigger on mentions of: 'Chrome\n  extension', 'browser extension', 'manifest.json', 'content script', 'service worker' (in\n  browser context), 'popup' (in browser extension context), 'side panel', 'chrome.* API',\n  'declarativeNetRequest', 'omnibox', 'context menu' (in extension context), 'userScripts',\n  'user script', 'script manager', or any request to build functionality that integrates with\n  the Chrome browser UI. Also trigger for publishing to the Chrome Web Store: 'publish\n  extension', preparing an extension for publishing, responding to a review rejection, writing\n  permission justifications, or drafting a privacy policy.\n---\n\n# Chrome Extensions\n\nBuild production-quality Chrome extensions using Manifest V3 and publish them to the Chrome Web Store.\n\n## Part 1 — Building Extensions\n\n### Mandatory Rules\n\nThese address the most common causes of broken extensions. Violating any produces a non-functional build.\n\n#### 1. Icons: only reference files you create — or omit icons entirely\n\n```\n❌ BROKEN — referencing files that don't exist or reusing one file for all sizes:\n   \"icons\": { \"16\": \"icon.png\", \"48\": \"icon.png\", \"128\": \"icon.png\" }\n\n✅ CORRECT — each size is a separate file at the correct pixel dimensions:\n   \"icons\": { \"16\": \"icons/icon-16.png\", \"48\": \"icons/icon-48.png\", \"128\": \"icons/icon-128.png\" }\n   (where icon-16.png is 16×16px, icon-48.png is 48×48px, icon-128.png is 128×128px)\n\n✅ ALSO CORRECT — omit icons from manifest if you cannot generate real PNG files:\n   (just remove the \"icons\" and \"default_icon\" fields — Chrome uses a default icon)\n```\n\n**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.\n\n#### 2. Side panel: you MUST provide a way to open it\n\nDefining `\"side_panel\": {\"default_path\": \"...\"}` does NOT make it openable. Add a trigger:\n\n```js\n// In service-worker.js — open side panel on extension icon click\n// IMPORTANT: chrome.action.onClicked ONLY fires when there is NO default_popup\nchrome.action.onClicked.addListener(async (tab) => {\n  await chrome.sidePanel.open({ windowId: tab.windowId });\n});\n```\n\nIf 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`.\n\n#### 3. Code execution: sandboxed iframes ONLY\n\nExtension CSP blocks `eval()`, `new Function()`, inline `<script>` in all extension pages.\n\n```js\n// ❌ BROKEN — direct iframe DOM access throws SecurityError\niframe.contentDocument.write(html);\n\n// ❌ BROKEN — eval in extension page\neval(userCode); // CSP blocks this\n\n// ✅ OPTION A: Sandbox in manifest + postMessage\n// manifest.json: { \"sandbox\": { \"pages\": [\"sandbox.html\"] } }\niframe.contentWindow.postMessage({ html, css, js }, '*');\n// sandbox.html receives and runs:\nwindow.addEventListener('message', (e) => { eval(e.data.js); /* allowed in sandbox */ });\n\n// ✅ OPTION B: Blob URL (creates separate origin, bypasses extension CSP)\niframe.src = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));\n\n// ✅ OPTION C: srcdoc\niframe.srcdoc = `<style>${css}</style>${html}<script>${js}<\\/script>`;\n```\n\nSee `references/extensions/csp-sandbox.md` for full details.\n\n#### 4. `tab.url` requires the `tabs` permission\n\nWithout it, `tab.url` silently returns `undefined` — no error thrown. See\n`references/extensions/permissions.md`.\n\n#### 5. Always use async/await — never `.then()` chains\n\n```js\n// ❌ BAD\nchrome.tabs.query({active: true, currentWindow: true}).then(tabs => {\n  chrome.scripting.executeScript({target: {tabId: tabs[0].id}, files: ['content.js']}).then(() => {});\n});\n\n// ✅ GOOD\nconst [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\nawait chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });\n```\n\nFor `runtime.onMessage` listeners that do async work:\n\n```js\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  (async () => {\n    const data = await chrome.storage.local.get('key');\n    sendResponse({ data });\n  })();\n  return true; // keeps channel open\n});\n```\n\n#### 6. Content scripts: don't block the main thread\n\nWhen modifying many DOM elements, batch with `requestAnimationFrame` and yield between batches:\n\n```js\nasync function highlightAll(elements) {\n  const BATCH = 20;\n  for (let i = 0; i < elements.length; i += BATCH) {\n    await new Promise(r => requestAnimationFrame(() => {\n      elements.slice(i, i + BATCH).forEach(el => el.style.backgroundColor = 'yellow');\n      r();\n    }));\n    if (globalThis.scheduler?.yield) await scheduler.yield();\n  }\n}\n```\n\nSee `references/extensions/content-scripts.md`.\n\n#### 7. Service workers are ephemeral — never store state in variables\n\n```js\n// ❌ BROKEN — state lost when SW terminates (~30s of inactivity)\nlet count = 0;\nchrome.tabs.onUpdated.addListener(() => { count++; });\n\n// ✅ CORRECT — persist in chrome.storage, read on every event\nchrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {\n  if (changeInfo.status !== 'complete') return;\n  const { count = 0 } = await chrome.storage.local.get('count');\n  await chrome.storage.local.set({ count: count + 1 });\n  await chrome.action.setBadgeText({ text: String(count + 1) });\n});\n```\n\nUse `chrome.alarms` instead of `setTimeout`/`setInterval`. See `references/extensions/service-worker.md`.\n\n#### 8. chrome.identity: extension ID differs between dev and production\n\nWhen 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.\n\nTo stabilize the ID during development, add a `\"key\"` field to manifest.json:\n1. Pack the extension once (chrome://extensions → Pack)\n2. Extract the public key from the .crx\n3. Add `\"key\": \"MIIBIjANBgkqh...\"` to manifest.json\n\nAlways document: \"After publishing to the Chrome Web Store, update the OAuth client with the store-assigned extension ID.\" See `references/extensions/auth-identity.md`.\n\n#### 9. Context menus: show user feedback after action\n\nWhen 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.\n\n#### 10. Prompt API: available in service workers, popup, and side panel\n\nThe `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:\n\n```js\nconst params = await LanguageModel.params();\n// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }\n```\n\nFor 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.\n\n#### 11. `chrome.action` API requires `action` in manifest\n\nUsing `chrome.action.setBadgeText`, `chrome.action.setIcon`, or `chrome.action.onClicked` requires\nan `\"action\"` key in manifest.json — even if it's empty. Without it, `chrome.action` is `undefined`.\n\n```js\n// ❌ BROKEN — manifest has no \"action\" key\nawait chrome.action.setBadgeText({ text: '5' });\n// TypeError: Cannot read properties of undefined (reading 'setBadgeText')\n\n// ✅ FIX — add \"action\" to manifest.json (at minimum an empty object)\n{ \"action\": {} }\n// or with a popup:\n{ \"action\": { \"default_popup\": \"popup/popup.html\" } }\n```\n\n#### 12. `activeTab` only works on direct user gestures — not from side panels\n\n`activeTab` grants temporary access to the current tab ONLY on a direct user gesture (action\nicon click, context menu item, keyboard shortcut, omnibox suggestion) — NOT from a button click\ninside a side panel or popup. Use `tabs` + `host_permissions` instead. See\n`references/extensions/permissions.md` and `references/extensions/side-panel.md`.\n\n#### 13. DevTools panel URLs are relative to the extension root\n\nWhen creating a DevTools panel, the panel HTML path is relative to the **extension root**, NOT\nrelative to the devtools page that calls `chrome.devtools.panels.create()`.\n\n```js\n// ❌ BROKEN — path relative to devtools/ directory\nchrome.devtools.panels.create(\"My Panel\", \"\", \"panel/panel.html\");\n\n// ✅ CORRECT — full path from extension root\nchrome.devtools.panels.create(\"My Panel\", \"\", \"devtools/panel/panel.html\");\n```\n\nSee `references/extensions/devtools.md`.\n\n#### 14. Offscreen documents have NO access to most chrome.* APIs\n\nOffscreen documents (`chrome.offscreen`) are **severely restricted**. Most `chrome.*` APIs\nare unavailable, including `chrome.downloads`, `chrome.tabs`, `chrome.action`, and others.\n\n```js\n// ❌ BROKEN — chrome.downloads is undefined in offscreen documents\nchrome.downloads.download({ url, filename: 'recording.webm' }); // TypeError\n\n// ❌ BROKEN — chrome.action is undefined in offscreen documents\nchrome.action.setBadgeText({ text: 'REC' }); // TypeError\n```\n\n**The only APIs available in offscreen documents are:**\n- `chrome.runtime.sendMessage` / `chrome.runtime.onMessage`\n- `chrome.runtime.getURL`\n- Standard Web APIs (DOM, fetch, MediaRecorder, Canvas, Web Audio, etc.)\n\n**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`.\n\n#### 15. Notifications and badge icons must reference real image files\n\n`chrome.notifications.create()` requires a valid `iconUrl` pointing to an actual image file.\nIf the file doesn't exist or the path is wrong, the call fails with `\"Unable to download all specified images.\"`\n\n```js\n// ❌ BROKEN — icon file doesn't exist\nchrome.notifications.create('reminder', {\n  type: 'basic',\n  iconUrl: 'icons/icon-128.png', // File not in extension!\n  title: 'Reminder',\n  message: 'Time is up!'\n});\n\n// ✅ Generate a data URL at runtime via OffscreenCanvas — no file needed.\n// See `references/extensions/icons.md` for a reusable implementation.\nconst iconUrl = await getIconDataUrl();\nchrome.notifications.create('reminder', { type: 'basic', iconUrl, title: 'Reminder', message: 'Time is up!' });\n```\n\nThis applies to ALL image references in chrome.* APIs — notifications, `chrome.action.setIcon`,\ncontext menu icons, etc. **If you reference a file, it must exist.**\n\n#### 16. Tab capture: guard against double-start with state locking\n\n`chrome.tabCapture.getMediaStreamId()` fails with `\"Cannot capture a tab with an active stream\"`\nif called while a previous capture is still active. Fast double-clicks on the extension icon\neasily trigger this. Use explicit state locking:\n\n```js\n// ❌ BROKEN — no guard against rapid clicks\nlet isRecording = false;\nchrome.action.onClicked.addListener(async (tab) => {\n  if (isRecording) { stopRecording(); isRecording = false; }\n  else { isRecording = true; startRecording(tab); } // Second click = \"active stream\" error\n});\n\n// ✅ CORRECT — use transitional states to lock out concurrent operations\n// State machine: 'idle' → 'starting' → 'recording' → 'stopping' → 'idle'\n// Store state in chrome.storage.session (survives SW restart, cleared on browser close)\nchrome.action.onClicked.addListener(async (tab) => {\n  const { recordingState = 'idle' } = await chrome.storage.session.get('recordingState');\n\n  if (recordingState === 'starting' || recordingState === 'stopping') return;\n\n  if (recordingState === 'idle') {\n    await chrome.storage.session.set({ recordingState: 'starting' });\n    try {\n      await startRecording(tab);\n      await chrome.storage.session.set({ recordingState: 'recording' });\n      await chrome.action.setBadgeText({ text: 'REC' });\n      await chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });\n    } catch (err) {\n      console.error('Failed to start recording:', err);\n      await chrome.storage.session.set({ recordingState: 'idle' });\n    }\n  } else if (recordingState === 'recording') {\n    await chrome.storage.session.set({ recordingState: 'stopping' });\n    try { await stopRecording(); }\n    finally {\n      await chrome.storage.session.set({ recordingState: 'idle' });\n      await chrome.action.setBadgeText({ text: '' });\n    }\n  }\n});\n```\n\nThis pattern applies to any chrome API that manages exclusive resources:\n`chrome.tabCapture`, `chrome.desktopCapture`, `chrome.offscreen.createDocument` (only one\noffscreen document allowed at a time). See `references/extensions/media-capture.md`.\n\n#### 17. `chrome.desktopCapture` requires a target tab with URL access\n\nWhen calling `chrome.desktopCapture.chooseDesktopMedia()` from a service worker, you must pass\nthe active tab as the `targetTab` parameter. The tab object must have its `url` field populated,\nwhich requires the `\"tabs\"` permission.\n\n```js\n// ❌ BROKEN — called without targetTab from service worker\nchrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], (streamId) => { ... });\n// Error: \"A target tab is required when called from a service worker context.\"\n\n// ❌ BROKEN — tab doesn't have url field (missing \"tabs\" permission)\nconst [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\nchrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => { ... });\n// Error: \"targetTab doesn't have URL field set.\"\n\n// ✅ CORRECT — \"tabs\" permission in manifest + pass tab object\n// manifest.json: { \"permissions\": [\"tabs\", \"desktopCapture\"] }\nconst [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\nchrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => {\n  if (!streamId) return; // User cancelled\n});\n```\n\n**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`.\n\n#### 18. User scripts: four non-obvious pitfalls\n\n`chrome.userScripts` runs **user-provided code** at runtime. Use it for script managers and\nuser automation — not for extension-bundled scripts.\n\n- **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.\n- **Registered scripts are cleared on extension update.** Persist configs in `chrome.storage`; re-register them in `runtime.onInstalled` for the `\"update\"` reason.\n- **Messaging requires explicit opt-in.** Call `configureWorld({ messaging: true })` first; listen on `runtime.onUserScriptMessage`, not `runtime.onMessage`.\n- **`ScriptSource` constraint:** each `js` entry must have exactly one of `code` or `file`. **`id` constraint:** cannot start with `_`.\n\nSee `references/extensions/user-scripts.md`.\n\n#### 19. `chrome.windows` has NO `.query()` method — use `getAll`, `getLastFocused`, or `getCurrent`\n\nUnlike `chrome.tabs.query()`, the `chrome.windows` API does NOT have a `.query()` method.\n\n```js\n// ❌ BROKEN — chrome.windows.query does not exist\nconst windows = await chrome.windows.query({ focused: true });\n// TypeError: chrome.windows.query is not a function\n\n// ✅ CORRECT — use the right method for your need\nconst focused = await chrome.windows.getLastFocused({ populate: true });\nconst current = await chrome.windows.getCurrent({ populate: true });\nconst all     = await chrome.windows.getAll({ populate: true });\n```\n\n**`chrome.windows` methods:** `getAll`, `getLastFocused`, `getCurrent`, `get(windowId)`, `create`, `update`, `remove`. See `references/extensions/tab-management.md`.\n\n#### 20. `chrome.permissions.request()` in the service worker must be called with no `await` before it in the message listener\n\nA user gesture from a UI context (side panel, popup) does propagate across `chrome.runtime.sendMessage`\nto the service worker's `onMessage` listener — but only for that one synchronous turn. If the\nlistener does an `await` (even a short delay) before calling `chrome.permissions.request()`, the\ngesture is gone and the call throws `\"This function must be called during a user gesture\"`. Call\nit as the first thing in the listener, with nothing awaited before it — see\n`references/extensions/permissions.md`.\n\n### Always Manifest V3\n\nNever generate Manifest V2 code.\n- `background.service_worker` not `background.scripts`\n- `chrome.action` not `chrome.browserAction`\n- `chrome.scripting.executeScript` not `chrome.tabs.executeScript`\n- `host_permissions` is separate from `permissions`\n- No inline scripts in HTML — use `<script src=\"file.js\">`\n- No inline event handlers — use `addEventListener`\n\n---\n\n## Part 2 — Publishing to the Chrome Web Store\n\nManage `CHROMEWEBSTORE.md` — the single source of truth for all Chrome Web Store listing\nmetadata, permissions justifications, privacy disclosures, version history, and publishing\nreadiness for a Chrome extension project.\n\n### Core Workflow\n\nEvery time you touch a Chrome extension project in a way that affects its store presence,\nupdate (or create) `CHROMEWEBSTORE.md` in the project root. The file tracks everything the\ndeveloper needs to fill out in the Chrome Developer Dashboard, so they can copy-paste from\na single doc instead of scrambling at publish time.\n\n#### When to create CHROMEWEBSTORE.md\n\nCreate it the moment any of these happen:\n- The user says they want to publish an extension\n- The user asks to \"prepare for the store\" or \"get ready to publish\"\n- You're building a new extension that will clearly end up on the store\n- The user asks about store listing requirements\n\nUse the template in `references/webstore/chromewebstore-template.md` as your starting point. Read it\nbefore generating the file.\n\n#### When to update CHROMEWEBSTORE.md\n\nUpdate it whenever:\n- **User-facing changes**: Bump the \"Last Updated\" date, update the feature list in\n  descriptions, and add an entry to Version History\n- **manifest.json changes**: If permissions, host_permissions, or content_scripts changed,\n  update the Permissions Justification section — every permission needs a plain-English\n  reason the review team can understand\n- **New release**: Add a Version History entry with version number, date, and summary\n- **Privacy-relevant changes**: If data collection, storage, or transmission changed,\n  update the Privacy & Data Use section and the privacy policy\n- **Asset changes**: If icons or UI changed, note which screenshots need refreshing\n- **Rejection response**: If the user reports a CWS rejection, update the file with the\n  fix and add a note to Version History\n\n### How to fill it out\n\nFor each section, pull information from the actual project files:\n1. Read `manifest.json` to extract name, version, description, permissions, host_permissions\n2. Scan the codebase for data collection (storage, fetch calls, analytics)\n3. Check for icon files and their dimensions\n4. Look at the extension's UI to understand features for the description\n\nWrite store-facing copy in a tone that is specific, honest, and benefit-oriented. The Chrome\nWeb Store review team rejects vague descriptions. \"Makes your life easier\" will be rejected.\n\"Highlights search results on any webpage and lets you save highlights to a local list\" will\npass.\n\n**Never mention implementation details.** Users care what the extension does for them, not\nhow it was built. Strip any mention of APIs, libraries, frameworks, or code patterns:\n\n| ❌ Implementation detail (cut it) | ✅ User benefit (keep it) |\n|-----------------------------------|--------------------------|\n| \"Uses a MutationObserver to detect page changes\" | \"Automatically detects new content as you browse\" |\n| \"Built with custom elements and Shadow DOM\" | \"Works seamlessly without affecting page styles\" |\n| \"Powered by a service worker for background processing\" | \"Runs quietly in the background without slowing your browser\" |\n| \"Leverages the chrome.storage.sync API\" | \"Your settings sync across all your devices\" |\n| \"Implements declarativeNetRequest for filtering\" | \"Blocks ads and trackers without reading your page content\" |\n\n### CHROMEWEBSTORE.md Sections\n\nRead `references/webstore/chromewebstore-template.md` before generating the file — it defines\nwhat each section covers and how to fill it out. The highest-risk section is Permissions\nJustification: write a specific plain-English reason per permission and per host_permission.\n\"Needed for the extension to work\" will be rejected. Read `references/webstore/privacy-policy.md`\nfor guidance on generating a privacy policy.\n\n### Pre-Publish Checklist\n\nBefore submission, run through `references/webstore/review-checklist.md`. The most common\nfirst-submission failures:\n- Every permission and host_permission must have a specific justification (not \"needed to work\")\n- Privacy policy URL must be live and match the data use disclosure form\n- At least 1 screenshot at 1280×800 or 640×400\n- ZIP must exclude `.git/`, `node_modules/`, `.env`, `CHROMEWEBSTORE.md`\n\n### Store Listing Copy Guidelines\n\nFor copy guidelines and common rejection reasons, see `references/webstore/store-listing.md`.\nKey rule: lead with function (\"Highlights search terms on any webpage\"), not feeling (\"Enjoy\nsearching again\").\n\n---\n\n## Reference Files\n\nFor detailed API patterns and publishing guidance, read the relevant file BEFORE writing code or content:\n\n| Topic | Reference |\n|-------|-----------|\n| Permissions | `references/extensions/permissions.md` |\n| Side panels | `references/extensions/side-panel.md` |\n| Content scripts & DOM | `references/extensions/content-scripts.md` |\n| Popups | `references/extensions/popup-ui.md` |\n| Service worker lifetime | `references/extensions/service-worker.md` |\n| Code execution & CSP | `references/extensions/csp-sandbox.md` |\n| API calls | `references/extensions/api-calling.md` |\n| Declarative Net Request | `references/extensions/declarative-net-request.md` |\n| Chrome Prompt API | `references/extensions/prompt-api.md` |\n| DevTools panels | `references/extensions/devtools.md` |\n| Authentication | `references/extensions/auth-identity.md` |\n| Context menus | `references/extensions/context-menus.md` |\n| Omnibox | `references/extensions/omnibox.md` |\n| Storage | `references/extensions/storage.md` |\n| Tab & window management | `references/extensions/tab-management.md` |\n| Tab/desktop capture | `references/extensions/media-capture.md` |\n| User scripts | `references/extensions/user-scripts.md` |\n| Message passing | `references/extensions/message-passing.md` |\n| Icons | `references/extensions/icons.md` |\n| CHROMEWEBSTORE.md template | `references/webstore/chromewebstore-template.md` |\n| Privacy policy guidance | `references/webstore/privacy-policy.md` |\n| Pre-publish review checklist | `references/webstore/review-checklist.md` |\n| Store listing tips & rejections | `references/webstore/store-listing.md` |\n\n## Output Checklist\n\nVerify EVERY item before delivering:\n\n- [ ] `manifest_version: 3` — no V2 APIs anywhere\n- [ ] All icon files referenced in manifest exist as real files with correct dimensions — or icons are omitted\n- [ ] Side panel has an explicit open trigger (not just a manifest declaration)\n- [ ] Code execution uses sandbox/blob/srcdoc — no `eval()` in extension pages\n- [ ] `tabs` permission declared if `tab.url` or `tab.title` is accessed\n- [ ] All code uses `async`/`await` — no `.then()` chains\n- [ ] Content scripts batch DOM updates with `requestAnimationFrame`\n- [ ] Service worker stores NO state in global variables — uses `chrome.storage`\n- [ ] No inline scripts or event handlers in HTML\n- [ ] Context menu actions show user confirmation\n- [ ] `\"action\": {}` (or more) present in manifest if using `chrome.action.*` APIs\n- [ ] If reading/scripting tabs from a side panel: use `tabs` + `host_permissions` (NOT `activeTab`)\n- [ ] DevTools panel paths in `chrome.devtools.panels.create()` are relative to extension root\n- [ ] Offscreen documents use ONLY `chrome.runtime` messaging — no `chrome.downloads`, `chrome.action`, etc.\n- [ ] All image refs in `chrome.notifications`, `chrome.action.setIcon`, etc. point to real files (or use data URLs)\n- [ ] Tab/desktop capture uses state locking to prevent double-start errors\n- [ ] `chrome.desktopCapture.chooseDesktopMedia` passes `targetTab` with `tabs` permission\n- [ ] `chrome.windows` calls use `getAll`/`getLastFocused`/`getCurrent` — NOT `.query()` (it doesn't exist)\n- [ ] `chrome.permissions.request()` in a service worker `onMessage` listener is called with no `await` before it (gesture is lost after the first async gap)\n- [ ] `chrome.userScripts` availability checked before use (API throws if user hasn't enabled it)\n- [ ] User script configs persisted in `chrome.storage` and restored on `runtime.onInstalled` `\"update\"` reason\n- [ ] `configureWorld({ messaging: true })` called before user scripts send messages; listening on `onUserScriptMessage` not `onMessage`\n- [ ] `ScriptSource` entries each have exactly one of `code` or `file` (not both, not neither)\n- [ ] User script `id` values do not start with underscore\n- [ ] `sidePanel.setPanelBehavior` uses `openPanelOnActionClick` — NOT `openPanelOnActionIconClick`\n- [ ] Error handling on all async operations\n- [ ] `host_permissions` scoped to specific domains (not `<all_urls>` unless needed)\n- [ ] `return true` in `onMessage` listeners with async responses\n- [ ] Any use of `\"tab\"` in `chrome.contextMenus` `contexts` requires Chrome M150+\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}