Agents manifest-v3-reference
Complete reference for Manifest V3 browser extension development with cross-browser compatibility and migration guidance
install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/web/browser-extension-dev/skills/manifest-v3-reference" ~/.claude/skills/arustydev-agents-manifest-v3-reference && rm -rf "$T"
manifest:
content/plugins/web/browser-extension-dev/skills/manifest-v3-reference/SKILL.mdsource content
Manifest V3 Reference
Complete reference for Manifest V3 browser extension development with cross-browser compatibility notes, Firefox MV2 fallbacks, and Safari-specific considerations.
Manifest Version Comparison
| Feature | MV2 | MV3 | Notes |
|---|---|---|---|
| Background | Persistent page | Service worker | No DOM access in MV3 |
| Remote code | Allowed | Forbidden | Must bundle all scripts |
| Host permissions | In permissions | Separate field | More granular control |
| Content scripts | Same | Same | No changes |
| Web accessible | Array | Object with matches | Per-resource rules |
| CSP | Configurable | Restricted | No unsafe-eval |
| Action | browserAction/pageAction | action | Unified API |
| Declarative | Optional | Required for webRequest | declarativeNetRequest |
Browser Support Matrix
| Browser | MV3 Status | MV2 Status | Minimum Version |
|---|---|---|---|
| Chrome | Required | Deprecated | 88+ (full), 102+ (service workers) |
| Firefox | Supported | Supported | 109+ (MV3), 48+ (MV2) |
| Safari | Required | Not supported | 15.4+ |
| Edge | Required | Deprecated | 88+ |
Core Manifest Structure
Minimal MV3 Manifest
{ "manifest_version": 3, "name": "Extension Name", "version": "1.0.0", "description": "Brief description (max 132 chars for Chrome)", "icons": { "16": "icons/16.png", "32": "icons/32.png", "48": "icons/48.png", "128": "icons/128.png" }, "action": { "default_icon": "icons/48.png", "default_popup": "popup.html", "default_title": "Click to open" }, "permissions": [ "storage" ], "background": { "service_worker": "background.js", "type": "module" } }
Firefox-Specific Fields
{ "browser_specific_settings": { "gecko": { "id": "extension@example.com", "strict_min_version": "109.0", "strict_max_version": "130.*", "data_collection_permissions": { "required": [], "optional": ["technicalAndInteraction"] } } } }
Safari-Specific Considerations
Safari extensions require:
- Xcode project wrapper
- App Store distribution
- Privacy manifest (
)PrivacyInfo.xcprivacy - Code signing
# Convert existing extension xcrun safari-web-extension-converter ./extension-dir \ --project-location ./safari-project \ --app-name "My Extension"
Background Scripts
MV3 Service Worker
// background.ts (MV3) // Service worker - no DOM, no persistent state // Use alarms for periodic tasks chrome.alarms.create('periodic-task', { periodInMinutes: 5 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'periodic-task') { performTask(); } }); // State must be stored explicitly chrome.runtime.onInstalled.addListener(() => { chrome.storage.local.set({ initialized: true }); }); // Handle messages chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { handleMessage(message).then(sendResponse); return true; // Async response });
MV2 Background Page (Firefox fallback)
{ "background": { "scripts": ["background.js"], "persistent": false } }
// background.js (MV2) // Event page with DOM access let state = {}; browser.runtime.onMessage.addListener((message, sender) => { return handleMessage(message); }); // Can use DOM APIs const parser = new DOMParser();
Cross-Browser Background Detection
// Detect environment const isServiceWorker = typeof ServiceWorkerGlobalScope !== 'undefined' && self instanceof ServiceWorkerGlobalScope; const isEventPage = typeof window !== 'undefined'; // Choose appropriate APIs function createOffscreenDocument() { if (chrome.offscreen) { return chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['DOM_PARSER'], justification: 'Parse HTML content' }); } // MV2/Firefox: Use background page DOM directly return Promise.resolve(); }
Permissions
Permission Types
| Type | MV3 Location | Purpose |
|---|---|---|
| API permissions | | Access to browser APIs |
| Host permissions | | Access to web content |
| Optional | | Runtime-requested |
| Optional host | | Runtime-requested URLs |
Permission Reference
| Permission | Description | User Warning |
|---|---|---|
| Current tab on user action | None |
| Schedule code execution | None |
| Read/write bookmarks | Yes |
| Read clipboard | Yes |
| Write clipboard | None |
| Custom context menus | None |
| Read/write cookies | Yes |
| Modify network requests | None |
| Manage downloads | None |
| Access location | Yes (at use) |
| Browser history | Yes |
| OAuth flows | None |
| System notifications | None |
| Execute scripts | None |
| Extension storage | None |
| Tab URLs and titles | Yes |
| Navigation events | None |
| Observe requests | None (MV3) |
Host Permissions
{ "host_permissions": [ "https://api.example.com/*", "*://*.example.org/*" ], "optional_host_permissions": [ "<all_urls>" ] }
Permission Patterns
| Pattern | Matches | Notes |
|---|---|---|
| All URLs | Requires justification |
| All HTTP(S) | Same as all_urls for web |
| Subdomains | Single domain family |
| Path prefix | Most restrictive |
Cross-Browser Permission Differences
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
+ | Full injection | Full injection | Limited to declared |
| Full support | Partial | Full support |
| Supported | Not supported | Not supported |
| Supported | Not supported | Not supported |
Action API
MV3 Unified Action
// MV3: Single unified action API chrome.action.setIcon({ path: 'icons/active.png' }); chrome.action.setBadgeText({ text: '5' }); chrome.action.setBadgeBackgroundColor({ color: '#FF0000' }); chrome.action.setTitle({ title: 'New title' }); chrome.action.onClicked.addListener((tab) => { // No popup defined - handle click });
MV2 browserAction/pageAction
// MV2: Separate APIs browser.browserAction.setIcon({ path: 'icons/active.png' }); browser.pageAction.show(tabId);
Cross-Browser Action Pattern
// Unified wrapper const action = chrome.action || chrome.browserAction || browser.browserAction; function setIcon(path: string) { return action.setIcon({ path }); } function setBadge(text: string) { return action.setBadgeText({ text }); }
Content Scripts
Manifest Declaration
{ "content_scripts": [ { "matches": ["https://*.example.com/*"], "js": ["content.js"], "css": ["content.css"], "run_at": "document_idle", "all_frames": false, "match_about_blank": false, "world": "ISOLATED" } ] }
Programmatic Injection (MV3)
// MV3: scripting API chrome.scripting.executeScript({ target: { tabId }, files: ['inject.js'], world: 'ISOLATED' }); // With function chrome.scripting.executeScript({ target: { tabId }, func: (arg) => { console.log('Injected with', arg); }, args: ['argument'] });
Programmatic Injection (MV2)
// MV2: tabs API browser.tabs.executeScript(tabId, { file: 'inject.js', runAt: 'document_idle' });
World Isolation
| World | Access | Use Case |
|---|---|---|
(default) | Own JS context | Most extensions |
| Page's JS context | Page script modification |
// MAIN world injection (MV3) chrome.scripting.executeScript({ target: { tabId }, func: () => { // Can access page's window, modify prototypes window.pageVariable = 'modified'; }, world: 'MAIN' });
Network Request Handling
Declarative Net Request (MV3)
{ "permissions": ["declarativeNetRequest"], "declarative_net_request": { "rule_resources": [ { "id": "ruleset_1", "enabled": true, "path": "rules.json" } ] } }
[ { "id": 1, "priority": 1, "action": { "type": "block" }, "condition": { "urlFilter": "||ads.example.com", "resourceTypes": ["script", "image"] } }, { "id": 2, "priority": 2, "action": { "type": "redirect", "redirect": { "url": "https://example.com/blocked" } }, "condition": { "urlFilter": "tracker.js", "resourceTypes": ["script"] } } ]
Dynamic Rules (MV3)
// Add rules at runtime chrome.declarativeNetRequest.updateDynamicRules({ addRules: [ { id: 1000, priority: 1, action: { type: 'block' }, condition: { urlFilter: userBlockedDomain } } ] });
WebRequest (MV2)
// MV2: Blocking webRequest browser.webRequest.onBeforeRequest.addListener( (details) => { if (shouldBlock(details.url)) { return { cancel: true }; } }, { urls: ['<all_urls>'] }, ['blocking'] );
Cross-Browser Network Handling
// Check for MV3 declarativeNetRequest if (chrome.declarativeNetRequest) { // Use declarative rules setupDeclarativeRules(); } else if (browser.webRequest) { // Fall back to MV2 webRequest setupWebRequestListeners(); }
Web Accessible Resources
MV3 Syntax
{ "web_accessible_resources": [ { "resources": ["inject.js", "styles.css"], "matches": ["https://*.example.com/*"], "use_dynamic_url": true }, { "resources": ["public/*"], "matches": ["<all_urls>"], "extension_ids": [] } ] }
MV2 Syntax
{ "web_accessible_resources": [ "inject.js", "styles.css", "public/*" ] }
Accessing Resources
// Get resource URL const url = chrome.runtime.getURL('inject.js'); // chrome-extension://EXTENSION_ID/inject.js // With dynamic URL (MV3) // chrome-extension://EXTENSION_ID/RANDOM_TOKEN/inject.js
Messaging
Internal Messaging
// Send from content script to background chrome.runtime.sendMessage({ type: 'getData' }, (response) => { console.log('Received:', response); }); // Background listener chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'getData') { getData().then(sendResponse); return true; // Async } });
Tab Messaging
// Background to specific tab chrome.tabs.sendMessage(tabId, { type: 'update' }); // Content script listener chrome.runtime.onMessage.addListener((message) => { if (message.type === 'update') { updateUI(); } });
External Messaging
{ "externally_connectable": { "matches": ["https://app.example.com/*"] } }
// From web page chrome.runtime.sendMessage(extensionId, { type: 'request' }, (response) => { console.log('Extension responded:', response); }); // In extension chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { // Validate sender.url if (isAllowedOrigin(sender.url)) { handleExternalMessage(message).then(sendResponse); return true; } });
Storage API
Storage Types
| Type | Quota | Sync | Persistence |
|---|---|---|---|
| 10MB (unlimited with permission) | No | Until cleared |
| 100KB total, 8KB/item | Yes | Cross-device |
| 10MB | No | Until browser close |
| N/A | N/A | Admin-configured |
Usage
// Set values await chrome.storage.local.set({ key: 'value' }); // Get values const result = await chrome.storage.local.get(['key']); console.log(result.key); // Remove values await chrome.storage.local.remove(['key']); // Listen for changes chrome.storage.onChanged.addListener((changes, areaName) => { for (const [key, { oldValue, newValue }] of Object.entries(changes)) { console.log(`${key} changed from ${oldValue} to ${newValue}`); } });
Session Storage (MV3)
// Memory-only storage - cleared when browser closes await chrome.storage.session.set({ temporaryData: value }); // Set access level for content scripts await chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' });
Alarms API
// Create alarm chrome.alarms.create('myAlarm', { delayInMinutes: 1, periodInMinutes: 5 }); // Listen for alarm chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'myAlarm') { performPeriodicTask(); } }); // Clear alarm chrome.alarms.clear('myAlarm');
Offscreen Documents (MV3 Chrome only)
// Create offscreen document for DOM access async function createOffscreen() { if (await chrome.offscreen.hasDocument()) return; await chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['DOM_PARSER', 'CLIPBOARD'], justification: 'Parse HTML and access clipboard' }); } // Communicate with offscreen document chrome.runtime.sendMessage({ target: 'offscreen', data: htmlContent });
Reasons:
AUDIO_PLAYBACK, BLOBS, CLIPBOARD, DOM_PARSER, DOM_SCRAPING, GEOLOCATION, LOCAL_STORAGE, MATCH_MEDIA, TESTING, USER_MEDIA, WEB_RTC, WORKERS
Side Panel (MV3 Chrome only)
{ "side_panel": { "default_path": "sidepanel.html" }, "permissions": ["sidePanel"] }
// Open side panel chrome.sidePanel.open({ tabId }); // Set panel behavior chrome.sidePanel.setOptions({ tabId, path: 'sidepanel.html', enabled: true });
Cross-Browser Compatibility Patterns
API Detection
// Check for API availability function hasAPI(name: string): boolean { const parts = name.split('.'); let obj: any = chrome; for (const part of parts) { if (obj[part] === undefined) return false; obj = obj[part]; } return true; } // Usage if (hasAPI('offscreen.createDocument')) { // Chrome MV3 offscreen } else if (hasAPI('tabs.executeScript')) { // MV2 injection }
Browser-Specific Manifest
// manifest.json for Chrome { "manifest_version": 3, "background": { "service_worker": "background.js" } } // manifest.json for Firefox { "manifest_version": 3, "background": { "scripts": ["background.js"] }, "browser_specific_settings": { "gecko": { "id": "extension@example.com" } } }
WXT Cross-Browser Handling
// wxt.config.ts export default defineConfig({ manifest: { // Common fields name: 'Extension', // Browser-specific overrides $browser_specific: { firefox: { browser_specific_settings: { gecko: { id: 'extension@example.com' } } } } } });
Migration Checklist: MV2 to MV3
Required Changes
- Update
to 3manifest_version - Convert background page to service worker
- Move
fromhost_permissionspermissions - Replace
/browser_action
withpage_actionaction - Remove remote code loading
- Update
syntaxweb_accessible_resources - Replace blocking
withwebRequestdeclarativeNetRequest
Code Changes
- Remove DOM usage from background
- Add state persistence (storage.session)
- Use
for injectionchrome.scripting - Handle service worker lifecycle
- Add offscreen document if DOM needed
Testing
- Test after browser restart
- Test after extension reload
- Test alarm persistence
- Test message handling timing
- Test content script injection
Related Resources
- wxt-framework-patterns skill: WXT-specific patterns
- cross-browser-compatibility skill: API compatibility matrices
- extension-security skill: Security best practices