Use Cases (更新: 2026/6/2)

Claude CodeでChrome拡張機能を開発する:Manifest V3と安全な権限設計

Claude CodeでChrome拡張機能を作る手順。MV3、service worker、content script、権限、テストを実例で解説。

Claude CodeでChrome拡張機能を開発する:Manifest V3と安全な権限設計

Claude CodeでChrome拡張機能を作る前に決めること

Chrome拡張機能は、Webアプリより小さく見えます。しかし実際には、Manifest V3、service worker、content script、message passing、権限、ストレージ、Chrome Web Storeの審査まで含む小さなプロダクトです。Claude Codeに「Chrome拡張を作って」とだけ頼むと、動く雰囲気のコードは出ますが、<all_urls>tabs、外部スクリプト読み込み、永続的なbackground前提の実装が混ざりやすくなります。

この記事の検索意図は「Claude CodeでChrome拡張機能を実装したいが、Manifest V3で何を作ればよいか分からない」です。そこで、選択した文字列をexample.com上でハイライトする最小拡張を作ります。ポップアップはあえて入れず、context menu、service worker、content script、storage、message passingだけに絞ります。小さく作るほど、権限レビューとストア審査で説明しやすいからです。

用語も先にそろえます。Manifest V3は、拡張機能の設定ファイルであるmanifest.jsonの現在の主要形式です。service workerは、拡張機能の裏側でイベントが来たときだけ起きる処理係です。content scriptは、WebページのDOMを読むためにページ側へ差し込まれるスクリプトです。message passingは、service workerとcontent scriptの間でJSONを送り合う連絡方法です。harnessという言葉が出る場合は、ここでは「エージェントが作業するための足場」と考えると分かりやすいです。

公式仕様は必ず一次情報で確認してください。Manifestの必須項目はChrome Extensions Manifest、service workerの考え方はAbout extension service workers、content scriptの制約はChromeのContent scriptsMDNのContent scripts、権限はDeclare permissionsMDNのpermissionsを読みます。Claude Codeの指示設計は内部記事のClaude Code生産性Tipsも合わせて見ると、出力のブレを減らせます。

使いどころを3つに分ける

1つ目のユースケースは、社内ナレッジや仕様書のハイライトです。特定ドメインのドキュメントだけにcontent scriptを入れ、選択したキーワードを色付きで強調します。https://docs.example.com/*のように対象を絞れば、読者やレビュー担当に「この拡張は社内ドキュメントだけを触る」と説明できます。

2つ目は、サポートチーム向けの作業補助です。問い合わせ画面で注文番号や顧客IDを見つけ、右クリックからタグ付けやコピーを実行します。この場合は個人情報を扱う可能性があるため、storageに保存する値、ログに出す値、外部APIへ送る値をClaude Codeに明示させます。便利さよりも、どのデータを読まないかを先に決めるのが現実的です。

3つ目は、記事執筆やSEOレビューの補助です。公開前の記事ページでdescriptionの長さ、h2の数、内部リンク、外部リンクを検査します。このブログの運用なら、Claude CodeセキュリティベストプラクティスClaude Code CLIツール開発と組み合わせ、CLIで記事を検査し、拡張機能でブラウザ上の表示を確認する流れが作れます。

Claude Codeへの依頼文

Claude Codeには、仕様、禁止事項、検証方法を同時に渡します。特にMV3では、service workerが常駐しないこと、content scriptが全APIを直接呼べないこと、権限を最小化することを最初に固定します。

Chrome拡張機能のManifest V3サンプルを作ってください。

要件:
- 対象URLは https://example.com/* だけに限定する
- 右クリックメニューから選択テキストをハイライトする
- service-worker.js、content-script.js、manifest.jsonを素のJavaScriptで書く
- storage.localに enabled と color を保存する
- content scriptとは runtime message で通信する
- <all_urls>、tabs、eval、外部CDN、リモートコード読み込みは禁止
- Playwrightで拡張が読み込まれたことを確認するsmoke scriptも付ける
- Chrome公式ドキュメントの観点で権限レビュー表を作る

この依頼文のポイントは「機能を増やさない」ことです。Claude Codeは親切にpopup、options page、アイコン、Vite、Reactまで足しがちです。最初の検証では便利なUIより、権限、通信、DOM操作、テストを小さく閉じます。

構成とManifest

今回は次の構成にします。ファイルをこのまま作れば、Chromeの「Load unpacked」で読み込めます。

mv3-highlighter/
  manifest.json
  service-worker.js
  content-script.js
  package.json
  playwright-extension-smoke.mjs

manifest.jsonは拡張機能の入口です。Chrome公式ドキュメントでは、すべての拡張機能にこのファイルが必要で、manifest_versionnameversionなどを定義します。ここではcontent_scripts.matcheshttps://example.com/*に限定し、host_permissionsは追加しません。

{
  "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"
    }
  ]
}

権限レビューは次のように書きます。Chrome拡張は「あとで使うかもしれない」権限を入れるほど、ユーザーの警告も審査の説明も重くなります。

項目今回の指定理由入れないもの
API permissionscontextMenus, storage右クリックメニューと設定保存だけに使うtabs, scripting, downloads
対象ページhttps://example.com/*デモ対象を1ドメインに限定する<all_urls>
host permissionsなしcontent scriptの静的マッチで足りる広いhost_permissions
外部コードなしMV3ではリモートコード実行を避けるCDN script、eval

service workerでイベントを受ける

MV3のservice workerは、古いbackground pageのように常駐しません。イベントが来たときに起き、処理が終わると止まります。したがって、メモリ上の変数に重要な状態を置かず、必要な設定はchrome.storage.localから読みます。storage APIの仕様はchrome.storageを確認してください。

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;
});

ここでの落とし穴は、sendResponseを非同期で呼ぶのにreturn trueを忘れることです。Chromeのmessage passingでは、非同期レスポンスを返す場合にチャンネルを開いたままにする必要があります。詳しくはChromeのMessage passingを参照します。

content scriptでDOMを安全に触る

content scriptはページのDOMを読んだり変更したりできますが、拡張機能のすべてのAPIを直接呼べるわけではありません。必要な処理はmessage passingでservice workerへ依頼します。また、ページ側のJavaScriptとは実行環境が分かれています。この分離は安全性に役立ちますが、「ページの変数を直接読めない」というつまずきにもなります。

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;
});

このコードは複雑なHTMLにも完全対応する万能ハイライトではありません。1つのtext node内の最初の一致だけを対象にし、フォーム、script、style、contenteditableは避けます。実プロダクトでは、複数一致、Shadow DOM、動的に追加される本文、既存のmarkとの競合を追加でテストします。

Playwrightで読み込みを検証する

拡張機能の完全なE2Eは難しいですが、少なくとも「Chromeが拡張を読み込んだ」「content scriptが対象ページに入った」は自動化できます。Playwrightの永続コンテキストはlaunchPersistentContextを使います。

{
  "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

手動確認も残します。chrome://extensionsでDeveloper modeを有効化し、Load unpackedからmv3-highlighterを選びます。https://example.com/を開き、見出しの一部を選択し、右クリックメニューから「Highlight selected text」を実行します。ハイライトが表示され、拡張機能のService workerコンソールにエラーが出ていなければ最初の合格です。

失敗例と落とし穴

よくある失敗は、permissionstabsを入れることです。今回のコードはcontext menuのクリックイベントからtab.idを受け取り、対象タブへメッセージを送るだけなので、tabs権限は不要です。タブのURLやタイトルを広く読む必要が出るまで入れません。

2つ目は、content scriptで秘密情報を扱うことです。content scriptはページのDOMに触れるため、Webページの構造変更、広告、ユーザー入力の影響を受けます。APIキー、Claudeのトークン、顧客データをcontent script内へ置くのは避け、必要ならservice worker側で最小のデータだけ扱います。

3つ目は、service workerの寿命を誤解することです。MV3のservice workerは処理後に止まるため、let cache = {}のようなメモリ状態に頼ると、次のイベントで消えていることがあります。設定、キュー、最終実行時刻はstorageに置き、起動時に読み直す設計にします。

4つ目は、DOM書き換えでページを壊すことです。innerHTMLで雑に置換すると、イベントハンドラ、アクセシビリティ、既存UIを壊します。今回の例ではtext nodeを分割してmarkを差し込んでいますが、それでも完全ではありません。Claude Codeには「フォーム、script、style、contenteditableを触らない」「clear関数を先に用意する」と具体的に指示します。

5つ目は、ストア提出直前に権限を広げることです。開発中に<all_urls>で動かすと、審査前に戻し忘れます。最初から狭いドメインで作り、必要なドメインを増やすときは、なぜ必要かをREADMEや審査メモに残します。

パッケージングと公開前レビュー

Chrome Web Storeへ出す前には、コード以外の品質も見ます。アイコン、説明文、プライバシーポリシー、スクリーンショット、権限の説明、利用データの扱いをそろえます。Chromeの公開準備はPrepare your Extensionを確認してください。

ZIP化は、テスト用のprofileやnode_modulesを含めないことが重要です。

zip -r mv3-highlighter.zip manifest.json service-worker.js content-script.js

Windows PowerShellなら次のようにします。

Compress-Archive -Path manifest.json,service-worker.js,content-script.js -DestinationPath mv3-highlighter.zip -Force

公開前チェックリストは次の通りです。

  • manifest.jsondescriptionがChrome Web Storeの上限内で、実際の機能を誇張していない
  • permissionscontent_scripts.matchesが最小になっている
  • service workerで非同期sendResponsereturn trueがある
  • content scriptがフォーム、script、style、contenteditableを壊さない
  • storageに秘密情報や不要な個人情報を保存していない
  • Playwright smokeと手動チェックの両方を通した
  • 公式ドキュメント、内部リンク、スクリーンショット、CTAが記事とストア説明にそろっている

マネタイズCTA

拡張機能の記事は、コードだけで終わると収益導線が弱くなります。読者は「作り方」だけでなく、「自分の業務に安全に入れる方法」「審査で落ちない説明」「Claude Codeにどこまで任せるか」を知りたいはずです。ClaudeCodeLabでは、実リポジトリを題材にしたClaude Code研修・導入相談で、権限レビュー、MV3設計、検証チェックリスト、レビュー用プロンプトまで整理できます。個人でまず型を集めたい場合は教材一覧や無料導線も合わせて使ってください。

この記事で紹介した内容を実際に試した結果、最初に引っかかったのはコードそのものではなく、権限の説明でした。<all_urls>を使えば動作確認は早いものの、なぜ全サイトが必要なのかを説明できません。https://example.com/*に絞り、context menuとstorageだけで作ると、Chromeの拡張管理画面でも警告が少なく、Claude Codeにレビューさせたときの指摘も具体的になりました。MV3拡張は「動いた」よりも「読める権限で動いた」ことを成果にすると、公開前の手戻りが大きく減ります。

#Claude Code #ブラウザ拡張 #Chrome拡張 #Manifest V3 #JavaScript
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。