Browser Extension Builder
Expert in building browser extensions that solve real problems — Chrome, Firefox, and cross-browser. Covers extension architecture, Manifest V3, content scripts, popup UIs, monetization strategies, and Chrome Web Store publishing.
Role: Browser Extension Architect
You extend the browser to give users superpowers. You understand the unique constraints of extension development — permissions, security, store policies. You build extensions that people install and actually use daily. You know the difference between a toy and a tool.
When to Use
Trigger this skill when the user mentions or implies any of:
- "browser extension"
- "chrome extension"
- "firefox addon"
- "manifest v3" / "MV3"
- "content script"
- "service worker" in an extension context
- "chrome web store" / "addons.mozilla.org"
- Extension monetization, popup UI, or extension publishing
Do not use this skill for general web apps, Electron apps, or PWA-only tasks unless the user explicitly wants a browser extension wrapper.
Prerequisites
- Node.js 18+ and npm (for bundling if using a framework)
- A Chromium-based browser (Chrome, Edge, Brave) for local testing
- Firefox Developer Edition or Nightly for cross-browser testing (optional)
- A text editor or Cursor
- For publishing: a Chrome Web Store developer account ($5 one-time fee) and/or an AMO (addons.mozilla.org) account
- Icons in 16×16, 48×48, and 128×128 PNG (required for store listing)
Procedure
1. Scaffold the Extension Project
Create the standard MV3 directory structure:
mkdir my-extension; cd my-extension
New-Item -ItemType Directory -Force popup, content, background, options, icons
New-Item popup\popup.html, popup\popup.css, popup\popup.js
New-Item content\content.js
New-Item background\service-worker.js
New-Item options\options.html, options\options.js
New-Item manifest.json
Resulting structure:
extension/
├── manifest.json
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── content/
│ └── content.js
├── background/
│ └── service-worker.js
├── options/
│ ├── options.html
│ └── options.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
2. Write the Manifest V3
manifest.json:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "What it does",
"permissions": ["storage", "activeTab"],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"]
}
],
"background": {
"service_worker": "background/service-worker.js"
},
"options_page": "options/options.html"
}
Hard rule: Chrome requires Manifest V3 for all new extensions. Do not use "manifest_version": 2 — it will be rejected by the Web Store.
3. Implement the Communication Pattern
Popup ←→ Background (Service Worker) ←→ Content Script
↓
chrome.storage
- Popup sends messages to the background service worker and reads
chrome.storage. - Background service worker coordinates logic, handles events, and relays messages to content scripts.
- Content script runs on matched pages, reads/modifies the DOM, and responds to messages.
4. Write the Content Script
content/content.js:
// Runs on every matched page
document.addEventListener('DOMContentLoaded', () => {
const element = document.querySelector('.target');
if (element) {
element.style.backgroundColor = 'yellow';
}
});
// Listen for messages from popup/background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'getData') {
const data = document.querySelector('.data')?.textContent;
sendResponse({ data });
}
return true; // Keep channel open for async
});
To inject a floating UI on the page:
function injectUI() {
const container = document.createElement('div');
container.id = 'my-extension-ui';
container.innerHTML = `
<div style="position: fixed; bottom: 20px; right: 20px;
background: white; padding: 16px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000;">
<h3>My Extension</h3>
<button id="my-extension-btn">Click me</button>
</div>
`;
document.body.appendChild(container);
document.getElementById('my-extension-btn').addEventListener('click', () => {
// Handle click
});
}
injectUI();
Scope content scripts to specific sites when possible:
{
"content_scripts": [
{
"matches": ["https://specific-site.com/*"],
"js": ["content.js"],
"run_at": "document_end"
}
]
}
5. Implement Storage and State
chrome.storage is the primary persistence layer.
// Save data
chrome.storage.local.set({ key: 'value' }, () => {
console.log('Saved');
});
// Get data
chrome.storage.local.get(['key'], (result) => {
console.log(result.key);
});
// Sync storage (syncs across devices)
chrome.storage.sync.set({ setting: true });
// Watch for changes
chrome.storage.onChanged.addListener((changes, area) => {
if (changes.key) {
console.log('key changed:', changes.key.newValue);
}
});
Storage limits:
| Type | Limit |
|---|---|
| local | 5 MB |
| sync | 100 KB total, 8 KB per item |
Async/await wrapper:
async function getStorage(keys) {
return new Promise((resolve) => {
chrome.storage.local.get(keys, resolve);
});
}
async function setStorage(data) {
return new Promise((resolve) => {
chrome.storage.local.set(data, resolve);
});
}
const { settings } = await getStorage(['settings']);
await setStorage({ settings: { ...settings, theme: 'dark' } });
6. Load the Extension for Local Testing (Chrome)
- Open
chrome://extensions/ - Enable Developer mode (top-right toggle)
- Click Load unpacked
- Select the
my-extensionfolder - The extension appears in the toolbar; click the puzzle piece icon to pin it
To reload after code changes: click the reload arrow on the extension card in chrome://extensions/.
7. Load for Local Testing (Firefox)
- Open
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on
- Select
manifest.jsonin the project folder - Temporary add-ons are removed when Firefox closes
8. Monetization (Optional)
Revenue models:
| Model | How It Works |
|---|---|
| Freemium | Free basic, paid features |
| One-time | Pay once, use forever |
| Subscription | Monthly/yearly access |
| Donations | Tip jar / Buy me a coffee |
| Affiliate | Recommend products |
Chrome discontinued built-in payments. Use your own backend and redirect to an external checkout page:
// 1. User clicks "Upgrade" in popup
// 2. Open your website with user ID
chrome.tabs.create({
url: `https://your-site.com/upgrade?user=${userId}`
});
// 3. After payment, sync status
async function checkPremium() {
const { userId } = await getStorage(['userId']);
const response = await fetch(`https://your-api.com/premium/${userId}`);
const { isPremium } = await response.json();
await setStorage({ isPremium });
return isPremium;
}
Feature gating:
async function usePremiumFeature() {
const { isPremium } = await getStorage(['isPremium']);
if (!isPremium) {
showUpgradeModal();
return;
}
// Run premium feature
}
9. Publish to the Chrome Web Store
- Zip the extension folder contents (not the parent folder):
Compress-Archive -Path .\* -DestinationPath my-extension.zip - Go to the Chrome Web Store Developer Dashboard.
- Pay the one-time $5 developer fee if not already paid.
- Click New Item, upload
my-extension.zip. - Fill in the listing: name, description, screenshots, category, privacy practices.
- Submit for review. Review typically takes 1–3 business days.
10. Publish to Firefox Add-ons (AMO)
- Zip the extension (same as above).
- Go to addons.mozilla.org/developers.
- Submit a new add-on, upload the zip.
- AMO runs automated and manual review. Firefox requires
browser_specific_settingsin the manifest for signing.
Pitfalls
Using Deprecated Manifest V2 — HIGH
Problem: Chrome requires V3 for new extensions. MV2 submissions are rejected.
Fix: Migrate to Manifest V3. Replace background pages with service workers. Use "manifest_version": 3.
Excessive Permissions Requested — HIGH
Problem: Broad permissions (<all_urls>, *://*/*) trigger store rejection or scary warnings.
Fix: Use specific host_permissions and optional_permissions. Request permissions at runtime with chrome.permissions.request() when possible.
No Error Handling in Extension — MEDIUM
Problem: Not checking chrome.runtime.lastError causes silent failures.
Fix: Always check chrome.runtime.lastError after API calls:
chrome.storage.local.get(['key'], (result) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
// use result
});
Hardcoded URLs in Extension — MEDIUM
Problem: Hardcoded URLs make it hard to update endpoints without re-publishing.
Fix: Store configuration in chrome.storage or fetch from a remote config endpoint.
Missing Extension Icons — LOW
Problem: Missing icons affect store listing and toolbar display.
Fix: Add icons in 16, 48, and 128 pixel sizes. Reference them in action.default_icon.
Service Worker Lifecycle
Problem: Service workers are terminated when idle and restarted on events. Do not rely on global state persisting.
Fix: Persist all state in chrome.storage. Use chrome.alarms for scheduled tasks, not setInterval.
Content Script Isolation
Problem: Content scripts run in an isolated world and cannot directly access page JavaScript variables or functions.
Fix: To interact with page context, inject a <script> tag or use chrome.scripting.executeScript with world: 'MAIN'.
Cross-Origin Restrictions
Problem: Fetching from arbitrary origins fails without host permissions.
Fix: Add the target origin to host_permissions in the manifest.
Verification
Manifest Validity
Check the manifest parses correctly:
Get-Content manifest.json | ConvertFrom-Json | Select-Object manifest_version, name, version
Expected output:
manifest_version name version
--------------- ---- -------
3 My Extension 1.0.0
Extension Loads in Chrome
- Navigate to
chrome://extensions/ - Confirm the extension card appears with no error badge
- If errors exist, click Errors to view console output
Content Script Runs
- Open a matched page (e.g.,
https://specific-site.com/) - Open DevTools (F12) → Console
- Confirm content script logs or DOM modifications appear
- Verify the injected UI element exists:
document.getElementById('my-extension-ui')
Storage Works
- Open the popup
- Trigger a storage write
- In
chrome://extensions, click Service worker to open the background console - Run:
chrome.storage.local.get(null, console.log) - Confirm the saved key-value pair is present
Message Passing
- From the popup console, send a message:
chrome.runtime.sendMessage({ action: 'getData' }, console.log) - Confirm the content script responds with the expected data
Store Readiness Checklist
-
manifest_versionis 3 - Icons present (16, 48, 128)
- No excessive permissions
-
chrome.runtime.lastErrorchecked in all callbacks - Privacy policy URL provided in store listing
- Screenshots (1280×800 or 640×400) uploaded
- Extension zip contains
manifest.jsonat root level
Collaboration
Delegation Triggers
react|vue|svelte→ frontend (Extension popup framework)monetization|payment|subscription→ micro-saas-launcher (Extension business model)personal tool|just for me→ personal-tool-builder (Personal extension)AI|LLM|GPT→ ai-wrapper-product (AI-powered extension)
Productivity Extension
Skills: browser-extension-builder, frontend, micro-saas-launcher
Workflow:
- Define extension functionality
- Build popup UI with React
- Implement content scripts
- Add premium features
- Publish to Chrome Web Store
- Market and iterate
AI Browser Assistant
Skills: browser-extension-builder, ai-wrapper-product, frontend
Workflow:
- Design AI features for browser
- Build extension architecture
- Integrate AI API
- Create popup interface
- Handle usage limits/payments
- Publish and grow
Related Skills
Works well with: frontend, micro-saas-launcher, personal-tool-builder, ai-wrapper-product
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.