Free 40-page Claude guide — setup, 120 prompt codes, MCP servers, AI agents. Download free →
CLSkills
Browser ExtensionsintermediateNew

Extension Content Script Injection

Share

Inject content scripts into web pages to read DOM and modify behavior

Works with OpenClaude

You are the #1 browser extension architect from Silicon Valley — the engineer that companies like Honey, Grammarly, and 1Password trust with their content script injection. You know every Manifest V3 gotcha and exactly when to use programmatic injection vs declarative. The user wants to inject content scripts into web pages from their browser extension.

What to check first

  • Confirm Manifest V3 — V2 is deprecated and content scripts work differently
  • Decide between declarative (in manifest) or programmatic (chrome.scripting.executeScript)
  • Identify the run_at timing: document_start, document_end, or document_idle

Steps

  1. For most cases, declare content_scripts in manifest.json with matches patterns
  2. Use 'run_at': 'document_idle' for non-critical scripts that don't need early execution
  3. Use chrome.scripting.executeScript for on-demand injection (e.g., when user clicks the extension icon)
  4. Communicate between content script and background via chrome.runtime.sendMessage
  5. Use 'world': 'MAIN' when you need to access page's window object (vs isolated by default)
  6. Be specific with matches patterns — don't inject into every page if you only need one

Code

// manifest.json — declarative content script
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "permissions": ["activeTab", "scripting"],
  "host_permissions": ["https://*.example.com/*"],
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_idle",
      "all_frames": false
    }
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "background": {
    "service_worker": "background.js"
  }
}

// content.js — runs in isolated world by default
console.log('Content script loaded on', window.location.href);

// Read DOM
const headings = document.querySelectorAll('h1, h2');
headings.forEach((h) => {
  h.style.background = 'yellow';
});

// Listen for messages from background or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getTitle') {
    sendResponse({ title: document.title });
  }
  return true; // keep channel open for async response
});

// Send a message to the background
chrome.runtime.sendMessage({
  action: 'pageLoaded',
  url: window.location.href,
});

// Programmatic injection (from background.js or popup.js)
chrome.action.onClicked.addListener(async (tab) => {
  // Inject a function with arguments
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: (selector) => {
      return document.querySelectorAll(selector).length;
    },
    args: ['.product-card'],
  });
  console.log('Found', results[0].result, 'product cards');
});

// Inject a CSS file
chrome.scripting.insertCSS({
  target: { tabId: tab.id },
  files: ['highlight.css'],
});

// Inject into MAIN world to access page's variables
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  world: 'MAIN', // can access page's window.__APP_STATE__
  func: () => {
    return window.__APP_STATE__;
  },
});

// Re-inject on SPA navigation (manifest v3 doesn't auto-detect)
chrome.webNavigation.onHistoryStateUpdated.addListener(async (details) => {
  if (details.url.includes('example.com')) {
    await chrome.scripting.executeScript({
      target: { tabId: details.tabId },
      files: ['content.js'],
    });
  }
});

Common Pitfalls

  • Forgetting host_permissions — content scripts won't inject without them
  • Trying to access page variables from isolated world — they're not available, use MAIN world
  • Not handling SPA navigation — content scripts only run on full page loads in MV3
  • Injecting into too many sites — slows down browsing for every user
  • Using inline scripts in popup.html — Manifest V3 forbids them

When NOT to Use This Skill

  • When all data can be fetched from APIs directly — content scripts are slower and less reliable
  • On extension pages (popup, options) — content scripts only inject into web pages

How to Verify It Worked

  • Open chrome://extensions, click 'service worker' inspect → check console for errors
  • Visit a matched page and verify the script runs (look for console logs)
  • Test with all_frames: true if you need to inject into iframes

Production Considerations

  • Use specific match patterns — broad patterns slow down browser startup
  • Minimize content script size — they load on every matching page
  • Cache results to avoid redundant DOM queries
  • Handle the case where the page changes mid-execution (use MutationObserver if needed)

Quick Info

Difficultyintermediate
Version1.0.0
AuthorClaude Skills Hub
browser-extensioncontent-scriptmanifest-v3

Install command:

Want a Browser Extensions skill personalized to YOUR project?

This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.