Comprehensive debugging guide for Chrome Extensions covering DevTools usage, service worker inspection, content script debugging, storage inspection, network analysis, performance profiling, and common error solutions. Use when troubleshooting extension issues.
Provides Chrome Extension debugging techniques for service workers, content scripts, message passing, storage, and network analysis. Use when troubleshooting extension errors, inspecting contexts, or diagnosing runtime issues.
/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.
chrome://extensions| Component | How to Access |
|---|---|
| Service Worker | chrome://extensions → Click "Service Worker" link |
| Popup | Right-click popup → "Inspect" |
| Side Panel | Right-click side panel → "Inspect" |
| Options Page | Right-click page → "Inspect" |
| Content Script | Page DevTools → Console → Select extension context |
chrome://extensions// Add lifecycle logging
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
});
self.addEventListener('activate', (event) => {
console.log('[SW] Activated');
});
// Log when service worker starts
console.log('[SW] Service worker loaded at', new Date().toISOString());
// Monitor fetch events
self.addEventListener('fetch', (event) => {
console.log('[SW] Fetch:', event.request.url);
});
Issue: Service Worker Not Loading
// Check manifest.json
{
"background": {
"service_worker": "background.js", // Verify path
"type": "module" // Add if using ES imports
}
}
// Check for syntax errors
// Open chrome://extensions and look for error badge
Issue: Service Worker Terminating Early
// Keep alive with alarms (not recommended for all cases)
chrome.alarms.create('keepAlive', { periodInMinutes: 1 });
// Better: Design for termination
// - Store state in chrome.storage
// - Re-register listeners at top level
// - Don't rely on global variables
Issue: Events Not Firing
// WRONG - Listener added async
setTimeout(() => {
chrome.runtime.onMessage.addListener(handler);
}, 1000);
// CORRECT - Top-level registration
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Handle asynchronously inside
handleAsync(message).then(sendResponse);
return true;
});
// Verify injection
console.log('[Content] Script loaded on:', window.location.href);
console.log('[Content] Document state:', document.readyState);
// Check for multiple injections
if (window.__myExtensionLoaded) {
console.warn('[Content] Already loaded!');
} else {
window.__myExtensionLoaded = true;
// Initialize
}
// Debug DOM queries
const element = document.querySelector('#target');
console.log('[Content] Target element:', element);
if (!element) {
console.log('[Content] Available elements:', document.body.innerHTML.slice(0, 500));
}
// Check manifest matches
{
"content_scripts": [{
"matches": ["*://*.example.com/*"], // Check pattern
"js": ["content.js"],
"run_at": "document_idle"
}]
}
// Verify URL matches pattern
// chrome://extensions - Content scripts won't run on chrome:// URLs
// Programmatic injection alternative
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
}).catch(error => {
console.error('Injection failed:', error);
// Common errors:
// - "Cannot access chrome:// URLs"
// - "Missing host_permissions"
});
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[BG] Message received:', {
message,
from: sender.tab ? `Tab ${sender.tab.id}` : 'Extension',
url: sender.url,
frameId: sender.frameId
});
// Your handling logic
return true;
});
// content.js
const originalSendMessage = chrome.runtime.sendMessage;
chrome.runtime.sendMessage = function(message, ...args) {
console.log('[CS] Sending message:', message);
return originalSendMessage.call(this, message, ...args);
};
"Receiving end does not exist"
// The target doesn't have a listener
// Solutions:
// 1. Content script not injected
// 2. Tab navigated away
// 3. Extension context invalidated
async function safeSend(tabId, message) {
try {
return await chrome.tabs.sendMessage(tabId, message);
} catch (error) {
if (error.message.includes('Receiving end does not exist')) {
// Inject content script first
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
return await chrome.tabs.sendMessage(tabId, message);
}
throw error;
}
}
Async Response Not Working
// WRONG - Missing return true
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetchData().then(sendResponse);
// Missing return true!
});
// CORRECT
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetchData().then(sendResponse);
return true; // CRITICAL for async
});
// Dump all storage contents
async function debugStorage() {
const local = await chrome.storage.local.get(null);
const sync = await chrome.storage.sync.get(null);
const session = await chrome.storage.session.get(null);
console.group('Storage Contents');
console.log('Local:', local);
console.log('Sync:', sync);
console.log('Session:', session);
console.groupEnd();
// Check quotas
const localBytes = await chrome.storage.local.getBytesInUse(null);
const syncBytes = await chrome.storage.sync.getBytesInUse(null);
console.group('Storage Usage');
console.log(`Local: ${localBytes} / 10,485,760 bytes`);
console.log(`Sync: ${syncBytes} / 102,400 bytes`);
console.groupEnd();
}
// Call from DevTools console
debugStorage();
// Add to background.js for debugging
chrome.storage.onChanged.addListener((changes, areaName) => {
console.group(`Storage changed [${areaName}]`);
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key}:`, oldValue, '→', newValue);
}
console.groupEnd();
});
// Wrap fetch for debugging
const originalFetch = fetch;
window.fetch = async function(...args) {
console.log('[Fetch]', args[0], args[1]);
try {
const response = await originalFetch.apply(this, args);
console.log('[Fetch Response]', response.status, response.url);
return response;
} catch (error) {
console.error('[Fetch Error]', error);
throw error;
}
};
// Enable rule matching display
chrome.declarativeNetRequest.setExtensionActionOptions({
displayActionCountAsBadgeText: true
});
// Get matched rules for a tab
const rules = await chrome.declarativeNetRequest.getMatchedRules({
tabId: tab.id
});
console.log('Matched rules:', rules);
// Test a URL against rules
const result = await chrome.declarativeNetRequest.testMatchOutcome({
url: 'https://example.com/script.js',
type: 'script',
initiator: 'https://example.com'
});
console.log('Would match:', result);
// Measure operation time
console.time('operation');
await someOperation();
console.timeEnd('operation');
// Performance marks
performance.mark('start');
await someOperation();
performance.mark('end');
performance.measure('operation', 'start', 'end');
console.log(performance.getEntriesByName('operation'));
// Force garbage collection (DevTools must be open)
// Click the trash can icon or use:
// Settings → Experiments → Enable "Timeline: show runtime call stats"
Cause: Extension was reloaded/updated while script was running
Solution:
// Check before Chrome API calls
function isContextValid() {
try {
chrome.runtime.id;
return true;
} catch {
return false;
}
}
// Notify user
if (!isContextValid()) {
showNotification('Extension updated. Please refresh the page.');
}
Cause: API doesn't exist or lacks permission
// Safe API access
if (chrome.sidePanel) {
chrome.sidePanel.open({ tabId });
} else {
console.warn('sidePanel API not available');
}
// Check manifest permissions
console.log('Permissions:', chrome.runtime.getManifest().permissions);
Cause: Unhandled promise rejection
// Always add catch
chrome.storage.local.get('key')
.then(handleResult)
.catch(handleError);
// Or use try-catch with async/await
try {
const result = await chrome.storage.local.get('key');
} catch (error) {
console.error('Storage error:', error);
}
// Callback style - check lastError
chrome.tabs.sendMessage(tabId, message, (response) => {
if (chrome.runtime.lastError) {
console.error('Error:', chrome.runtime.lastError.message);
return;
}
// Process response
});
// Promise style - use try-catch
try {
const response = await chrome.tabs.sendMessage(tabId, message);
} catch (error) {
console.error('Error:', error.message);
}
chrome://extensions for error badgereturn true for async responseschrome.runtime.lastError// Development helper in background.js
if (process.env.NODE_ENV === 'development') {
// Watch for file changes
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
if (event.data === 'reload') {
chrome.runtime.reload();
}
};
// Or simpler: manual reload
console.log('Press F5 in chrome://extensions to reload');
}
// watch.js (Node.js)
const WebSocket = require('ws');
const chokidar = require('chokidar');
const wss = new WebSocket.Server({ port: 8080 });
chokidar.watch('./src').on('change', () => {
wss.clients.forEach(client => {
client.send('reload');
});
});
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 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 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.