Skill
Community

Manifest V3 Reference

Install
1
Install the plugin
$
npx claudepluginhub arustydev/agents --plugin browser-extension-dev

Want just this skill?

Then install: npx claudepluginhub u/[userId]/[slug]

Description

Complete reference for Manifest V3 browser extension development with cross-browser compatibility notes, Firefox MV2 fallbacks, and Safari-specific considerations.

Tool Access

This skill uses the workspace's default tool permissions.

Skill 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 permissionspermissionsAccess to browser APIs
Host permissionshost_permissionsAccess to web content
Optionaloptional_permissionsRuntime-requested
Optional hostoptional_host_permissionsRuntime-requested URLs

Permission Reference

PermissionDescriptionUser Warning
activeTabCurrent tab on user actionNone
alarmsSchedule code executionNone
bookmarksRead/write bookmarksYes
clipboardReadRead clipboardYes
clipboardWriteWrite clipboardNone
contextMenusCustom context menusNone
cookiesRead/write cookiesYes
declarativeNetRequestModify network requestsNone
downloadsManage downloadsNone
geolocationAccess locationYes (at use)
historyBrowser historyYes
identityOAuth flowsNone
notificationsSystem notificationsNone
scriptingExecute scriptsNone
storageExtension storageNone
tabsTab URLs and titlesYes
webNavigationNavigation eventsNone
webRequestObserve 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 + scriptingFull injectionFull injectionLimited to declared
declarativeNetRequestFull supportPartialFull support
offscreenSupportedNot supportedNot supported
sidePanelSupportedNot 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
MAINPage'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
local10MB (unlimited with permission)NoUntil cleared
sync100KB total, 8KB/itemYesCross-device
session10MBNoUntil browser close
managedN/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
Stats
Stars6
Forks2
Last CommitMar 18, 2026

Similar Skills