Skip to content

I Built a Chrome Extension Using Claude Code in One Weekend — Here's What Happened

Chrome extensionsMay 15, 2026

AI scaffolds MV3 fast; permissions, storage, and Web Store policy still need a human who has shipped before.

I Built a Chrome Extension Using Claude Code in One Weekend — Here's What Happened

Everyone talks about AI-generated code. Few people show the actual workflow — the prompts that worked, the output that didn't, and the manual fixes that shipped the product.

This is that post. I used Claude Code to scaffold a Chrome extension from scratch over a weekend. Here's exactly what happened.

The Setup

The project: a Chrome extension that tracks how much time you spend on different websites and shows a daily breakdown in a popup. Think a simpler version of Amethyst.

The tools: Claude Code in the terminal, Vite + TypeScript + CRXJS (the same stack from the TypeScript setup guide).

The constraint: I wanted to see how far AI could take the project before I had to step in.

What Claude Code Got Right

Project Scaffolding

First prompt:

"Create a Chrome extension with Manifest V3 using TypeScript and Vite with the CRXJS plugin. It should have a popup, a background service worker, and a content script. Set up the project structure, tsconfig, and vite config."

Claude Code generated a working project structure in one shot — manifest.json, vite.config.ts, tsconfig.json, entry files for popup, background, and content script. The CRXJS config was correct. The manifest had proper MV3 fields.

Time saved: ~20 minutes of manual setup.

Typed Message Passing

"Add typed message passing between the popup and background service worker using discriminated unions. Messages: GET_STATS (returns daily site stats), RESET_STATS (clears today's data)."

It produced the exact pattern from the message passing guide — discriminated union types, a generic sendMessage wrapper, and a listener with proper return true for async responses.

It even added chrome.runtime.lastError checking. Surprised me.

Storage Schema

"Create a typed chrome.storage.local wrapper for storing per-site time tracking data. Schema: siteStats (map of domain → seconds), currentSession (active site and start timestamp), dailyReset (last reset date)."

Clean output. Generic get/set functions with proper type inference. Similar to the typed storage approach.

What Claude Code Got Wrong

Content Script Injection Timing

Generated code used document.addEventListener('DOMContentLoaded', ...) in the content script. Content scripts declared in manifest.json already run after DOM is ready by default (run_at: "document_idle"). The listener was redundant and occasionally caused double-initialization.

My fix: Removed the wrapper, ran code at top level.

Service Worker Lifecycle

Claude Code stored the active tab timer in a setInterval inside the service worker. This breaks because the service worker terminates when idle — setInterval doesn't keep it alive in MV3.

My fix: Replaced setInterval with chrome.alarms.create (1-minute intervals) and stored the session start time in chrome.storage.local. On each alarm, calculate elapsed time from the stored timestamp. This is exactly the kind of thing the storage post warns about.

Tab Focus Detection

The generated chrome.tabs.onActivated listener didn't account for window focus. If the user switches to a different application entirely, the extension should stop tracking. Chrome doesn't fire onActivated when the browser loses focus.

My fix: Added chrome.windows.onFocusChanged listener to pause tracking when Chrome loses focus.

Popup Styling

This is where AI code quality drops hardest. The popup CSS was generic — no consistent spacing, no attention to the 380px width constraint, no dark mode. Functional but ugly.

My fix: Rewrote the CSS from scratch. ~30 minutes.

The Prompt Patterns That Worked

Specific architecture requests: "Use discriminated unions for message types" beats "add message passing."

Referencing MV3 constraints explicitly: "Remember this is a MV3 service worker that terminates when idle" prevents the setInterval mistake.

Asking for error handling: "Add error handling for cases where the content script isn't injected" produced proper try/catch around chrome.tabs.sendMessage.

Incremental prompts: Building feature-by-feature (first scaffold, then messages, then storage, then UI) produced better results than one massive prompt.

The Prompt Patterns That Failed

"Make it look good": Vague styling prompts produce generic output. Be specific: "Use system-ui font, 380px width, 8px border-radius on cards, #1a1a2e background."

"Handle all edge cases": AI can't know your edge cases. The window focus issue, the content script on chrome:// pages, the popup re-rendering on close/reopen — these came from testing, not prompting.

Complex state machines: When I asked Claude Code to implement a state machine for "idle → tracking → paused → tracking" transitions with all the timer logic, it produced code that was structurally correct but had subtle timing bugs. State machines need manual verification.

Final Scorecard

AreaAI did itI fixed it
Project scaffold100%
Manifest config95%Tweaked permissions
Message passing types100%
Storage wrapper90%Added migration logic
Background logic60%Timer, lifecycle, focus
Content script80%Injection timing
Popup UI/CSS30%Full rewrite
Testing0%All manual

Rough estimate: AI handled ~60% of the total code. I wrote or rewrote the remaining 40% — and that 40% was the hardest 40%.

The Real Lesson

Claude Code is excellent at generating boilerplate, type definitions, and well-known patterns. It's weakest at browser-specific edge cases, visual design, and anything that requires testing against real behavior.

The winning workflow:

  1. Let AI scaffold the structure and patterns
  2. Review every file — understand what it generated (no vibe coding)
  3. Test in the browser immediately
  4. Fix the gaps yourself

The "one weekend" timeline was real — but only because I already understood extension architecture and could spot the AI's mistakes. If I were learning extensions from scratch, the AI output would've looked correct and broken silently.

What's Next

  1. Build the real thingHow I Built ResistGate covers the full product journey with all the hard decisions.
  2. Publish itHow to Publish a Chrome Extension to the Web Store is the final step.

This is part of the Chrome Extensions from Zero to Product series, where I walk through everything I learned building ResistGate and Amethyst — from first popup to published product.

Notes from the build

Get more AI engineering insights

Follow the work: AI tools, browser products, product decisions, and honest lessons from the build.

By subscribing, you agree to receive Orlando's emails. No spam. Unsubscribe anytime.

I Built a Chrome Extension Using Claude Code in One Weekend — Here's What Happened | Orlando Ascanio