Développer une extension Chrome avec Claude Code : MV3, messages et permissions minimales
Créez une extension Chrome MV3 avec Claude Code : manifest, service worker, content script, permissions et tests.
Commencer par le périmètre, pas par l’interface
Une extension Chrome semble petite, mais elle combine plusieurs sujets sérieux : manifest.json, Manifest V3, service worker, content script, messagerie, permissions, stockage, revue de sécurité, packaging et explication pour le Chrome Web Store. Si vous demandez simplement à Claude Code de “créer une extension Chrome”, il peut produire un projet trop large avec <all_urls>, tabs, popup, React, Vite et scripts externes.
Dans cet article, nous construisons un exemple volontairement limité. L’extension souligne le texte sélectionné sur https://example.com/* via le menu contextuel. Elle utilise JavaScript brut, chrome.storage.local, un service worker, un content script et des messages runtime. Elle évite les permissions larges, le code distant et les dépendances inutiles. Le but est d’obtenir une base copiable, testable et facile à expliquer.
Quelques notions doivent être claires. Manifest V3 est le format actuel du manifeste des extensions Chrome. Le service worker est un traitement de fond événementiel qui se réveille au besoin et ne reste pas actif en permanence. Le content script est injecté dans une page Web et peut lire ou modifier le DOM. Le message passing est l’échange de messages JSON entre contextes d’extension. Si vous croisez le mot harness dans un projet avec agent, pensez à une “structure d’appui” qui permet à l’agent de travailler proprement.
Gardez les sources officielles ouvertes : Chrome Extensions Manifest, About extension service workers, Chrome Content scripts, MDN Content scripts, Chrome Declare permissions et MDN permissions. Pour mieux cadrer Claude Code, lisez aussi les conseils de productivité Claude Code.
Trois cas d’usage concrets
Premier cas : la revue de documentation interne. Une extension limitée à https://docs.example.com/* peut souligner un terme produit pendant la relecture d’une note de version. Le périmètre est étroit, donc l’explication de permission est simple.
Deuxième cas : l’assistance au support. Un agent peut sélectionner un numéro de commande dans un outil interne et utiliser le menu contextuel pour le mettre en évidence. Comme des données personnelles peuvent être visibles, demandez à Claude Code de lister ce que l’extension lit, stocke, transmet et ignore.
Troisième cas : le contrôle éditorial et SEO. Un content script peut vérifier la longueur de la description, le nombre de h2, les liens internes et les liens officiels externes sur une page brouillon. Cette approche complète bien les contrôles CLI de développement d’outils CLI avec Claude Code et les réflexes de sécurité Claude Code.
Prompt à donner à Claude Code
Le prompt doit inclure la fonctionnalité, les interdits et la méthode de vérification. Pour MV3, insistez sur le service worker non persistant et les permissions minimales.
Crée un exemple d'extension Chrome Manifest V3.
Exigences :
- Limiter l'URL cible à https://example.com/*
- Ajouter un élément de menu contextuel pour souligner le texte sélectionné
- Utiliser JavaScript brut : manifest.json, service-worker.js, content-script.js
- Stocker enabled et color dans chrome.storage.local
- Communiquer avec le content script via runtime messages
- Ne pas utiliser <all_urls>, tabs, eval, scripts CDN externes ni code distant
- Ajouter un smoke script Playwright qui confirme le chargement de l'extension
- Inclure une table de revue des permissions basée sur la documentation Chrome
Le point important est de limiter la première version. Si Claude Code ajoute une popup, une page d’options, des icônes, un bundler et des permissions larges, demandez-lui de réduire l’exemple avant d’analyser le comportement.
Structure et Manifest
Cette structure se charge directement avec “Load unpacked” dans 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"
}
]
}
| Zone | Réglage | Raison | Évité |
|---|---|---|---|
| Permissions API | contextMenus, storage | Menu et réglages | tabs, scripting, downloads |
| Pages | https://example.com/* | Un domaine de démonstration | <all_urls> |
| Host permissions | Aucune | Le content script statique suffit | Permissions larges |
| Code distant | Aucun | Revue MV3 plus simple | CDN, eval |
Service worker
Le service worker reçoit les événements de l’extension et envoie un message au content script. Comme il peut s’arrêter entre deux événements, il faut lire les réglages depuis le storage au moment utile.
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;
});
L’erreur fréquente consiste à oublier return true lorsque sendResponse est appelé après une promesse. Sans cela, le canal de réponse peut se fermer trop tôt. Revoyez ce point avec Chrome Message passing.
Content script
Le content script modifie le DOM de la page. Il ne doit pas contenir de secret, de token Claude ou de clé API. Il attend un message, applique le surlignage et écrit un marqueur data-* pour le smoke test.
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;
});
Ce code reste volontairement modeste. Il traite la première occurrence dans chaque text node et évite les formulaires, scripts, styles et zones éditables. Pour un produit réel, testez les occurrences multiples, le Shadow DOM, le contenu dynamique et les conflits avec des balises mark existantes.
Tests Playwright et vérification manuelle
Playwright peut charger une extension non empaquetée avec launchPersistentContext. Le test suivant confirme que le content script est bien injecté.
{
"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
Vérifiez aussi manuellement : ouvrez chrome://extensions, activez Developer mode, chargez le dossier avec Load unpacked, visitez https://example.com/, sélectionnez un texte et lancez “Highlight selected text”. La console du service worker ne doit pas afficher d’erreur.
Pièges, packaging et retour d’expérience
Les pièges classiques sont l’ajout trop rapide de tabs, le stockage de secrets dans le content script, la confiance excessive dans la mémoire du service worker, les remplacements DOM avec innerHTML et l’oubli de retirer <all_urls> après le débogage. Chaque point rend la revue plus difficile.
Avant publication, suivez Prepare your Extension. Préparez icônes, captures, politique de confidentialité, justification des permissions et description courte. Le ZIP ne doit pas contenir node_modules ni le profil Playwright.
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
La valeur commerciale n’est pas seulement dans la génération des fichiers. Elle se trouve dans la revue des permissions, les limites de sécurité, les tests et l’explication de publication. ClaudeCodeLab peut accompagner cela via la formation et le conseil Claude Code. Pour travailler en autonomie, consultez aussi la bibliothèque de produits.
Note pratique : lorsque Masa a testé ce modèle, le point le plus utile n’était pas la syntaxe JavaScript, mais la justification des permissions. Avec <all_urls>, la démonstration est rapide mais l’explication est faible. En limitant à https://example.com/* et à contextMenus plus storage, l’avertissement Chrome devient plus acceptable et la revue de Claude Code plus précise. En MV3, “ça marche” ne suffit pas ; “ça marche avec des permissions explicables” est le vrai seuil.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.