Claude CodeでChrome拡張をManifest V3で開発する手順
manifest.jsonの必須キー、service workerとcontent scriptの分離、chrome://extensionsでのデバッグ手順までをClaude Codeでの開発フローとして扱います。
Manifest V3(MV3)はChrome拡張機能プラットフォームの最新仕様です。MV2にあった常駐型のバックグラウンドページはservice workerに置き換わり、ページ上で動くcontent scriptとは実行環境を分けて書く必要があります。この記事では、manifest.jsonの必須キーからservice workerとcontent scriptの分離までを扱います。chrome://extensionsでの読み込みとデバッグも含め、Claude Codeで拡張機能を開発するときの具体的な手順を示します。
Chrome拡張(Manifest V3)の全体像
拡張機能は大きく3つの要素で構成されます。設定を書くmanifest.json、バックグラウンドのイベント処理を担うservice worker、ページ上で動くcontent scriptです。この3層構成を前提にコードを組み立てます。
MV3が目指すのは、プライバシー・セキュリティ・パフォーマンスの改善です。公式はこの移行を数年がかりの取り組みと位置付けており、実務で押さえるべき変更点は次の3つです。
- service workerへの移行: MV2の常駐バックグラウンドページは、必要なときだけ起動するservice workerに置き換わりました
- リモートコードの禁止: 拡張機能パッケージに含まれないJavaScriptを実行時に読み込むことができません。CDN経由のライブラリ読み込みは通りません
- webRequestの非推奨化: 通信をすべて拡張機能経由でプロキシしていたwebRequestのblocking版は非推奨で、
declarativeNetRequestが代替として案内されています
この発想はElectronアプリのmain process/renderer processの分離と近いものがあります。Claude CodeでElectronアプリを開発する手順ではメイン/レンダラー構成を扱っています。プロセスを分離して書くという考え方は、そちらも参考になります。
Claude Codeにコードを書かせるときは、この3点を制約条件として先に伝えておくと、MV2時代の実装パターン(常駐前提のロジックやCDNからのライブラリ読み込み)を提案されにくくなります。
manifest.jsonに必要なキー
すべての拡張機能はルートディレクトリにmanifest.jsonを置きます。公式が示す最小構成は次の5キーです。
{
"manifest_version": 3,
"name": "Minimal Manifest",
"version": "1.0.0",
"description": "A basic example extension with only required keys",
"icons": {
"48": "images/icon-48.png",
"128": "images/icon-128.png"
}
}manifest_versionは3で固定します。ここにbackground・content_scripts・permissions・host_permissionsを足していく形で挙動を組み立てます。
| キー | 役割 | 必須/任意 |
|---|---|---|
manifest_version | 役割バージョン指定。3で固定 | 必須/任意必須 |
name / version / description / icons | 役割拡張機能の基本情報 | 必須/任意必須 |
background.service_worker | 役割バックグラウンド処理を書くファイルを指定 | 必須/任意service worker利用時に必須 |
content_scripts | 役割ページに挿入するスクリプトをmatchesパターンで指定 | 必須/任意content script利用時に必須 |
permissions / host_permissions | 役割使うAPIとアクセス先ドメインの許可 | 必須/任意使うAPIに応じて必須 |
content_scriptsではmatchesが必須で、対象パターンを指定しないと自動実行されません。css・jsはどちらか、または両方を指定できます。
service workerとcontent scriptを分離して書く
MV3のservice workerは、必要なときにロードされ、休止すればアンロードされます。イベントを受け取っている間は動き続けますが、シャットダウンすることもあります。DOMへのアクセスはできず、必要ならoffscreen documentと組み合わせます。
content scriptはページの実行コンテキストで動きますが、拡張機能本体とは分離された世界(isolated world)で実行されます。ページ側のJavaScript変数を直接読み書きすることはできません。content scriptから直接呼べる拡張機能APIは限られています。
content scriptから直接呼べるのは次のAPIです。
dom/i18n/storageruntime.connect()/runtime.getManifest()/runtime.getURL()runtime.id/runtime.onConnect/runtime.onMessage/runtime.sendMessage()
tabs・scripting・declarativeNetRequestなど、それ以外の大半のAPIはcontent scriptから直接呼べません。「content scriptがタブを操作する」ような処理は避け、content scriptからservice workerへメッセージを送り、service worker側でchrome.tabsやchrome.scriptingを呼ぶ設計にします。外部サーバーへfetch()するときも、対象URLをhost_permissionsに宣言していないとリクエストが通りません。
Claude Codeでブラウザ・OS向けの拡張機能を作る手順は他にも扱っています。Claude CodeでFigmaプラグインを開発する手順やClaude CodeでRaycast拡張を作る手順も、本体とUI/ページを分離して書く設計は共通しています。
content scriptとservice workerの間でメッセージをやり取りする
1回限りのメッセージにはruntime.sendMessage()(拡張機能内向け)とtabs.sendMessage()(content script向け)を使います。例えば次のような形になります。
content-script.js:
document.addEventListener("mouseup", () => {
const text = window.getSelection()?.toString().trim();
if (!text) return;
chrome.runtime.sendMessage({ type: "SAVE_SELECTION", text });
});service-worker.js:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== "SAVE_SELECTION") return;
chrome.storage.local.get({ notes: [] }, ({ notes }) => {
notes.push({ text: message.text, url: sender.tab?.url, savedAt: Date.now() });
chrome.storage.local.set({ notes }, () => sendResponse({ ok: true }));
});
return true;
});sendResponseは既定では同期呼び出しが前提です。chrome.storageのような非同期処理をはさんで後から応答したい場合は、リスナーの戻り値としてリテラルのtrueを返し、メッセージチャネルを開いたままにします。Chrome 148からはリスナーがPromiseを返す形でも非同期応答ができるようになっていますが、公式もこの対応は段階的な展開中だとしています。当面はreturn trueのパターンを使うのが無難です。
Claude Codeで開発ループを回す
Claude Codeにこの3層構成のコードを書かせるとき、CLAUDE.mdへMV3固有の制約を明文化しておくと、MV2的な実装への逆戻りを防げます。例えば次のような断片です。
## Chrome拡張(Manifest V3)の規約
- manifest_version は3固定。MV2のbackground pageパターンに戻さない
- backgroundはservice_workerのみ。常駐前提の状態をグローバル変数に溜めない
- content scriptとservice workerの状態共有はchrome.storage経由で行う
- 外部CDNからスクリプトを読み込まない(パッケージ外のコード実行は不可)
- 新しいchrome.*APIを使うときはmanifest.jsonのpermissions/host_permissionsを同時に更新するmanifest.jsonの書き間違いはchrome://extensionsで読み込むまで気づきにくいため、編集のたびに機械的に検証するループを組んでおくと手戻りが減ります。最小構成なら次のようなチェックスクリプトで十分です。
scripts/check-manifest.js:
const fs = require("fs");
const manifest = JSON.parse(fs.readFileSync("manifest.json", "utf8"));
if (manifest.manifest_version !== 3) {
console.error("manifest_version must be 3");
process.exit(1);
}
for (const key of ["name", "version", "description", "icons"]) {
if (!(key in manifest)) {
console.error(`missing key: ${key}`);
process.exit(1);
}
}
console.log("manifest.json ok");これをhooksに登録すると、Claude Codeがmanifest.jsonをEdit/Writeするたびに自動で実行されます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"if": "Edit(manifest.json)",
"command": "node scripts/check-manifest.js"
}
]
}
]
}
}permissions.allowにビルド・確認コマンドを登録しておけば、毎回の確認プロンプトを減らせます。
{
"permissions": {
"allow": ["Bash(node scripts/*)", "Bash(npm run *)"]
}
}Claude Codeにこのスクリプトの出力を読ませます。missing keyのようなエラーが出たら該当キーを追記させるループを回すと、chrome://extensionsで読み込む前の段階で構造的なミスを潰せます。
chrome://extensionsで読み込みとデバッグをする
コードを書いたら、パッケージ化せずにローカルで動作確認できます。手順は次のとおりです。
chrome://extensionsを新しいタブで開く- 右上の「デベロッパーモード」を有効にする
- 「パッケージ化されていない拡張機能を読み込む」をクリックし、拡張機能のディレクトリを選択する
manifest.jsonにエラーがあると、この時点で「拡張機能を読み込めませんでした」というダイアログが表示されます。例えばversionキーをversionsのように書き間違えると、「Required value version is missing or invalid」というエラーが出ます。permissionsの値をactiveTabではなくactivetabと小文字で書いた場合、読み込みは通ります。ただし拡張機能の管理ページの「エラー」ボタンに「Permission 'activetab' is unknown or URL pattern is malformed」という警告が記録されます。
コードを編集したら、拡張機能カードの「更新」アイコン(オン/オフの切り替えスイッチの隣にある円形の矢印アイコン)をクリックして反映します。どこまで再読み込みが必要かはコンポーネントによって変わります。
| コンポーネント | 再読み込みの要否 |
|---|---|
| manifest.json | 再読み込みの要否必要 |
| service worker | 再読み込みの要否必要 |
| content script | 再読み込みの要否必要(対象ページの再読み込みも) |
| popup | 再読み込みの要否不要 |
| オプションページ | 再読み込みの要否不要 |
service workerのログを見るには、拡張機能カードの「Inspect views」の横にあるリンクをクリックしてDevToolsを開きます。ここで注意が必要です。公式ドキュメントは、service workerをインスペクトしている間はservice workerがアクティブなまま保たれると説明しています。service workerが終了したときに正しく動くかを確認したい場合は、DevToolsを閉じてから動作を試す必要があります。
service workerが実際にいつ起動・終了するかを確認したいときは、拡張機能IDを控えます。chrome-extension://<拡張機能ID>/manifest.jsonを開き、DevToolsのApplicationパネルからService Workersペインを開きます。ここのstart/stopリンクで、service workerを手動で起動・終了させて挙動を確認できます。
content scriptのエラーは、拡張機能の管理ページの「エラー」ボタンには出ません。実行時エラー・console.warn・console.errorだけが管理ページに記録される仕組みで、content scriptはページの中で動くため、対象ページ自体のDevToolsコンソールを開いて確認します。コンソール上部のコンテキスト切り替えドロップダウン(既定は「top」)から該当する拡張機能を選ぶと、その拡張機能のcontent scriptが出したエラーだけを絞り込めます。
よくあるつまずき
- 常駐前提のコードを書いてしまう: service workerはイベントが無ければ終了します。グローバル変数に状態を溜める実装は、次のイベントで消えている可能性があります。永続化が必要な値は
chrome.storageに保存します - content scriptから直接chrome.tabsを呼ぼうとする: content scriptから直接呼べるAPIは限られています。タブ操作やスクリプト注入が必要な処理は、メッセージでservice worker側に依頼します
sendResponseを非同期で呼んで応答が返らない: リスナーの戻り値でtrueを返し忘れると、非同期処理の完了を待たずにメッセージチャネルが閉じ、応答が届きません- CDNからライブラリを読み込もうとする: MV3はパッケージ外のコード実行を許可しません。使うライブラリは
npm installしてバンドルに含めます - permissionsの値の大文字小文字を間違える:
activeTabのようなキャメルケースの値を小文字化すると、読み込みは通りますが、拡張機能の管理ページの「エラー」ボタンにエラーが記録されます
Chrome拡張機能を自分で作る話ではなく、Claude公式のChrome拡張の使い方を知りたい場合は別記事があります。Claude Chrome拡張機能でできることを参照してください。ここで扱っているのは、自分で拡張機能を開発する側の手順です。
拡張機能を読み込んだブラウザでのコンソールログ取得やスクリーンショット取得を自動化したい場合は、ブラウザ操作の自動化が参考になります。詳しくはChrome DevTools MCPでClaude Codeのパフォーマンス計測を自動化するで扱っています。
まとめ
Manifest V3の拡張機能開発は、manifest.jsonの必須キーを満たすところから始まります。土台になるのは、service worker(常駐しないイベント処理)とcontent script(isolated worldで動くページ内スクリプト)を分離して書くことです。Claude Codeで開発する場合は、CLAUDE.mdにMV3固有の制約を明記しておきます。manifest.jsonを編集するたびに検証スクリプトを走らせるループを組んでおくと、chrome://extensionsで読み込む前の段階でミスを減らせます。まずは最小構成のmanifest.jsonとservice worker 1つを用意し、パッケージ化されていない拡張機能として読み込むところから始めるのが近道です。