How to Build a Chrome Extension with AI โ€” From Zero to Shipped

Chrome extensions with AI features are one of the highest-leverage things a developer can build right now. Small surface area, real user value, fast to ship.

But most tutorials cover either Chrome extension basics or AI API integration โ€” never both together. This guide does both. You will build a working Chrome extension that calls the Anthropic Claude API, processes page content, and returns AI-generated output to the user.

By the end you will have a working extension you can load locally, iterate on, and publish to the Chrome Web Store.


๐ŸŽฏ Quick Answer (30-Second Read)

  • What you are building: A Chrome extension with a popup UI that sends selected text or page content to Claude and displays the AI response
  • Core pieces: manifest.json, popup HTML/JS, content script, background service worker, Anthropic API call
  • API used: Anthropic Claude API (claude-sonnet-4-5)
  • Key constraint: Never expose your API key in client-side extension code โ€” use a backend proxy
  • Time to build: 2โ€“4 hours for a working MVP
  • Prerequisites: JavaScript, basic Chrome extension concepts, an Anthropic API key

How Chrome Extensions Work with AI

Before writing code, understand the four moving parts in an AI-powered Chrome extension:

flowchart TD A([๐Ÿ‘ค User clicks extension]) --> B[Popup UI\npopup.html + popup.js] B --> C{What does\nuser want?} C -->|Selected text| D[Content Script\ncontent.js] C -->|Full page| D C -->|Direct input| E[Popup handles it\ndirectly] D -->|sends text via\nchrome.runtime.sendMessage| F[Background\nService Worker\nbackground.js] E -->|fetch request| F F -->|POST /v1/messages| G[Backend Proxy\nNode / Vercel / CF Worker] G -->|authenticated request| H[Anthropic API\nClaude Sonnet] H -->|AI response| G G -->|response| F F -->|sendResponse| B B --> I([โœ… Display result\nto user]) style A fill:#0f172a,color:#e2e8f0,stroke:#334155 style I fill:#166534,color:#dcfce7,stroke:#16a34a style H fill:#7c2d12,color:#fed7aa,stroke:#f97316 style G fill:#78350f,color:#fef3c7,stroke:#f59e0b style B fill:#1e293b,color:#cbd5e1,stroke:#475569 style C fill:#1e293b,color:#cbd5e1,stroke:#475569 style D fill:#312e81,color:#e0e7ff,stroke:#6366f1 style E fill:#312e81,color:#e0e7ff,stroke:#6366f1 style F fill:#1e3a5f,color:#bfdbfe,stroke:#3b82f6

Popup โ€” the small window that opens when the user clicks your extension icon. This is your main UI.

Content script โ€” JavaScript injected into the current webpage. It can read DOM content, selected text, and page metadata. It cannot make cross-origin API calls directly.

Background service worker โ€” runs in the background, handles API calls, manages state between popup and content script. This is where your Anthropic API call lives.

Backend proxy โ€” a thin server that holds your API key securely. The extension calls your proxy, your proxy calls Anthropic. Never call Anthropic directly from the extension with a hardcoded key.


Project Structure

chrome-ai-extension/
โ”œโ”€โ”€ manifest.json
โ”œโ”€โ”€ popup.html
โ”œโ”€โ”€ popup.js
โ”œโ”€โ”€ content.js
โ”œโ”€โ”€ background.js
โ”œโ”€โ”€ styles.css
โ”œโ”€โ”€ icons/
โ”‚   โ”œโ”€โ”€ icon16.png
โ”‚   โ”œโ”€โ”€ icon48.png
โ”‚   โ””โ”€โ”€ icon128.png
โ””โ”€โ”€ proxy/               โ† separate backend project
    โ”œโ”€โ”€ index.js
    โ””โ”€โ”€ package.json

Step-by-Step Build Guide

Step 1 โ€” Create the manifest.json

The manifest is the configuration file Chrome reads to understand your extension. Use Manifest V3 โ€” it is the current standard and required for new Chrome Web Store submissions.

{
  "manifest_version": 3,
  "name": "AI Page Assistant",
  "version": "1.0.0",
  "description": "Summarize and analyze any page with Claude AI",
  "permissions": [
    "activeTab",
    "scripting",
    "storage"
  ],
  "host_permissions": [
    "https://your-proxy-domain.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": [""],
      "js": ["content.js"]
    }
  ]
}

Key permissions explained:

  • activeTab โ€” access the currently active tab when user clicks extension
  • scripting โ€” inject content scripts programmatically
  • storage โ€” save user preferences locally
  • host_permissions โ€” allow fetch calls to your proxy domain

Step 2 โ€” Build the Popup UI





  
  


  

AI Assistant

Step 3 โ€” Write the Popup Logic

// popup.js
const PROXY_URL = 'https://your-proxy-domain.com/api/claude';

function showLoading() {
  document.getElementById('loading').classList.remove('hidden');
  document.getElementById('result').classList.add('hidden');
  document.getElementById('error').classList.add('hidden');
}

function showResult(text) {
  document.getElementById('loading').classList.add('hidden');
  const result = document.getElementById('result');
  result.textContent = text;
  result.classList.remove('hidden');
}

function showError(message) {
  document.getElementById('loading').classList.add('hidden');
  const error = document.getElementById('error');
  error.textContent = message;
  error.classList.remove('hidden');
}

async function getPageContent() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => document.body.innerText.slice(0, 8000)
  });
  return results[0].result;
}

async function getSelectedText() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => window.getSelection().toString()
  });
  return results[0].result;
}

async function callClaude(prompt) {
  const response = await fetch(PROXY_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt })
  });

  if (!response.ok) {
    throw new Error('API request failed: ' + response.status);
  }

  const data = await response.json();
  return data.result;
}

document.getElementById('summarizeBtn').addEventListener('click', async () => {
  showLoading();
  try {
    const content = await getPageContent();
    const result = await callClaude(
      'Summarize this page content clearly and concisely:\n\n' + content
    );
    showResult(result);
  } catch (err) {
    showError('Failed to summarize: ' + err.message);
  }
});

document.getElementById('selectedBtn').addEventListener('click', async () => {
  showLoading();
  try {
    const selected = await getSelectedText();
    if (!selected) return showError('No text selected on the page.');
    const result = await callClaude(
      'Analyze and explain this selected text:\n\n' + selected
    );
    showResult(result);
  } catch (err) {
    showError('Failed to analyze: ' + err.message);
  }
});

document.getElementById('askBtn').addEventListener('click', async () => {
  const prompt = document.getElementById('customPrompt').value.trim();
  if (!prompt) return showError('Please enter a question.');
  showLoading();
  try {
    const content = await getPageContent();
    const result = await callClaude(
      'Page content:\n' + content + '\n\nQuestion: ' + prompt
    );
    showResult(result);
  } catch (err) {
    showError('Request failed: ' + err.message);
  }
});

Step 4 โ€” Build the Backend Proxy

Never call the Anthropic API directly from extension code. Your API key would be visible to anyone who inspects the extension. Deploy a thin proxy instead.

// proxy/index.js โ€” deploy to Vercel, Railway, or Cloudflare Workers
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { prompt } = req.body;

  if (!prompt || typeof prompt !== 'string') {
    return res.status(400).json({ error: 'Invalid prompt' });
  }

  if (prompt.length > 20000) {
    return res.status(400).json({ error: 'Prompt too long' });
  }

  try {
    const message = await client.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 1024,
      messages: [{ role: 'user', content: prompt }]
    });

    return res.status(200).json({
      result: message.content[0].text
    });
  } catch (err) {
    console.error('Anthropic API error:', err);
    return res.status(500).json({ error: 'AI request failed' });
  }
}

Deploy this to Vercel with one command:

cd proxy
npm install @anthropic-ai/sdk
vercel deploy

Set ANTHROPIC_API_KEY as an environment variable in your Vercel project settings. Update PROXY_URL in popup.js with your deployed URL.

Step 5 โ€” Add Basic Styles

/* styles.css */
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  width: 360px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #0f172a;
  color: #e2e8f0;
}

.container { padding: 16px; }

.title {
  font-size: 16px;
  font-weight: 700;
  color: #f8fafc;
  margin-bottom: 12px;
  letter-spacing: -0.02em;
}

.button-group { display: flex; gap: 8px; margin-bottom: 10px; }

.btn {
  padding: 8px 14px;
  border: none;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; }
.btn-primary { background: #6366f1; color: #fff; flex: 1; }
.btn-secondary { background: #1e293b; color: #94a3b8; flex: 1; }
.btn-ghost {
  width: 100%;
  background: #1e293b;
  color: #e2e8f0;
  margin-top: 8px;
}

textarea {
  width: 100%;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 6px;
  color: #e2e8f0;
  padding: 8px 10px;
  font-size: 13px;
  resize: none;
  outline: none;
}
textarea:focus { border-color: #6366f1; }

.loading {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  font-size: 13px;
  color: #94a3b8;
}

.result {
  margin-top: 12px;
  padding: 10px;
  background: #1e293b;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.6;
  color: #e2e8f0;
  max-height: 300px;
  overflow-y: auto;
  white-space: pre-wrap;
}

.error {
  margin-top: 12px;
  padding: 10px;
  background: #7f1d1d;
  border-radius: 6px;
  font-size: 13px;
  color: #fecaca;
}

.hidden { display: none !important; }

.spinner {
  width: 14px; height: 14px;
  border: 2px solid #334155;
  border-top-color: #6366f1;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }

Step 6 โ€” Load the Extension in Chrome

  1. Open Chrome and go to chrome://extensions
  2. Enable Developer mode (toggle top-right)
  3. Click Load unpacked
  4. Select your extension folder (the one with manifest.json)
  5. The extension appears in your toolbar โ€” click it to test

Any changes to your files require clicking the refresh icon on chrome://extensions.


Key Features to Add Next

  • Streaming responses โ€” use the Anthropic streaming API so results appear word by word instead of all at once
  • Conversation history โ€” store previous Q&A pairs in chrome.storage.local for context across questions
  • Page-specific prompts โ€” detect the current domain and auto-suggest relevant prompts (GitHub โ†’ explain this PR, news sites โ†’ summarize article)
  • Copy to clipboard button โ€” add a one-click copy button below every AI response
  • Keyboard shortcut โ€” register a commands entry in manifest to open the popup without clicking the icon

Common Errors and Fixes

Error Cause Fix
Cannot read properties of undefined (reading 'result') Content script not injected yet Add await before executeScript and check tab permissions
Refused to connect to proxy URL Missing host_permissions in manifest Add your proxy domain to host_permissions in manifest.json
Extension context invalidated Extension reloaded while popup was open Reload the popup โ€” this is expected during development
403 from proxy CORS not configured on proxy Add Access-Control-Allow-Origin: * header to proxy responses
Response is empty Page text extraction returned nothing Check document.body.innerText vs document.body.textContent for the target page

Real Developer Use Case

A developer built an internal Chrome extension for their content team. The extension added a sidebar button to every webpage that, when clicked, sent the full article text to Claude and returned a structured summary with key points, sentiment, and suggested social media captions.

The popup UI took one afternoon. The proxy deployed to Vercel in 20 minutes. The content team had a working tool the same day, without waiting for a full web app to be designed and built.

Three months later, the same extension was extended with a second button that detected product pages and auto-generated SEO metadata. Same architecture โ€” new prompt, new button.


Frequently Asked Questions

Can I call the Anthropic API directly from the extension without a proxy?
Technically yes, but you should never do it. Any API key hardcoded or stored in extension files is accessible to anyone who installs or inspects the extension. Use a backend proxy that holds the key server-side. Vercel serverless functions are the lowest-friction option for this.

Does this work with Manifest V2?
The code in this guide targets Manifest V3, which is required for new Chrome Web Store submissions as of 2024. MV2 extensions still run but cannot be submitted as new listings. Use MV3 for any new extension you plan to publish.

How do I handle pages that block content scripts?
Some pages (Chrome Web Store, chrome:// URLs, certain enterprise pages) block content script injection. Wrap your executeScript call in a try-catch and show a friendly error when the page cannot be accessed.

How do I publish to the Chrome Web Store?
Create a developer account at chrome.google.com/webstore/devconsole (one-time $5 fee), zip your extension folder excluding the proxy directory, upload the zip, fill in the store listing, and submit for review. Review typically takes 1โ€“3 business days.

Can I use a different AI model in my extension?
Yes โ€” swap the model string in your proxy from claude-sonnet-4-5 to any model in the Anthropic API. Sonnet is the best default for extensions: fast enough for real-time use, smart enough for complex tasks, and cheaper than Opus for high-volume usage.


Conclusion

Building a Chrome extension with AI is one of the fastest paths from idea to shipped product in 2025. The surface area is small, the distribution channel (Chrome Web Store) is built-in, and the Anthropic API handles the hard intelligence layer.

The architecture is always the same: popup UI โ†’ content script for page data โ†’ background worker โ†’ backend proxy โ†’ Claude API โ†’ display result.

Get the proxy deployed, get the manifest right, load it unpacked, and iterate. Most developers have a working AI Chrome extension within a single day of focused work.


Related reads: How Developers Use AI to Build Apps Faster ยท Claude Code Setup Guide ยท How AI Agents Write Code Automatically ยท How to Create a CLAUDE.md Configuration File