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”.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.