How to Build a Claude-Powered Chrome Extension
If you want to build a Chrome extension that uses Claude — for summarizing pages, rewriting text, answering questions about whatever's on screen, or acting as an inline assistant — the core challenge isn't the AI part. It's wiring together Manifest V3's architecture (service workers, content scripts, popups) with an API call that can't run directly from a content script due to CORS and API key exposure. This guide walks through the full setup: extension structure, where the API call actually lives, and how to keep your Claude access secure while shipping something that works.
The short version: you build a standard Manifest V3 extension with a background service worker that handles all API requests, a content script that reads page context, and a popup or side panel for the UI. The Claude call itself goes through a backend endpoint rather than directly from the browser, because embedding a raw API key in extension code means anyone can extract it from the packaged .crx file.
Why you can't call Claude directly from the extension
Chrome extensions ship as unpacked files that end users (or anyone who downloads your extension) can inspect. If you hardcode an API key into a background.js file, it's trivially extractable with dev tools. Browser-based AI APIs also frequently block direct cross-origin requests from extension contexts, so even ignoring the security problem, you'll hit CORS errors.
The standard fix is a thin backend proxy: your extension calls your own HTTPS endpoint, and that endpoint calls Claude with a server-side key. This is exactly the pattern SubToAPI is built for — it turns your Claude access into a standard HTTPS API with sub_live_... application keys that you can call from a server-side route, while your extension talks to that route instead of touching Claude directly. You get streaming, usage metadata, and team seats without maintaining your own backend from scratch. See the quickstart for the setup.
Extension structure
A minimal setup looks like this:
my-claude-extension/
manifest.json
background.js # service worker, handles API calls
content.js # reads page text, injects UI
popup.html
popup.js
icons/
manifest.json
{
"manifest_version": 3,
"name": "Claude Page Assistant",
"version": "1.0.0",
"permissions": ["activeTab", "scripting", "storage"],
"host_permissions": ["https://api.subtoapi.app/*"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
Note the host_permissions entry — Manifest V3 requires you to declare which origins your service worker is allowed to fetch from. Only list the proxy endpoint, never a raw AI provider domain with a key attached to it.
Reading page context with a content script
The content script's job is to grab whatever text the user wants Claude to look at — the full article body, selected text, or a specific DOM region — and pass it to the background worker via chrome.runtime.sendMessage.
// content.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "GET_PAGE_TEXT") {
sendResponse({ text: document.body.innerText.slice(0, 8000) });
}
});
Keep the slice reasonably small — sending the entire DOM text for a dense page wastes tokens and slows down the response. Trim to the main content area if you can identify it.
Calling Claude from the service worker
The background script is where the actual request happens. It never holds a Claude key directly — it calls your proxy, which does:
// background.js
chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => {
if (msg.type === "ASK_CLAUDE") {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${msg.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: msg.prompt }]
})
});
const data = await res.json();
sendResponse(data);
}
return true;
});
In practice, don't ship the sub_live_... key inside the extension bundle either — treat it the same way you'd treat any API credential. The usual approach is to have users authenticate through your own lightweight backend (even a single serverless function) that injects the key server-side, or store it in chrome.storage.local only if the extension is strictly for personal/internal use and never distributed publicly. For a distributed extension, route everything through your own authenticated backend that then calls SubToAPI — see /docs/messages for the request format.
Streaming responses into the UI
For anything longer than a one-line answer, streaming makes the extension feel responsive instead of frozen for several seconds. SubToAPI supports streaming the same way the underlying Claude API does, with server-sent events you can read chunk by chunk and forward to the popup as they arrive. Details on the stream format are in /docs/streaming. In a service worker, you'd read the stream and post incremental updates to the popup via chrome.runtime.sendMessage so the UI updates token by token instead of waiting for the full response.
Popup UI
The popup is just HTML/JS that sends a message to the background worker and renders whatever comes back:
// popup.js
document.getElementById("ask").addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const { text } = await chrome.tabs.sendMessage(tab.id, { type: "GET_PAGE_TEXT" });
const response = await chrome.runtime.sendMessage({
type: "ASK_CLAUDE",
prompt: `Summarize this page:\n\n${text}`
});
document.getElementById("output").innerText = response.content[0].text;
});
This is the whole loop: content script reads the page, popup triggers the request, background worker calls the proxy, response renders back in the popup.
Handling tool use for richer extensions
If your extension needs Claude to take structured actions — extract specific fields, trigger a follow-up fetch, or format output a certain way — use tool definitions in your request rather than parsing free text. This is more reliable than regex-ing Claude's output and scales better as your extension's feature set grows. /docs/tools covers the request shape for defining tools and handling tool-call responses.
Packaging and testing
Load the extension unpacked via chrome://extensions → Developer mode → Load unpacked while building. Test against real pages with varied content length, since token limits and slow responses are where most Claude-powered extensions break down in practice — truncate aggressively and set reasonable max_tokens values before publishing to the Chrome Web Store.
Questions
Can I call the Claude API directly from a content script? No. Browser extension contexts face CORS restrictions, and any API key embedded in extension code is extractable by end users. Route requests through a backend or proxy service instead.
Do I need my own backend, or can SubToAPI replace it? SubToAPI replaces the Claude-side backend work — key management, streaming, usage tracking — but you still need a thin layer (even a single function) to avoid shipping a live key inside a publicly distributed extension.
How do I avoid hitting token limits with long pages? Trim page text to the relevant section before sending it, cap max_tokens on the response, and consider summarizing in chunks for very long articles rather than sending the entire DOM text in one request.