Manifest V2 to V3 migration guide covering background page to service worker, API changes, webRequest to declarativeNetRequest, remote code removal, executeScript changes, and persistence patterns. Use when upgrading extensions to Manifest V3.
Provides a comprehensive migration guide for upgrading Chrome extensions from Manifest V2 to V3. Use when developers need to convert background pages to service workers, replace webRequest with declarativeNetRequest, remove remote code, or handle API changes like executeScript and action APIs.
/plugin marketplace add francanete/fran-marketplace/plugin install chrome-extension-expert@fran-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
| Feature | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | Persistent page | Service worker |
| Remote code | Allowed | Forbidden |
| Web requests | webRequest API | declarativeNetRequest |
| Host permissions | In permissions | Separate field |
| Content scripts | executeScript string | executeScript files/func |
| CSP | Customizable | Restricted |
| Action | browser_action/page_action | action |
MV2:
{
"manifest_version": 2,
"name": "My Extension",
"version": "1.0",
"browser_action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"permissions": [
"tabs",
"storage",
"*://*.example.com/*"
]
}
MV3:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"tabs",
"storage"
],
"host_permissions": [
"*://*.example.com/*"
]
}
browser_action / page_action → actionbackground.scripts → background.service_workerhost_permissions"type": "module" for ES modules| Background Page | Service Worker |
|---|---|
| Persistent (optional) | Always terminates |
| DOM access | No DOM |
| window object | No window |
| localStorage | No localStorage |
| XMLHttpRequest | fetch only |
| setTimeout reliable | setTimeout may not fire |
1. Remove DOM dependencies:
// MV2 - Using DOM
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// MV3 - Use offscreen document
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
// Send HTML to offscreen document for parsing
2. Replace window with self:
// MV2
window.addEventListener('message', handler);
// MV3
self.addEventListener('message', handler);
3. Handle termination:
// MV2 - Persistent state
let cachedData = null;
// MV3 - Must persist to storage
async function getCachedData() {
const { cachedData } = await chrome.storage.session.get('cachedData');
return cachedData;
}
async function setCachedData(data) {
await chrome.storage.session.set({ cachedData: data });
}
4. Register listeners at top level:
// MV2 - Could add listeners anytime
setTimeout(() => {
chrome.runtime.onMessage.addListener(handler);
}, 1000);
// MV3 - Must be at top level
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Can still handle asynchronously
handleMessage(message).then(sendResponse);
return true;
});
// MV2 - setInterval
setInterval(checkForUpdates, 60000);
// MV3 - Alarms
chrome.alarms.create('checkUpdates', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'checkUpdates') {
checkForUpdates();
}
});
// Service worker can terminate anytime
// Must save state to storage
// On state change
async function updateState(newState) {
state = { ...state, ...newState };
await chrome.storage.session.set({ state });
}
// On service worker start
async function restoreState() {
const { state } = await chrome.storage.session.get('state');
return state || initialState;
}
// Restore state immediately on load
let state;
restoreState().then(s => state = s);
// For long-running operations
// Create an alarm to keep service worker alive
async function startLongOperation() {
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 });
try {
await longOperation();
} finally {
chrome.alarms.clear('keepAlive');
}
}
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') {
// Just keeps worker alive
}
});
MV2 - webRequest (blocking):
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (shouldBlock(details.url)) {
return { cancel: true };
}
},
{ urls: ['<all_urls>'] },
['blocking']
);
MV3 - declarativeNetRequest:
manifest.json:
{
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
}
}
rules.json:
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||ads.example.com",
"resourceTypes": ["script", "image", "xmlhttprequest"]
}
},
{
"id": 2,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "extensionPath": "/blocked.html" }
},
"condition": {
"urlFilter": "||tracking.com/*",
"resourceTypes": ["main_frame"]
}
}
]
// Add rules at runtime
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 100,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: userBlockedDomain,
resourceTypes: ['main_frame', 'sub_frame']
}
}],
removeRuleIds: [99] // Remove old rule
});
// Session rules (cleared on browser restart)
await chrome.declarativeNetRequest.updateSessionRules({
addRules: [/* ... */]
});
Some use cases still require webRequest (without blocking):
{
"permissions": ["webRequest"],
"host_permissions": ["*://*.example.com/*"]
}
// Non-blocking observation still works
chrome.webRequest.onCompleted.addListener(
(details) => {
logRequest(details);
},
{ urls: ['*://*.example.com/*'] }
);
Not Allowed in MV3:
// Loading external scripts
const script = document.createElement('script');
script.src = 'https://external.com/script.js'; // Blocked
// Eval and Function constructor
eval(code); // Blocked
new Function(code); // Blocked
// External script tags in HTML
<script src="https://cdn.example.com/lib.js"></script> // Blocked
1. Bundle all dependencies:
npm install library
# Bundle with webpack/rollup/esbuild
2. For dynamic configuration:
// MV2 - Fetch and eval config
const config = await fetch('https://api.com/config.js');
eval(config);
// MV3 - Fetch JSON data only
const response = await fetch('https://api.com/config.json');
const config = await response.json();
// Use config data, don't execute code
3. For user scripts (sandbox):
{
"sandbox": {
"pages": ["sandbox.html"]
}
}
// sandbox.html can use eval
// Communicate via postMessage
const frame = document.getElementById('sandbox');
frame.contentWindow.postMessage({ code: userCode }, '*');
// Execute string of code
chrome.tabs.executeScript(tabId, {
code: 'document.body.style.background = "red"'
});
// Execute file
chrome.tabs.executeScript(tabId, {
file: 'content.js'
});
// Execute function
await chrome.scripting.executeScript({
target: { tabId },
func: () => {
document.body.style.background = 'red';
}
});
// Execute function with arguments
await chrome.scripting.executeScript({
target: { tabId },
func: (color) => {
document.body.style.background = color;
},
args: ['red']
});
// Execute file
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
// Specify world
await chrome.scripting.executeScript({
target: { tabId },
world: 'MAIN', // Access page's JavaScript context
func: () => window.somePageVariable
});
// MV2
chrome.tabs.insertCSS(tabId, { code: 'body { color: red; }' });
// MV3
await chrome.scripting.insertCSS({
target: { tabId },
css: 'body { color: red; }'
});
// Or file
await chrome.scripting.insertCSS({
target: { tabId },
files: ['styles.css']
});
// MV2
chrome.browserAction.setIcon({ path: 'icon.png' });
chrome.browserAction.setBadgeText({ text: '5' });
chrome.browserAction.onClicked.addListener(handler);
chrome.pageAction.show(tabId);
// MV3
chrome.action.setIcon({ path: 'icon.png' });
chrome.action.setBadgeText({ text: '5' });
chrome.action.onClicked.addListener(handler);
// No separate pageAction - use action for all
chrome.action.enable(tabId);
chrome.action.disable(tabId);
{
"web_accessible_resources": [
"images/*",
"script.js"
]
}
{
"web_accessible_resources": [{
"resources": ["images/*", "script.js"],
"matches": ["*://*.example.com/*"]
}, {
"resources": ["public/*"],
"matches": ["<all_urls>"],
"use_dynamic_url": true
}]
}
{
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
Restrictions:
unsafe-evalunsafe-inlinewasm-unsafe-eval allowed for WebAssemblymanifest_version to 3browser_action/page_action with actionhost_permissionsbackground to service_workerweb_accessible_resources formatwindow with selfexecuteScript callsinsertCSS callschrome.scripting APIwebRequest blocking with declarativeNetRequestProblem: State lost when worker terminates
Solution:
// Use storage instead of variables
chrome.storage.session.set({ key: value });
// Restore on start
chrome.storage.session.get('key').then(({ key }) => {
// Use restored value
});
Problem: No DOMParser in service worker
Solution: Use offscreen document
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML'
});
Problem: webRequest blocking not available
Solution: Use declarativeNetRequest with static rules
Problem: Can't execute user-provided code
Solution: Sandbox page with postMessage communication
Problem: Can't load from CDN
Solution: Bundle with npm/webpack
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.