Building Browser Extensions with Claude
AI generated
Claude
>_
Claude AI · Browser Extensions · Manifest V3
Building Browser Extensions with Claude
Thinking through Manifest V3 structure, the permission model, and store review requirements from the start

Since the mandatory switch to Manifest V3, the architecture of browser extensions has changed fundamentally: persistent background pages are a thing of the past, replaced by a service worker that can be terminated and restarted at any time. Anyone building their own extension needs to think through this lifecycle model from the beginning rather than patching it in later. This article shows how Claude helps with manifest structure, permissions, communication between components, and preparing for store reviews.

13 min read Manifest V3 Browser Extensions Chrome Web Store Firefox Add-ons

1. Why Manifest V3 fundamentally changes the architecture

Under Manifest V2, background scripts ran in a permanently open, long lived background page, where global state could simply be kept in ordinary JavaScript variables as long as the browser stayed open. Manifest V3 replaces this model with a service worker that, for security and resource reasons, the browser can terminate at any time after a few seconds of inactivity, restarting it on the next relevant action, but without any previously held in memory state.

This shift isn't a purely technical detail, it requires a fundamentally different way of thinking about architecture: any state meant to survive beyond a single event handler must be explicitly persisted to chrome.storage instead of living in a variable that silently disappears the moment the service worker gets terminated. Claude works well for systematically scanning existing Manifest V2 code for exactly these spots where a permanently running background process is implicitly assumed.

2. Thinking through manifest structure and the permission model

The manifest.json doesn't just define technical entry points, it also tells the user, at the install dialog, which permissions the extension requests, which directly affects willingness to install. An extension that broadly requests access to all websites via <all_urls> looks considerably more suspicious to many users and to store reviewers than one that deliberately lists only the actually needed domains via host_permissions.

Claude works well for deriving the minimal required permissions from the planned functionality, instead of requesting broad permissions from the start out of convenience. For optional features in particular, it's worth looking at optional_permissions, which only get requested at runtime when actually used, rather than already at install time, which both increases user acceptance and lowers scrutiny risk during store review.


{
  "manifest_version": 3,
  "name": "Link Preview Helper",
  "version": "1.0.0",
  "permissions": ["storage", "activeTab"],
  "optional_permissions": ["tabs"],
  "host_permissions": [
    "https://api.example.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"]
    }
  ],
  "action": {
    "default_popup": "popup.html"
  }
}

3. Content script vs. background service worker: separating responsibilities

Content scripts run in the context of the visited web page and have direct access to its DOM, but are subject to the same content security policy restrictions as the page itself and can't directly use certain browser wide APIs, say for cross origin requests. The service worker, by contrast, runs isolated from any individual web page, has access to the full extension API, but can't directly touch a page's DOM.

This separation forces a clear architectural decision about where logic belongs: DOM manipulation and reading page content belong in the content script, while API calls to external services, managing persistent state, and coordinating across multiple tabs should happen in the service worker. Claude works well for splitting planned functionality along this boundary and checking whether a planned operation is even technically possible in its intended context.

4. Designing communication between content script and service worker

Since content script and service worker run in separate processes, they communicate exclusively through asynchronous messages rather than direct function calls, which demands a different mental model than classic synchronous calls within the same execution environment. The chrome.runtime.sendMessage API offers no guarantee that a message actually gets delivered, say if the service worker happened to be terminated at the exact moment the message arrives.

Claude can help design a robust messaging protocol that plans for explicit response acknowledgments and timeouts, instead of silently assuming reliable delivery. For cases where messages get exchanged repeatedly over a longer period, say a live update while scrolling a page, it's worth switching to a Port based, long lived connection channel instead of individual one off messages.


// content.js: sending a message to the service worker, with error handling
async function reportPageData(data) {
  try {
    const response = await chrome.runtime.sendMessage({
      type: 'PAGE_DATA_COLLECTED',
      payload: data,
    });
    if (!response?.ok) {
      console.warn('Service worker did not acknowledge the message');
    }
  } catch (err) {
    // Service worker was inactive or just got terminated
    console.error('Message could not be delivered', err);
  }
}

// background.js: reacting to messages and explicitly responding
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'PAGE_DATA_COLLECTED') {
    persistPageData(message.payload).then(() => sendResponse({ ok: true }));
    return true; // keeps the channel open for the async response
  }
});

5. Storage and state: chrome.storage instead of global variables

Since the service worker can be terminated at any time, every global variable meant to hold state across multiple events is a latent bug. The chrome.storage API provides the intended, persistent replacement, where chrome.storage.local suits larger, device bound data, while chrome.storage.sync automatically synchronizes smaller settings across a signed in user's devices, but is subject to considerably tighter storage quotas.

Claude can be asked deliberately to check existing code for implicit assumptions about durable in memory state, say a counter incremented on every page visit that silently resets to zero every time the service worker terminates without persistence. Such bugs often go unnoticed during local testing, because the service worker rarely stays inactive long enough to get terminated during active development, but they show up reliably in real day to day use.

6. Keeping permissions minimal under the least privilege principle

Every requested permission increases not just user skepticism at the install dialog but also the depth of scrutiny during the store review process, because certain permissions classified as sensitive automatically trigger a manual, often multi week review instead of a faster, largely automated approval. Permissions like webRequest with blocking capabilities or broad host permissions across all websites belong to the most critically scrutinized categories.

Claude works well for checking, for every API function actually used in the code, what minimal permission is really required for it, and for surfacing cases where a permission declared in the manifest is no longer used in the code at all, a common leftover from earlier development phases that unnecessarily raises suspicion at every store review.

7. Meeting Chrome Web Store review requirements

The Chrome Web Store requires, among other things, a clear privacy policy that precisely describes what user data gets collected and what it's used for, as well as compliance with the single purpose policy, under which an extension must fulfill a clearly scoped, description traceable main purpose instead of bundling a confusing collection of unrelated features. Violations of this policy are among the most common rejection reasons in the review process.

Claude works well for cross checking your own store description and privacy policy against the actual feature set and the actually requested permissions, because discrepancies between claimed and actual behavior reliably surface in the review's automated code scan and lead to rejections that restart the entire review cycle, which can take anywhere from several days to weeks.

8. Firefox add-on specifics: WebExtensions differences

Firefox supports the same WebExtensions standard as Chrome, but implements individual APIs with differing behavior, say promise based APIs via the browser namespace instead of the callback based chrome namespace, along with sometimes different Manifest V3 support for certain service worker features. An extension developed and tested exclusively against the Chrome API therefore doesn't automatically run flawlessly in Firefox.

Claude can help introduce a suitable compatibility polyfill and deliberately identify code spots relying on Chrome specific behavior, say certain timing assumptions about service worker termination that differ between browsers. For an extension meant to be published to both stores, it's worth building a shared, browser neutral abstraction layer early in the design instead of two completely separate codebases.


// Browser neutral abstraction instead of direct chrome.* calls
const api = typeof browser !== 'undefined' ? browser : chrome;

async function getStoredSettings() {
  // Promise based in Firefox, and also promise based in Chrome via a polyfill
  return api.storage.local.get('settings');
}

9. Manifest V3 components at a glance

The following table summarizes the central components of a Manifest V3 extension along with their respective lifetime and typical pitfalls.

Component Responsibility Lifetime Typical pitfall
Background service worker Central logic, API calls, coordination Short lived, terminable at any time State kept in global variables instead of chrome.storage
Content script DOM access on the visited page Lives with the given tab Subject to the visited page's CSP
Popup UI shown on clicking the extension icon Only while the popup is open State gets lost when closed
Options page Extension configuration interface Only while the page is open Settings don't sync across devices without chrome.storage.sync
DevTools page Extends the browser developer tools Only while DevTools is open Restricted API access compared to the service worker

Mironsoft

AI-assisted development, agent workflows, and team processes

Using Claude or other AI tools on the team, but without a clear workflow?

We set up AI-assisted development workflows for teams, from CLAUDE.md conventions to subagent strategies to code review processes that combine human oversight with AI speed.

Workflow Setup

Cleanly set up CLAUDE.md, project conventions, and tool permissions for the team.

Agent Strategy

Build subagent and automation workflows for recurring development tasks.

Team Onboarding

Train developers in productive, safe use of AI coding assistants.

10. Summary

Building Browser Extensions with Claude: The Essentials at a Glance

Core idea

Manifest V3 replaces persistent background pages with a service worker terminable at any time, requiring state to be explicitly persisted.

Key principle

Keep permissions minimal, since every extra permission raises both user skepticism and review scrutiny.

Biggest architectural cut

Clear separation between content script for DOM access and service worker for API logic and coordination.

Store rule

Description, privacy policy, and requested permissions must exactly match actual behavior.

11. FAQ: Building Browser Extensions with Claude: The Essentials at a Glance

1Why does Manifest V3 fundamentally change extension architecture?
Because the service worker can be terminated at any time and loses all in memory state when it happens.
2Where should persistent state live in a Manifest V3 extension?
In chrome.storage.local or chrome.storage.sync, not in global JavaScript variables.
3What technically distinguishes a content script from a service worker?
Content scripts have DOM access but follow the page's CSP, the service worker has the full extension API but no DOM access.
4Why isn't chrome.runtime.sendMessage always reliable?
Because the service worker can be terminated at the exact moment a message arrives.
5What does the Chrome Web Store's single purpose policy require?
An extension must fulfill a clearly scoped main purpose that's traceable in its description.
6Why should permissions be kept minimal?
Because they raise both user skepticism and the depth of scrutiny during the store review process.
7What are optional_permissions in the manifest?
Permissions only requested at runtime when actually used, rather than already at install time.
8How do Chrome and Firefox extension APIs differ?
Firefox uses a promise based browser namespace, Chrome traditionally a callback based chrome namespace.
9What's the most common reason for Chrome Web Store rejections?
Violations of the single purpose policy along with discrepancies between the privacy policy and actual behavior.
10When is a Port based connection channel worth it over individual messages?
When messages get exchanged repeatedly over a longer period, say for live updates while scrolling.