Desenvolver extensões Chrome com Claude Code: MV3, mensagens e permissões mínimas
Crie uma extensão Chrome MV3 com Claude Code: manifest, service worker, content script, permissões, storage e testes.
Comece pelo limite da extensão
Uma extensão Chrome parece pequena, mas é um produto completo em miniatura. Ela envolve manifest.json, Manifest V3, service worker, content script, mensagens, permissões, armazenamento, revisão de segurança, empacotamento e uma explicação clara para a Chrome Web Store. Se você pedir apenas “crie uma extensão Chrome” ao Claude Code, ele pode gerar um projeto grande com <all_urls>, tabs, popup, React, Vite e scripts externos.
Aqui vamos construir uma versão controlada: uma extensão Manifest V3 que destaca texto selecionado em https://example.com/* usando o menu de contexto. Ela usa JavaScript simples, chrome.storage.local, um service worker, um content script e runtime messages. Não usamos permissões amplas, código remoto nem dependências desnecessárias. Assim o exemplo fica copiável, testável e fácil de explicar.
Os termos importam. Manifest V3 é o formato atual do manifesto de extensões Chrome. O service worker é um processo de fundo orientado a eventos, que pode parar entre execuções. O content script é injetado em páginas e pode ler ou alterar o DOM. Message passing é a troca de mensagens JSON entre contextos da extensão. Quando aparecer a palavra harness em desenvolvimento com agentes, pense em uma “base de apoio” para o agente trabalhar com segurança.
Use documentação oficial durante a revisão: Chrome Extensions Manifest, About extension service workers, Chrome Content scripts, MDN Content scripts, Chrome Declare permissions e MDN permissions. Para prompts melhores, veja também dicas de produtividade com Claude Code.
Três casos de uso práticos
O primeiro caso é revisão de documentação interna. A extensão pode ser limitada a https://docs.example.com/* e destacar termos de produto em notas de release. Como o padrão de URL é estreito, a justificativa de permissão fica simples.
O segundo caso é suporte ao cliente. Um atendente pode selecionar um número de pedido em uma tela administrativa e usar o menu de contexto para destacar ou copiar a informação. Como pode haver dados pessoais, peça ao Claude Code uma tabela com o que a extensão lê, armazena, envia e ignora.
O terceiro caso é QA editorial e SEO. Um content script pode verificar tamanho de description, quantidade de h2, links internos e links oficiais externos em uma página de rascunho. Isso combina bem com verificações de CLI em desenvolvimento de ferramentas CLI com Claude Code e com práticas de segurança com Claude Code.
Prompt para Claude Code
O prompt precisa trazer função, restrições e validação. Em MV3, deixe explícito que o service worker não é persistente e que permissões amplas não são aceitáveis.
Crie uma extensão Chrome Manifest V3 de exemplo.
Requisitos:
- Limitar a URL alvo a https://example.com/*
- Adicionar um item de menu de contexto para destacar texto selecionado
- Usar JavaScript puro: manifest.json, service-worker.js, content-script.js
- Salvar enabled e color em chrome.storage.local
- Comunicar com o content script usando runtime messages
- Não usar <all_urls>, tabs, eval, scripts CDN externos ou código remoto
- Adicionar um smoke script Playwright que confirme o carregamento da extensão
- Incluir uma tabela de revisão de permissões baseada na documentação do Chrome
A regra é manter a primeira versão pequena. Se Claude Code adicionar popup, options page, ícones, bundler e permissões amplas, peça para reduzir antes de revisar comportamento.
Estrutura e Manifest
Esta pasta pode ser carregada diretamente com “Load unpacked” em chrome://extensions.
mv3-highlighter/
manifest.json
service-worker.js
content-script.js
package.json
playwright-extension-smoke.mjs
{
"manifest_version": 3,
"name": "Claude Code MV3 Highlighter",
"version": "0.1.0",
"description": "Highlights selected text on example.com from a context menu.",
"action": {
"default_title": "MV3 Highlighter"
},
"permissions": ["contextMenus", "storage"],
"background": {
"service_worker": "service-worker.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://example.com/*"],
"js": ["content-script.js"],
"run_at": "document_idle"
}
]
}
| Área | Configuração | Motivo | Evitado |
|---|---|---|---|
| Permissões API | contextMenus, storage | Menu e configurações | tabs, scripting, downloads |
| Acesso a páginas | https://example.com/* | Um domínio de demonstração | <all_urls> |
| Host permissions | Nenhuma | O content script estático basta | Permissões amplas |
| Código remoto | Nenhum | Revisão MV3 mais simples | CDN, eval |
Service worker
O service worker recebe eventos e envia mensagens ao content script. Como ele pode parar entre eventos, as configurações precisam ser lidas do storage quando necessário.
const MENU_ID = "highlight-selection";
const DEFAULT_SETTINGS = {
enabled: true,
color: "#fff176"
};
async function readSettings() {
return chrome.storage.local.get(DEFAULT_SETTINGS);
}
chrome.runtime.onInstalled.addListener(async () => {
const settings = await readSettings();
await chrome.storage.local.set(settings);
chrome.contextMenus.create({
id: MENU_ID,
title: "Highlight selected text",
contexts: ["selection"],
documentUrlPatterns: ["https://example.com/*"]
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== MENU_ID || !tab?.id || !info.selectionText) {
return;
}
const settings = await readSettings();
await chrome.tabs.sendMessage(tab.id, {
type: "HIGHLIGHT_SELECTION",
text: info.selectionText.slice(0, 120),
enabled: settings.enabled,
color: settings.color
});
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === "GET_SETTINGS") {
readSettings().then(sendResponse);
return true;
}
if (message?.type === "SAVE_SETTINGS") {
const nextSettings = {
enabled: Boolean(message.enabled),
color: String(message.color || DEFAULT_SETTINGS.color)
};
chrome.storage.local.set(nextSettings).then(() => {
sendResponse({ ok: true, settings: nextSettings });
});
return true;
}
return false;
});
O erro comum é esquecer return true quando sendResponse é chamado depois de uma Promise. Confira esse trecho com Chrome Message passing.
Content script
O content script altera o DOM, mas não deve armazenar chaves, tokens ou dados sensíveis. Ele espera uma mensagem, destaca o texto e grava um marcador data-* para o teste Playwright.
const HIGHLIGHT_CLASS = "cc-mv3-highlight";
document.documentElement.dataset.ccMv3Highlighter = "ready";
function shouldSkipNode(node) {
const parent = node.parentElement;
if (!parent) return true;
return Boolean(
parent.closest(
`script, style, textarea, input, [contenteditable="true"], .${HIGHLIGHT_CLASS}`
)
);
}
function clearHighlights() {
for (const mark of document.querySelectorAll(`.${HIGHLIGHT_CLASS}`)) {
const text = document.createTextNode(mark.textContent || "");
mark.replaceWith(text);
text.parentElement?.normalize();
}
}
function createMark(text, color) {
const mark = document.createElement("mark");
mark.className = HIGHLIGHT_CLASS;
mark.textContent = text;
mark.style.backgroundColor = color;
mark.style.color = "#111";
mark.style.padding = "0 2px";
return mark;
}
function highlightText(term, color) {
const query = term.trim();
if (!query) return 0;
clearHighlights();
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (shouldSkipNode(node)) return NodeFilter.FILTER_REJECT;
return node.nodeValue.toLowerCase().includes(query.toLowerCase())
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
}
});
const matches = [];
while (walker.nextNode()) {
const node = walker.currentNode;
const index = node.nodeValue.toLowerCase().indexOf(query.toLowerCase());
if (index >= 0) matches.push({ node, index });
}
for (const { node, index } of matches.reverse()) {
const selected = node.splitText(index);
selected.splitText(query.length);
selected.replaceWith(createMark(selected.nodeValue, color));
}
return matches.length;
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type !== "HIGHLIGHT_SELECTION") {
return false;
}
if (!message.enabled) {
clearHighlights();
sendResponse({ ok: true, count: 0 });
return false;
}
const count = highlightText(String(message.text || ""), message.color || "#fff176");
sendResponse({ ok: true, count });
return false;
});
O exemplo é simples de propósito. Ele destaca a primeira ocorrência em cada text node e evita formulários, scripts, estilos e áreas editáveis. Em produção, teste múltiplas ocorrências, Shadow DOM, conteúdo dinâmico e conflito com elementos mark.
Playwright e checklist manual
Com launchPersistentContext, Playwright carrega uma extensão não empacotada e verifica se o content script entrou na página.
{
"type": "module",
"scripts": {
"smoke": "node playwright-extension-smoke.mjs"
},
"devDependencies": {
"playwright": "^1.52.0"
}
}
import { chromium } from "playwright";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const extensionPath = path.resolve(__dirname);
const userDataDir = path.resolve(__dirname, ".pw-extension-profile");
const context = await chromium.launchPersistentContext(userDataDir, {
headless: false,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`
]
});
const page = await context.newPage();
await page.goto("https://example.com/");
await page.waitForFunction(() => {
return document.documentElement.dataset.ccMv3Highlighter === "ready";
});
console.log("Extension content script is loaded on https://example.com/");
await page.waitForTimeout(3000);
await context.close();
npm install
npm run smoke
Teste manual: abra chrome://extensions, ative Developer mode, carregue a pasta com Load unpacked, visite https://example.com/, selecione parte do título e clique em “Highlight selected text”. Confira também o console do service worker.
Armadilhas, pacote e resultado prático
As armadilhas mais comuns são adicionar tabs cedo demais, colocar segredos no content script, confiar em memória do service worker, substituir DOM com innerHTML e esquecer <all_urls> depois do debug.
Antes de publicar, siga Prepare your Extension. Prepare ícones, screenshots, política de privacidade, explicação de permissões e descrição curta. Empacote só os arquivos da extensão:
zip -r mv3-highlighter.zip manifest.json service-worker.js content-script.js
Compress-Archive -Path manifest.json,service-worker.js,content-script.js -DestinationPath mv3-highlighter.zip -Force
A monetização está menos no código gerado e mais no processo: revisão de permissões, limites de segurança, testes e publicação. A ClaudeCodeLab ajuda equipes com treinamento e consultoria em Claude Code, revisando MV3, prompts, checklists e justificativas de permissão em repositórios reais. Para estudar sozinho, veja a biblioteca de produtos.
Nota prática: quando Masa testou esse padrão, o primeiro problema útil não foi a sintaxe, e sim a justificativa das permissões. Com <all_urls> a demonstração é rápida, mas a explicação fica fraca. Ao limitar para https://example.com/* e manter apenas contextMenus e storage, o aviso do Chrome ficou menor e a revisão do Claude Code ficou mais objetiva. Em MV3, o marco real não é “funciona”, mas “funciona com permissões explicáveis”.
Artigos relacionados
Bun Runtime com Claude Code: adoção prática
Adote Bun com Claude Code: Bun.serve, scripts do package, testes, compatibilidade Node, riscos e rollout.
Roadmap de Claude Code para projetos pessoais: dos primeiros 30 minutos à monetização
Guia para usar Claude Code em aprendizado, projetos de fim de semana, Obsidian, checklist de publicação e monetização.
Desenvolvimento Go com Claude Code: go.mod, testes, concorrencia e race detector
Guia pratico Claude Code para Go: mapa do repo, go.mod, go work, API/CLI, testes, context, concorrencia, benchmarks.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.