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.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add aibot88/sec_skill_store --skill firefox-extension --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Firefox Extension?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/aibot88-firefox-extension)More formats (shields.io, HTML) on the badges page.
---
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.
metadata:
author: mte90
version: 1.0.0
tags:
- firefox
- webextension
- browser-extension
- mozilla
- amo
- manifest-v3
---
# 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+)
```json
{
"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)
```json
{
"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:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
// Show notification
browser.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: 'Title',
message: 'Message'
});
```
### Firefox-Specific APIs
**Sidebar Action:**
```javascript
// Toggle sidebar
browser.sidebarAction.open();
browser.sidebarAction.close();
// Set panel
await browser.sidebarAction.setPanel({ panel: 'sidebar.html' });
```
**Container Tabs (Contextual Identities):**
```javascript
// 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:**
```javascript
// 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
```json
{
"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
```bash
npm install -g web-ext
```
### Commands
**Run extension (development):**
```bash
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:**
```bash
web-ext lint # Validate manifest and code
web-ext lint --warnings-as-errors
web-ext lint --self-hosted # Skip AMO-specific checks
```
**Build extension:**
```bash
web-ext build # Create .zip in web-ext-artifacts/
web-ext build --source-dir ./src
web-ext build --artifacts-dir ./dist
```
**Sign extension:**
```bash
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:**
```bash
web-ext docs # Open documentation
web-ext dump-config # Show configuration
```
### Configuration File (.web-ext-config.js)
```javascript
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
```bash
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:**
```bash
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
```bash
# 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
```javascript
// 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):**
```javascript
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):**
```javascript
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)
```javascript
// Keep service worker alive briefly
let keepAlive;
browser.runtime.onMessage.addListener((msg) => {
clearTimeout(keepAlive);
keepAlive = setTimeout(() => {}, 25000);
return handleMessage(msg);
});
```
### Error Handling
```javascript
async function safeAsync(fn) {
try {
return await fn();
} catch (error) {
console.error('Error:', error);
return { error: error.message };
}
}
```
### Cross-Browser Compatibility
```javascript
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
```bash
# 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
```bash
npm install webextension-polyfill
```
### Usage
```javascript
// 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
```json
{
"background": {
"scripts": ["browser-polyfill.js", "background.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["browser-polyfill.js", "content.js"]
}]
}
```
### TypeScript Support
```bash
npm install @types/webextension-polyfill
```
```typescript
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
- [MDN WebExtensions](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)
- [Extension Workshop](https://extensionworkshop.com/)
- [Manifest V3 Migration](https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/)
- [web-ext CLI Reference](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/)
- [AMO Developer Hub](https://addons.mozilla.org/developers/)
- [webextension-polyfill](https://github.com/mozilla/webextension-polyfill)
- [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!