Build a Chrome Extension

Beginner-Friendly Teaching Edition  ·  Hands-on project
What you will learn
How to build a real Chrome browser extension with Claude Code — not a Hello World, but a tool the author uses daily. You will go from requirements analysis to packaging and installation, including authentic debugging and a textbook refactor from 1,211 lines in one file to a clean modular design.
Plan mode: requirementsDefine architectureWrite CLAUDE.mdBuild module by moduleDebug & test

The Big Idea

The author built a Bilibili creator-assistant extension "out of pure laziness" — dozens of comments needed replies every day. The first version ran in about two hours, but all the code was crammed into one 1,211-line content.js where "any change felt like defusing a bomb." A later v3.0 refactor shrank content.js to 175 lines across 7 independent modules.

Easy way to remember:
The goal of this chapter is "maintainable," not just "runnable." Just "runnable" is nowhere near enough.

Why Chrome Extensions Are a Good Project

ReasonWhy it matters
The technical barrier is just rightNo Swift, no backend. HTML + CSS + JavaScript — exactly what Claude Code does best. Claude has a solid grasp of Manifest V3, so it handles Service Workers and permission declarations for you.
There is a real use caseYou use a browser every day. A tool you use daily is far more motivating than a to-do list you open twice.
The feedback loop is instantChange code → open the extensions page → click "Reload" → refresh the page → see the result. No compilation, no deployment, no review wait.

Phase 0 — Requirements Analysis

The early mistake: jumping straight to "build an auto-reply plugin." Claude produced a very basic version (detect a new comment, reply "Thanks for your support") — functional, but not what was wanted.

The fix: enter Plan mode first (Shift+Tab) and talk through requirements:

I want to build a Bilibili creator assistant Chrome extension. Core requirements:
1. Automatically scan video comments
2. Match comments against keyword rules and auto-reply
3. Support both global rules and per-video rules
4. Don't re-reply to comments that have already been replied to
5. A simple management panel to view running status
Help me analyze these requirements first and propose a technical approach.

In Plan mode Claude outputs a full technical proposal — file structure, technology choices, analysis of tricky parts. Do not rush to say "great, let's start." Read it carefully and raise questions.

Lesson: time spent discussing requirements in Plan mode is always less than time spent tearing things apart and rebuilding. Version 1 skipped the discussion: 2 hours to build, but the v3.0 refactor took several days. 30 minutes of upfront architecture thinking would have avoided most of it.

Phase 1 — Project Initialization

Based on the approach we discussed, create the Chrome extension project. Start with the skeleton:
- manifest.json (MV3)
- background.js (Service Worker)
- content.js (Content Script)
- popup.html + popup.js (management panel)
- lib/ directory (business modules)
Only request permissions that are strictly necessary.

Code example: manifest.json (the "soul" of an extension)

{
  "manifest_version": 3,
  "name": "Bilibili Creator Assistant",
  "version": "3.0.0",
  "permissions": ["storage", "alarms", "activeTab"],
  "host_permissions": ["*://*.bilibili.com/*"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [{
    "matches": ["*://*.bilibili.com/video/*"],
    "js": ["content.js"]
  }],
  "action": {
    "default_popup": "popup.html"
  }
}

Explaining the key decisions

DecisionWhy
"manifest_version": 3MV2 is deprecated by Chrome. Claude uses MV3 automatically — but many online tutorials still use MV2, and referencing them can make Claude mix the two APIs. Add "Must use Manifest V3" to CLAUDE.md.
Service Worker, not a Background PageThe biggest MV3 change: the background script is a Service Worker the browser can terminate at any time. You cannot use setInterval for scheduled tasks — use chrome.alarms. Put this in CLAUDE.md or Claude occasionally falls back to the old pattern.
Minimal permissionsThe Chrome Web Store review is more likely to reject extensions with excessive permissions, and users hesitate at a long permission list.

Phase 2 — Core Architecture: Who Owns What

The most common mistake in extension development is putting code in the wrong place. MV3 has three runtime environments with completely different responsibilities:

EnvironmentFileCan doCannot do
Service Workerbackground.jsScheduled tasks, global state, message routingAccess page DOM, read page cookies
Content Scriptcontent.jsManipulate page DOM, read page contextCall chrome.alarms and similar APIs directly
Popup / Optionspopup.jsUser interface, configuration managementRun after the user closes the popup

The v3 architecture

background.js (Command Center)
  ├── Owns the timed scan loop (chrome.alarms, once per minute)
  ├── Routes all messages (popup/content/widget -> centralized handling)
  └── Manages global state (on/off switch, rate limit level, etc.)
       ↕  chrome.runtime.sendMessage
content.js (Front-line Scout)
  ├── Extracts current video info (BV ID, title, etc.)
  ├── Proxies API calls (requires page cookies)
  └── Forwards logs and status to the floating widget
       ↕  Message passing
lib/ (Business Brain)
  ├── db.js         -> Unified data layer, all storage reads/writes go here
  ├── scanner.js    -> Comment scanning engine
  ├── rules.js      -> Rule matching logic
  ├── api.js        -> Bilibili API wrapper
  ├── ai.js         -> AI reply generation
  └── rate-limiter.js -> 4-level rate limiting with graceful degradation

The key design principle: content.js only does what it absolutely must — read page cookies (background.js cannot in MV3) and manipulate page DOM. Everything else lives in background.js and lib/.

Architecture principles:
1. content.js is a thin shell - only extracts video info and proxies API calls
2. background.js is the command center - owns the scan loop and state management
3. All business logic goes in independent modules under lib/
4. All data storage goes through lib/db.js - no direct chrome.storage calls elsewhere
5. Modules communicate via message passing, not shared state
Write these into the project's CLAUDE.md.
Lesson: have Claude write the project's CLAUDE.md and encode the architecture principles there. Every future conversation will respect them — the "give the AI a map" idea from Chapter 5, applied.

Phase 3 — Building Modules One by One

1
Data layer (db.js) — the foundation everything depends on. Interfaces like getSystemState()/setSystemState(), getConfig()/setConfig(), getRules()/setRules(), getVideo(bvid)/setVideo(), markReplied(bvid, rpid). Storage consolidated from 20+ scattered keys into 5 structured keys (sys:state, sys:config, sys:rules, v:{bvid}). All writes are atomic: read → merge → write, to prevent concurrent overwrites.
2
Rule matching (rules.js) — the smallest module, 62 lines. Input: a comment and a set of rules. Output: the match result. A pure function, no side effects, very easy to test.
3
API wrapper (api.js) — wraps Bilibili's comment fetching and reply sending. Because the API needs page-cookie authentication, these functions actually run in the content.js context and return results via message passing.
4
Scanning engine (scanner.js) — wires the previous three together: fetch comments → filter already-replied → match rules → generate reply → send. Uses dependency injection (see code below).
5
Rate limiter (rate-limiter.js) — 4-level graceful degradation (see table below).
6
Background script (background.js) — assembles all modules, implements the scan loop. Woken by chrome.alarms once per minute, scanning in priority order (currently open tabs first).
7
Content Script and UI — built last. content.js exposes a few simple methods to the floating widget; popup.js handles the management panel.

Code example: dependency injection in scanner.js

async function scanOneVideo(bvid, rules, config, {
  sendReplyFn,        // function to send a reply
  fetchCommentsFn,    // function to fetch comments
  logFn               // logging function
}) {
  // business logic
}

Explaining the code

scanner.js runs inside background.js, but API calls must execute in the content.js context. Instead of calling the API directly, scanOneVideo receives functions from outside. background.js passes in a function that "relays the call to content.js via messaging," and scanner.js never needs to know that detail. This keeps the business logic testable and decoupled from the messaging plumbing.

The 4-level rate limiter

LevelReply intervalScan intervalTrigger
normal5 seconds2 secondsDefault
slow15 seconds5 secondsRate-limit warning received
slower30 seconds10 seconds3 consecutive errors
pausedPausedPausedAccount throttled — resumes after 1 hour

A successful reply decrements the error counter, automatically recovering to normal — "like a spring: compress it, and it bounces back."

Phase 4 — Debugging Tips

AreaHow to debug it
Service WorkerOn chrome://extensions, click the "Service Worker" link under your extension to open a dedicated DevTools. console.log from background.js appears here, not in the page console.
Content ScriptSee content.js logs in the webpage's DevTools console. Remember content.js runs in an isolated environment and does not share globals with the page's own JavaScript.
ReloadingAfter changing code: click refresh on chrome://extensions. Popup/options pages: close and reopen. content.js: refresh the target page. Service Worker: click "Update" next to the "Service Worker" link.
Claude Code can help you write debugging utilities. The v3 build has a simple log system that retains the last 200 entries, auto-scrolls to clear old ones, and displays them in the popup panel — invaluable for tracking down problems.

Phase 5 — Installation and Testing

During development you do not publish to the Chrome Web Store. On chrome://extensions, enable "Developer mode," click "Load unpacked," and select your project directory.

A testing checklist (have Claude generate one):

  • Basic functionality: add a rule → scan comments → auto-reply
  • Deduplication: the same comment is never replied to twice
  • Rate limiting: rapid consecutive replies automatically slow down
  • Persistence: close the browser and reopen — state and data are preserved
  • Multiple tabs: open several video pages at once, each running independently

Real-World Story: From 1,211 Lines to 175

Version 1 worked, but content.js was one 1,211-line file with API calls, scanning logic, rule matching, and UI updates all tangled together. MV3 Service Workers can sleep at any time, making setInterval scheduling unreliable. The refactor was not planned — the author wanted to add AI-assisted replies and found it impossible to work with in that file.

The current content.js is 1,211 lines with all logic mixed together. I want to refactor it into:
- background.js as the operational hub
- content.js as a thin shell (only video info extraction and API proxying)
- lib/ directory for all business modules
Don't do this all at once - work in steps. First help me analyze the current code and map out where each part belongs.

Claude produced a migration plan splitting the 1,211 lines into 8 functional chunks, annotating where each belonged. Then they executed step by step — test after each migration, confirm nothing regressed, then move on.

The final result

FileLinesResponsibility
background.js468Message routing + scan orchestration
content.js175Video info extraction + message bridging
lib/db.js410Unified data layer
lib/scanner.js322Comment scanning engine
lib/api.js137Bilibili API wrapper
lib/ai.js127AI reply generation
lib/rate-limiter.js100Rate limiting with graceful degradation
lib/rules.js62Rule matching
lib/migrate.js253v2 → v3 data migration
Remember: total line count actually increased (1,211 → 2,000+), but each file now has a clear, single responsibility that can be understood and modified in isolation. The point of refactoring was never to reduce lines of code — it is to reduce cognitive overhead.
Watch out: the most common refactoring mistake is trying to do it all at once. Do not ask Claude to rewrite everything in one shot — migrate in steps, confirming functionality after each. The author ran a full browser test cycle after every module migration. Slower is fine; stability beats speed.

Real-World Use Cases: Other Extension Ideas

IdeaWhat it exercises
Web annotation tool — highlight selected text, save locallyContent script DOM manipulation + storage persistence
AI translation assistant — select a paragraph, call an AI to translateContent script + external API calls
Social media timer — track time per site, alert when overBackground alarms + multi-site content scripts
GitHub enhancer — show CI status on PR pages, auto-apply labelsGitHub API + content script injection

Whichever you choose, the core workflow is the same: Plan mode to analyze requirements → define architecture (who owns what) → write CLAUDE.md to lock in the principles → implement module by module → debug and test.

Visual Mental Model: The Three MV3 Environments

          ┌─────────────────────────────────────────────┐
          │  POPUP / OPTIONS  (popup.js)                 │
          │  UI + config  ·  dies when popup closes      │
          └───────────────┬─────────────────────────────┘
                          │ messages
   ┌──────────────────────▼──────────────────────┐
   │  SERVICE WORKER  (background.js)             │  <- alarms, global state,
   │  command center  ·  can be killed anytime   │     message routing
   └──────────────────────┬──────────────────────┘
                          │ messages
   ┌──────────────────────▼──────────────────────┐
   │  CONTENT SCRIPT  (content.js)  THIN SHELL    │  <- DOM + page cookies ONLY
   └─────────────────────────────────────────────┘
   lib/*  =  all business logic, called from background.js

Important Comparisons

v1 (runnable)v3 (maintainable)
StructureOne content.js, 1,211 linescontent.js 175 lines + 7–8 focused modules
SchedulingsetInterval (unreliable under MV3)chrome.alarms
Storage20+ scattered keys5 structured keys, all via db.js
Changing a featureHunt through the whole file firstOpen one module, understand it in isolation
Total lines1,2112,000+ (more lines, less cognitive load)
MV2 (old)MV3 (current)
Background Page (persistent)Service Worker (can be terminated anytime)
setInterval for timerschrome.alarms
Still shown in many online tutorialsRequired by Chrome; state "Must use Manifest V3" in CLAUDE.md

Common Beginner Mistakes

  • Jumping to "build the plugin" without a Plan-mode requirements discussion.
  • Putting code in the wrong environment — e.g. business logic in content.js.
  • Using setInterval in an MV3 Service Worker instead of chrome.alarms.
  • Letting Claude reference MV2 tutorials, mixing the two APIs.
  • Requesting more permissions than needed — risks store rejection and scares users.
  • Direct chrome.storage calls scattered everywhere instead of one data layer.
  • Refactoring everything in one shot instead of step-by-step with tests.
  • Chasing fewer lines instead of clearer responsibilities.

Best Practices

  • Start in Plan mode and read the proposal before coding.
  • Decide "who owns what" across the three MV3 environments before writing modules.
  • Encode architecture principles in CLAUDE.md so every session respects them (incl. "Must use Manifest V3", "use chrome.alarms").
  • Request minimal permissions.
  • Build bottom-up: data layer first, UI last.
  • Route all storage through one db.js; make writes atomic (read → merge → write).
  • Use dependency injection to keep business logic testable and decoupled from messaging.
  • Add a logging system for debugging (e.g. last 200 entries in the popup).
  • Refactor in steps, testing after each migration.
  • Optimise for reduced cognitive overhead, not line count.

Interview / Revision Questions

  1. Why is a Chrome extension a good project after the earlier chapters?
  2. What is the instant feedback loop for extension development?
  3. What mistake does jumping straight to "build the plugin" cause, and what is the fix?
  4. What are the three MV3 runtime environments, and what can each one not do?
  5. Why can't you use setInterval in an MV3 Service Worker?
  6. What is the "key design principle" for content.js?
  7. What are the five architecture principles written into CLAUDE.md?
  8. In what order are modules built, and why is db.js first?
  9. Explain dependency injection in scanner.js and why it is used.
  10. Describe the 4-level rate limiter and how it recovers.
  11. Where do background.js logs appear versus content.js logs?
  12. What is the real point of the 1,211 → 175 refactor, given total lines went up?

Practice Exercises

Exercise 1: Pick one extension idea from the list and write a Plan-mode requirements message with 4–5 numbered core requirements.
Exercise 2: Write a manifest.json (MV3) requesting only the permissions your idea truly needs, and justify each one.
Exercise 3: For your idea, fill a table of "who owns what" across Service Worker / Content Script / Popup.
Exercise 4: Write a CLAUDE.md with 5 architecture principles including "Must use Manifest V3."
Exercise 5: Implement a tiny pure-function module (like rules.js) and write two tests for it.
Exercise 6: Take any 300+ line file and have Claude produce a step-by-step migration plan (do not execute) into 3–4 modules.

Quick Memory Map

Build a Chrome Extension
│
├── Goal: MAINTAINABLE, not just runnable
│
├── Workflow
│   Plan mode (requirements) -> architecture (who owns what)
│   -> CLAUDE.md (lock principles) -> module by module -> debug + test
│
├── MV3 environments
│   ├── Service Worker (background.js): alarms, state, routing; NO DOM/cookies; can be killed
│   ├── Content Script (content.js): DOM + page cookies ONLY (thin shell)
│   └── Popup/Options (popup.js): UI + config; dies on close
│   -> setInterval OUT, chrome.alarms IN
│
├── Build order: db.js -> rules.js -> api.js -> scanner.js
│                 -> rate-limiter.js -> background.js -> content.js + UI
│   ├── all storage via db.js, atomic read->merge->write
│   └── scanner.js uses dependency injection (sendReplyFn, fetchCommentsFn, logFn)
│
├── Rate limiter: normal -> slow -> slower -> paused (recovers on success)
│
├── Debug: SW logs in SW DevTools; CS logs in page DevTools; reload per environment
│
└── Refactor story: 1,211-line content.js -> 175 lines + modules
    (more total lines, LESS cognitive overhead; migrate in tested steps)

Complete Chapter Revision

  1. Build a real, maintainable Chrome extension, not a toy.
  2. Extensions suit Claude Code: right technical barrier, real daily use, instant feedback loop.
  3. Phase 0: discuss requirements in Plan mode before coding — it always beats rebuilding later.
  4. Phase 1: scaffold with MV3; manifest.json uses manifest_version 3, a Service Worker, and minimal permissions.
  5. Phase 2: MV3 has three environments with distinct jobs; content.js is a thin shell (DOM + cookies only); all logic lives in background.js and lib/; write the principles into CLAUDE.md.
  6. Phase 3: build bottom-up — db.js first (5 structured keys, atomic writes), then rules.js, api.js, scanner.js (dependency injection), rate-limiter.js (4 levels), background.js, then UI.
  7. Phase 4: debug each environment separately; reload rules differ per environment; add a log system.
  8. Phase 5: "Load unpacked" in Developer mode; run a checklist covering functionality, dedup, rate limiting, persistence, multi-tab.
  9. The 1,211 → 175 refactor: migrate in tested steps; total lines rose but cognitive overhead fell.
  10. Same workflow applies to any extension idea (annotation, translation, timer, GitHub enhancer).

Final Takeaway

The chapter's central lesson:

"Runnable" is not the finish line. Decide who owns what before you write modules, lock the architecture into CLAUDE.md, build bottom-up one module at a time, and refactor in small tested steps. Refactoring is about reducing cognitive overhead, not line count — and Claude Code's reach goes well beyond web pages.

This teaching edition is based on the supplied April 2026, 2nd edition of Claude Code: The Complete Guide (§11, "Hands-on Project: Chrome Extension"). Manifest V3 details, Chrome APIs, file/line counts and the sample project reflect that edition and may change over time.