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.md
source 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

FeatureMV2MV3Notes
BackgroundPersistent pageService workerNo DOM access in MV3
Remote codeAllowedForbiddenMust bundle all scripts
Host permissionsIn permissionsSeparate fieldMore granular control
Content scriptsSameSameNo changes
Web accessibleArrayObject with matchesPer-resource rules
CSPConfigurableRestrictedNo unsafe-eval
ActionbrowserAction/pageActionactionUnified API
DeclarativeOptionalRequired for webRequestdeclarativeNetRequest

Browser Support Matrix

BrowserMV3 StatusMV2 StatusMinimum Version
ChromeRequiredDeprecated88+ (full), 102+ (service workers)
FirefoxSupportedSupported109+ (MV3), 48+ (MV2)
SafariRequiredNot supported15.4+
EdgeRequiredDeprecated88+

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:

  1. Xcode project wrapper
  2. App Store distribution
  3. Privacy manifest (
    PrivacyInfo.xcprivacy
    )
  4. 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

TypeMV3 LocationPurpose
API permissions
permissions
Access to browser APIs
Host permissions
host_permissions
Access to web content
Optional
optional_permissions
Runtime-requested
Optional host
optional_host_permissions
Runtime-requested URLs

Permission Reference

PermissionDescriptionUser Warning
activeTab
Current tab on user actionNone
alarms
Schedule code executionNone
bookmarks
Read/write bookmarksYes
clipboardRead
Read clipboardYes
clipboardWrite
Write clipboardNone
contextMenus
Custom context menusNone
cookies
Read/write cookiesYes
declarativeNetRequest
Modify network requestsNone
downloads
Manage downloadsNone
geolocation
Access locationYes (at use)
history
Browser historyYes
identity
OAuth flowsNone
notifications
System notificationsNone
scripting
Execute scriptsNone
storage
Extension storageNone
tabs
Tab URLs and titlesYes
webNavigation
Navigation eventsNone
webRequest
Observe requestsNone (MV3)

Host Permissions

{
  "host_permissions": [
    "https://api.example.com/*",
    "*://*.example.org/*"
  ],
  "optional_host_permissions": [
    "<all_urls>"
  ]
}

Permission Patterns

PatternMatchesNotes
<all_urls>
All URLsRequires justification
*://*/*
All HTTP(S)Same as all_urls for web
https://*.example.com/*
SubdomainsSingle domain family
https://example.com/api/*
Path prefixMost restrictive

Cross-Browser Permission Differences

FeatureChromeFirefoxSafari
activeTab
+
scripting
Full injectionFull injectionLimited to declared
declarativeNetRequest
Full supportPartialFull support
offscreen
SupportedNot supportedNot supported
sidePanel
SupportedNot supportedNot 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

WorldAccessUse Case
ISOLATED
(default)
Own JS contextMost extensions
MAIN
Page's JS contextPage 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

TypeQuotaSyncPersistence
local
10MB (unlimited with permission)NoUntil cleared
sync
100KB total, 8KB/itemYesCross-device
session
10MBNoUntil browser close
managed
N/AN/AAdmin-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
    manifest_version
    to 3
  • Convert background page to service worker
  • Move
    host_permissions
    from
    permissions
  • Replace
    browser_action
    /
    page_action
    with
    action
  • Remove remote code loading
  • Update
    web_accessible_resources
    syntax
  • Replace blocking
    webRequest
    with
    declarativeNetRequest

Code Changes

  • Remove DOM usage from background
  • Add state persistence (storage.session)
  • Use
    chrome.scripting
    for injection
  • 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