Agent Skills: Firefox WebExtension Development

Comprehensive guide for developing WebExtensions (browser extensions) for Mozilla Firefox, including Manifest V2/V3 configuration, all WebExtension APIs, security practices, web-ext CLI, and AMO submission.

UncategorizedID: CodeAtCode/oss-ai-skills/firefox-extension

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CodeAtCode/oss-ai-skills/tree/HEAD/extend/firefox-extension

Skill Files

Browse the full folder contents for firefox-extension.

Download Skill

Loading file tree…

extend/firefox-extension/SKILL.md

Skill Metadata

Name
firefox-extension
Description
Comprehensive guide for developing WebExtensions (browser extensions) for Mozilla Firefox, including Manifest V2/V3 configuration, all WebExtension APIs, security practices, web-ext CLI, and AMO submission.

Firefox WebExtension Development

Complete reference for building, testing, and publishing browser extensions for Mozilla Firefox.

Overview

Firefox extensions use the WebExtensions API with the browser.* namespace (Promise-based natively). Firefox supports both Manifest V2 and V3, with MV2 NOT deprecated (unlike Chrome).

Key Characteristics:

  • Global namespace: browser (Promise-based)
  • Both MV2 and MV3 supported
  • Firefox-specific APIs: sidebarAction, userScripts, contextualIdentities, protocol_handlers
  • Submission via AMO (addons.mozilla.org)

Manifest Structure

Manifest V3 (Recommended, Firefox 109+)

{
  "manifest_version": 3,
  "name": "Extension Name",
  "version": "1.0.0",
  "description": "Brief description",

  "browser_specific_settings": {
    "gecko": {
      "id": "extension@example.com",
      "strict_min_version": "109.0"
    },
    "gecko_android": {
      "strict_min_version": "109.0"
    }
  },

  "background": {
    "service_worker": "background.js",
    "type": "module"
  },

  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "96": "icons/icon-96.png"
    },
    "default_title": "Extension Title"
  },

  "content_scripts": [
    {
      "matches": ["https://*/*"],
      "js": ["content.js"],
      "css": ["styles.css"],
      "run_at": "document_idle"
    }
  ],

  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://api.example.com/*"],

  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self';"
  }
}

Manifest V2 (Still Supported)

{
  "manifest_version": 2,
  "name": "Extension Name",
  "version": "1.0.0",

  "browser_specific_settings": {
    "gecko": {
      "id": "extension@example.com",
      "strict_min_version": "78.0"
    }
  },

  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },

  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": "icons/icon-48.png"
  },

  "permissions": ["storage", "activeTab", "https://*/*"]
}

MV2 vs MV3 Key Differences

| Feature | MV2 | MV3 | |---------|-----|-----| | Toolbar button | browser_action | action | | Background | background.scripts / page | background.service_worker | | Host permissions | In permissions | Separate host_permissions | | Default CSP | script-src 'self'; object-src 'self'; | script-src 'self'; upgrade-insecure-requests; | | Request blocking | webRequest.onBeforeRequest | declarativeNetRequest |

All Manifest Keys Reference

Metadata:

  • name (required) - Extension name
  • version (required) - Version string (e.g., "1.0.0")
  • description - Short description
  • author - Author name
  • homepage_url - Extension homepage
  • icons - Extension icons object

Firefox-Specific:

  • browser_specific_settings.gecko.id - Required for AMO
  • browser_specific_settings.gecko.strict_min_version - Minimum Firefox version
  • browser_specific_settings.gecko.strict_max_version - Maximum Firefox version
  • browser_specific_settings.gecko_android - Android-specific settings

Background & Scripts:

  • background - Service worker (MV3) or scripts/page (MV2)
  • content_scripts - Scripts injected into pages
  • userScripts - User script registration (Firefox-only)
  • declarative_net_request - Rule-based request modification

UI Components:

  • action (MV3) / browser_action (MV2) - Toolbar button
  • page_action - Address bar button
  • sidebar_action - Sidebar panel (Firefox-only)
  • options_ui - Options page configuration
  • devtools_page - DevTools extension page

Permissions:

  • permissions - API permissions
  • host_permissions (MV3) - Host access
  • optional_permissions - Optional API permissions
  • optional_host_permissions - Optional host access

Other:

  • commands - Keyboard shortcuts
  • omnibox - Address bar integration
  • web_accessible_resources - Resources accessible from pages
  • protocol_handlers - Custom protocol handlers
  • chrome_settings_overrides - Override homepage/search
  • chrome_url_overrides - Override new tab/bookmarks

WebExtension APIs

Complete API Namespace List (51 APIs)

| API | Permission | Description | |-----|------------|-------------| | action | - | Toolbar button (MV3) | | alarms | alarms | Schedule code execution | | bookmarks | bookmarks | Bookmark management | | browserAction | - | Toolbar button (MV2) | | browserSettings | - | Browser settings | | browsingData | browsingData | Clear browsing data | | clipboard | clipboardWrite | Clipboard access | | commands | - | Keyboard shortcuts | | contentScripts | - | Register content scripts | | contextualIdentities | contextualIdentities | Container tabs (Firefox-only) | | cookies | cookies | Cookie management | | declarativeNetRequest | declarativeNetRequest | Rule-based request blocking | | devtools | devtools | DevTools integration | | dns | dns | DNS resolution | | downloads | downloads | Download management | | events | - | Common event types | | extension | - | Extension utilities | | find | find | Find text in pages | | history | history | Browser history | | i18n | - | Internationalization | | identity | identity | OAuth2 authentication | | idle | idle | Idle state detection | | management | management | Installed add-ons info | | menus | menus | Context menu items | | notifications | notifications | System notifications | | omnibox | - | Address bar suggestions | | pageAction | - | Address bar button (MV2) | | permissions | - | Runtime permissions | | pkcs11 | pkcs11 | PKCS#11 modules | | privacy | privacy | Privacy settings | | proxy | proxy | Request proxying | | runtime | - | Extension runtime | | scripting | scripting | Inject scripts/CSS (MV3) | | search | search | Search engines | | sessions | sessions | Closed tabs/windows | | sidebarAction | - | Sidebar (Firefox-only) | | storage | storage | Local/managed storage | | tabGroups | tabGroups | Tab groups | | tabs | tabs | Tab management | | theme | theme | Theme API | | topSites | topSites | Frequently visited | | userScripts | userScripts | User scripts (Firefox-only) | | webNavigation | webNavigation | Navigation events | | webRequest | webRequest | Request interception | | windows | - | Window management |

Core API Usage Patterns

Tabs API:

// Query tabs
const tabs = await browser.tabs.query({ active: true, currentWindow: true });

// Create tab
const tab = await browser.tabs.create({ url: 'https://example.com' });

// Update tab
await browser.tabs.update(tabId, { active: true });

// Send message to tab
await browser.tabs.sendMessage(tabId, { action: 'update' });

// Execute script (MV3)
await browser.scripting.executeScript({
  target: { tabId },
  files: ['content.js']
});

Storage API:

// Save data
await browser.storage.local.set({ key: 'value', settings: config });

// Get data
const { key, settings } = await browser.storage.local.get(['key', 'settings']);

// Remove data
await browser.storage.local.remove('key');

// Clear all
await browser.storage.local.clear();

// Listen for changes
browser.storage.onChanged.addListener((changes, area) => {
  if (changes.key) {
    console.log('Old:', changes.key.oldValue, 'New:', changes.key.newValue);
  }
});

Runtime Messaging:

// Content script → Background
const response = await browser.runtime.sendMessage({ action: 'getData' });

// Background listener
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getData') {
    return Promise.resolve({ data: 'response' });
  }
});

// Background → Content script
browser.tabs.sendMessage(tabId, { action: 'update' });

// External messaging (from web pages)
browser.runtime.onMessageExternal.addListener((message, sender) => {
  if (sender.id === 'allowed-extension-id') {
    // Handle message
  }
});

Context Menus:

// Create context menu
browser.menus.create({
  id: 'my-menu',
  title: 'My Menu Item',
  contexts: ['selection']
});

// Handle click
browser.menus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'my-menu') {
    console.log('Selected:', info.selectionText);
  }
});

Alarms:

// Create alarm
browser.alarms.create('my-alarm', { delayInMinutes: 1, periodInMinutes: 5 });

// Handle alarm
browser.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'my-alarm') {
    // Do work
  }
});

Notifications:

// Show notification
browser.notifications.create({
  type: 'basic',
  iconUrl: 'icon.png',
  title: 'Title',
  message: 'Message'
});

Firefox-Specific APIs

Sidebar Action:

// Toggle sidebar
browser.sidebarAction.open();
browser.sidebarAction.close();

// Set panel
await browser.sidebarAction.setPanel({ panel: 'sidebar.html' });

Container Tabs (Contextual Identities):

// List containers
const containers = await browser.contextualIdentities.query({});

// Create container
const container = await browser.contextualIdentities.create({
  name: 'Work',
  color: 'blue',
  icon: 'briefcase'
});

// Create tab in container
await browser.tabs.create({
  url: 'https://work.example.com',
  cookieStoreId: container.cookieStoreId
});

User Scripts:

// Register user script
await browser.userScripts.register([{
  js: [{ file: 'script.js' }],
  matches: ['*://example.com/*'],
  runAt: 'document_start'
}]);

Security & Content Security Policy

Default CSP

MV3: script-src 'self'; upgrade-insecure-requests; MV2: script-src 'self'; object-src 'self';

CSP Restrictions

Forbidden (causes AMO rejection):

  • Remote script sources
  • 'unsafe-inline'
  • 'unsafe-eval' (except 'wasm-unsafe-eval' for WebAssembly)
  • Data URLs for scripts
  • eval(), new Function(), string-based code execution

Allowed:

  • 'self' - Scripts from extension package
  • 'wasm-unsafe-eval' - WebAssembly support
  • http://localhost:<port> - Development only (remove before submission)

Custom CSP Example

{
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
    "sandbox": "sandbox allow-scripts allow-forms allow-popups; script-src 'self';"
  }
}

Security Best Practices

  1. Package all dependencies locally - No CDN scripts
  2. Request minimal permissions - Use activeTab instead of <all_urls> when possible
  3. Use optional_permissions - Request permissions at runtime for non-critical features
  4. Validate user input - Sanitize HTML before innerHTML, validate URLs
  5. Use HTTPS - All external requests should use HTTPS
  6. No obfuscated code - Must be reviewable for AMO

web-ext CLI Reference

Installation

npm install -g web-ext

Commands

Run extension (development):

web-ext run                                    # Default Firefox
web-ext run --firefox-path /path/to/firefox    # Specific Firefox
web-ext run --target firefox-android           # Android
web-ext run --profile-create-new               # Clean profile
web-ext run --start-url https://example.com    # Start URL
web-ext run --verbose                          # Verbose output
web-ext run --no-reload                        # Disable auto-reload

Lint extension:

web-ext lint                   # Validate manifest and code
web-ext lint --warnings-as-errors
web-ext lint --self-hosted     # Skip AMO-specific checks

Build extension:

web-ext build                  # Create .zip in web-ext-artifacts/
web-ext build --source-dir ./src
web-ext build --artifacts-dir ./dist

Sign extension:

web-ext sign --api-key $KEY --api-secret $SECRET
web-ext sign --channel listed      # Public on AMO
web-ext sign --channel unlisted    # Direct download

Other commands:

web-ext docs       # Open documentation
web-ext dump-config  # Show configuration

Configuration File (.web-ext-config.js)

module.exports = {
  sourceDir: './src',
  artifactsDir: './dist',

  run: {
    firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
    startUrl: 'https://example.com',
    pref: ['extensions.webextensions.debug=true']
  },

  build: {
    overwriteDest: true
  },

  sign: {
    apiKey: process.env.AMO_API_KEY,
    apiSecret: process.env.AMO_API_SECRET,
    channel: 'listed'
  }
};

Environment Variables

WEB_EXT_API_KEY=your_key
WEB_EXT_API_SECRET=your_secret
WEB_EXT_SOURCE_DIR=./src
WEB_EXT_ARTIFACTS_DIR=./dist
WEB_EXT_FIREFOX=/path/to/firefox

AMO Submission Process

Pre-Submission Checklist

  • [ ] Extension ID in browser_specific_settings.gecko.id
  • [ ] web-ext lint passes with no errors
  • [ ] All permissions are necessary and documented
  • [ ] Privacy policy included (if collecting data)
  • [ ] No obfuscated code
  • [ ] Source code available (if using build tools)
  • [ ] Icons: 48x48 and 96x96 minimum
  • [ ] Screenshots: minimum 464x200px
  • [ ] Description is clear and accurate

Submission Steps

  1. Build extension:

    web-ext build
    
  2. Create developer account:

    • Visit https://addons.mozilla.org/developers/
    • Sign up and complete profile
  3. Submit:

    • Go to Developer Hub → "Submit a New Add-on"
    • Upload .zip from web-ext build
    • Choose distribution: Listed (public) or Unlisted (direct)
  4. Fill listing:

    • Name, description, categories
    • Screenshots, icons
    • Privacy policy URL (if collecting data)
    • Support email/URL
  5. Review process:

    • Automated validation: 5-15 minutes
    • Human review: 1-7 days for listed extensions
    • Respond to reviewer comments promptly

Common Rejection Reasons

| Reason | Solution | |--------|----------| | Remote code execution | Package all scripts locally | | Unnecessary permissions | Remove unused permissions | | Obfuscated code | Provide source code | | Missing privacy policy | Add policy if collecting data | | Unclear functionality | Improve description | | CSP violations | Fix CSP to not allow remote scripts | | Hidden functionality | Disclose all features |

Data Collection Requirements

If extension collects/transmits user data, privacy policy must disclose:

  • What data is collected
  • How it's transmitted
  • Purpose of collection
  • User consent mechanism
  • Data retention policy

Testing & Debugging

Development Workflow

# Start development
web-ext run --verbose

# Auto-reloads on file changes
# Access via about:debugging

Debugging Tools

Access debugging:

  1. Open about:debugging#/runtime/this-firefox
  2. Click "Inspect" next to extension

Debug components:

  • Background: Console in debugging page
  • Popup: Right-click popup → "Inspect"
  • Content scripts: Regular page DevTools Console
  • Options page: Right-click options → "Inspect"

Debugging Commands

// Check manifest
browser.runtime.getManifest();

// Check permissions
browser.permissions.contains({ permissions: ['tabs'] });

// Get extension URL
browser.runtime.getURL('/path/to/resource');

// Check last error
if (browser.runtime.lastError) {
  console.error(browser.runtime.lastError);
}

// Reload extension programmatically
browser.runtime.reload();

Testing Strategies

Unit Testing (Jest):

import { mockBrowser } from 'webextension-pockito';

describe('Extension', () => {
  beforeEach(() => mockBrowser.reset());

  test('storage', async () => {
    await browser.storage.local.set({ key: 'value' });
    const result = await browser.storage.local.get('key');
    expect(result.key).toBe('value');
  });
});

Integration Testing (Selenium):

const { Builder } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');

const driver = await new Builder()
  .forBrowser('firefox')
  .setFirefoxOptions(new firefox.Options()
    .setPreference('extensions.autoDisableScopes', 0))
  .build();

Best Practices

Background Service Worker (MV3)

// Keep service worker alive briefly
let keepAlive;

browser.runtime.onMessage.addListener((msg) => {
  clearTimeout(keepAlive);
  keepAlive = setTimeout(() => {}, 25000);
  return handleMessage(msg);
});

Error Handling

async function safeAsync(fn) {
  try {
    return await fn();
  } catch (error) {
    console.error('Error:', error);
    return { error: error.message };
  }
}

Cross-Browser Compatibility

import browser from 'webextension-polyfill';

// Feature detection
if (browser.sidebarAction) {
  // Firefox-specific
  browser.sidebarAction.open();
}

// Fallback pattern
const action = browser.action || browser.browserAction;
action.setPopup({ popup: 'popup.html' });

Performance

  • Use browser.tabs.query() with specific filters
  • Debounce frequent operations
  • Cache storage API results
  • Use alarms API instead of setInterval in background

Troubleshooting

Common Issues

| Error | Cause | Solution | |-------|-------|----------| | browser is not defined | Script not in extension context | Check manifest script paths | | Permission denied | Missing permission | Add to manifest permissions | | CSP violation | Remote script | Move script to extension package | | Service worker terminated | Long operation | Use alarms API, avoid blocking | | webRequest not blocking | MV3 limitation | Use declarativeNetRequest | | ID mismatch | Different IDs | Set consistent ID in manifest |

Debug Commands

# Validate manifest
web-ext lint --verbose

# Run with debug prefs
web-ext run --pref extensions.webextensions.debug=true

# Test in clean profile
web-ext run --profile-create-new

# Check specific Firefox
web-ext run --firefox /path/to/firefox-beta

Cross-Browser with webextension-polyfill

Installation

npm install webextension-polyfill

Usage

// ES modules
import browser from 'webextension-polyfill';

// CommonJS
const browser = require('webextension-polyfill');

// Now browser.* works in Chrome with promises
const tabs = await browser.tabs.query({ active: true });

Manifest Setup

{
  "background": {
    "scripts": ["browser-polyfill.js", "background.js"]
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["browser-polyfill.js", "content.js"]
  }]
}

TypeScript Support

npm install @types/webextension-polyfill
import browser from 'webextension-polyfill';

async function getActiveTab(): Promise<browser.Tabs.Tab> {
  const [tab] = await browser.tabs.query({ active: true });
  return tab;
}

File Structure Template

my-extension/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.js
├── options.html
├── options.js
├── browser-polyfill.js
├── styles/
│   ├── popup.css
│   └── content.css
├── icons/
│   ├── icon-16.png
│   ├── icon-32.png
│   ├── icon-48.png
│   └── icon-96.png
├── _locales/
│   ├── en/
│   │   └── messages.json
│   └── it/
│       └── messages.json
├── .web-ext-config.js
├── package.json
└── README.md

References

Firefox WebExtension Development Skill | Agent Skills