Guides the creation, debugging, and publication of Chrome browser extensions.
Install
mkdir -p .claude/skills/chrome-extensions && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16748" && unzip -o skill.zip -d .claude/skills/chrome-extensions && rm skill.zipInstalls to .claude/skills/chrome-extensions
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Ensure icon files exist and match specified dimensions
- →Provide a way to open the side panel in an extension
- →Execute code in sandboxed iframes or Blob URLs to bypass CSP
- →Require `tabs` permission for accessing `tab.url` or `tab.title`
- →Use `async`/`await` for asynchronous operations
How it works
The skill provides mandatory rules and best practices for building Chrome Extensions, covering aspects like icon handling, side panel activation, code execution, and API usage.
Inputs & outputs
When to use chrome-extensions
- →Build browser extension
- →Debug extension service worker
- →Prepare for Web Store publishing
About this skill
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
❌ 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)
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:
// 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 });
});
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.
// ❌ 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>`;
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.
// manifest.json — REQUIRED if you read tab.url or tab.title anywhere:
{ "permissions": ["tabs"] }
See references/extensions/tab-management.md.
5. Always use async/await — never .then() chains
// ❌ 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'] });
For runtime.onMessage listeners that do async work:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const data = await chrome.storage.local.get('key');
sendResponse({ data });
})();
return true; // keeps channel open
});
6. Content scripts: don't block the main thread
When modifying many DOM elements, batch with requestAnimationFrame and yield between batches:
async 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();
}
}
See references/extensions/content-scripts.md.
7. Service workers are ephemeral — never store state in variables
// ❌ 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) });
});
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:
- Pack the extension once (chrome://extensions → Pack)
- Extract the public key from the .crx
- 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:
const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }
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.
// ❌ 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" } }
12. activeTab only works on direct user gestures — not from side panels
activeTab grants temporary access to the current tab ONLY when triggered by:
- Clicking the extension action icon
- A context menu item
- A keyboard shortcut from the
commandsAPI - Accepting an omnibox suggestion
It does NOT grant access when clicking a button in a side panel, popup button that opens later, or any programmatic trigger.
// ❌ 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>"] }
See 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().
// ❌ 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");
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.
// ❌ 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' }); // TypeError
The only APIs available in offscreen documents are:
chrome.runtime.sendMessage/chrome.runtime.onMessagechrome.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 referenc
Content truncated.
When not to use it
- →When developing extensions for browsers other than Chrome
- →When the project does not involve Manifest V3 best practices
Limitations
- →The skill is specific to Chrome Extensions and Manifest V3.
- →The skill focuses on building and publishing extensions, not general web development.
How it compares
This skill enforces Manifest V3 best practices and common fixes during Chrome Extension development, reducing common errors and ensuring compliance, unlike manual development that might overlook these details.
Compared to similar skills
chrome-extensions side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| chrome-extensions (this skill) | 0 | 3mo | Review | Intermediate |
| flutter-development | 1,555 | 5mo | No flags | Intermediate |
| frontend-design | 481 | 2mo | No flags | Advanced |
| webapp-testing | 353 | 4mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
frontend-design
anthropics
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
webapp-testing
anthropics
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
nextjs-developer
zenobi-us
Expert Next.js developer mastering Next.js 14+ with App Router and full-stack features. Specializes in server components, server actions, performance optimization, and production deployment with focus on building fast, SEO-friendly applications.
screenshot-to-code
OneWave-AI
Convert UI screenshots into working HTML/CSS/React/Vue code. Detects design patterns, components, and generates responsive layouts. Use this when users provide screenshots of websites, apps, or UI designs and want code implementation.
zustand
lobehub
Zustand state management guide. Use when working with store code (src/store/**), implementing actions, managing state, or creating slices. Triggers on Zustand store development, state management questions, or action implementation.