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層構成を前提にコードを組み立てます。Manifest V3やChrome Web Storeの公開まわりのガイドをスキルとして渡す方法は、modern-web-guidanceプラグインの記事にあります。
MV3が目指すのは、プライバシー・セキュリティ・パフォーマンスの改善です。この移行は数年がかりの取り組みで、実務で押さえるべき変更点は次の3つです。
MV2から変わる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・name・versionは拡張機能プラットフォームの必須キーです。descriptionとiconsは、Chromeウェブストアで必須になるキーです。ローカルで読み込むだけなら、後者の2つが無くても動きます。
{
"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 | 役割拡張機能の説明とアイコン | 必須/任意Chromeウェブストアで必須(公開時) |
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と組み合わせます。Firefoxはservice workerを使わないため、同じ拡張を持ち込むときの指摘はClaude CodeでFirefoxアドオンを作る手順で確認できます。
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を呼ぶ設計にします。外部サーバーのデータをCORS制約を受けずに取得したい場合は、対象URLをhost_permissionsに宣言したうえで、service worker側でfetch()します。
Claude Codeでブラウザ・OS向けの拡張機能を作る手順は他にも扱っています。Claude CodeでFigmaプラグインを開発する手順やClaude CodeでRaycast拡張を作る手順も、本体とUI/ページを分離して書く設計は共通しています。
content scriptをどの方法でページに入れるか
manifest.jsonのcontent_scriptsに書くのは、注入方法の1つにすぎません。content scriptには静的宣言・動的宣言・プログラムによる注入の3つがあります。対象ページが決まっているかどうかで選びます。
注入方法の使い分け
静的宣言
manifest.jsonのcontent_scriptsにmatchesとjs・cssを書きます。既知のページ群で自動実行したいときの方法です。
動的宣言・プログラム注入
動的宣言はservice workerから実行時に登録します。プログラム注入は、タブのクリックなどをきっかけにscripting.executeScript()で入れます。
静的宣言には細かい指定があります。run_atの既定値はdocument_idleで、DOMの構築が終わったあと、画像やフレームの読み込みが終わる前後に注入されます。all_framesをtrueにすると、最上位フレームだけでなく一致するすべてのフレームに入ります。除外にはexclude_matchesを使います。
プログラム注入では、funcに渡した関数がコピーとして対象ページに送られます。関数の外側の変数を参照するとReferenceErrorになるので、必要な値は引数で渡します。
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で読み込むまで気づきにくいため、編集のたびに機械的に検証するループを組んでおくと手戻りが減ります。最小構成なら次のようなチェックスクリプトで十分です。descriptionとiconsの有無も見るのは、ウェブストアへの公開を見据えているためです。
scripts/check-manifest.js:
const fs = require("fs");
const manifest = JSON.parse(fs.readFileSync("manifest.json", "utf8"));
const errors = [];
if (manifest.manifest_version !== 3) errors.push("manifest_version must be 3");
// description と icons はウェブストア公開時に必須(ローカル読み込みだけなら不要)
for (const key of ["name", "version", "description", "icons"]) {
if (!(key in manifest)) errors.push(`missing key: ${key}`);
}
const sw = manifest.background?.service_worker;
if (sw && !fs.existsSync(sw)) errors.push(`service_worker not found: ${sw}`);
for (const cs of manifest.content_scripts ?? []) {
if (!cs.matches?.length) errors.push("content_scripts entry has no matches");
for (const f of [...(cs.js ?? []), ...(cs.css ?? [])]) {
if (!fs.existsSync(f)) errors.push(`content script file not found: ${f}`);
}
}
if (errors.length) {
console.error(errors.join("\n"));
process.exit(1);
}
console.log("manifest.json ok");このスクリプトはエラーを1件で止めず、まとめて出力します。MV2のまま、versionのタイプミス、matchesなし、ファイル未作成のmanifest.jsonを渡すと、Node.js 20で次のように出ます。
$ node scripts/check-manifest.js
manifest_version must be 3
missing key: version
service_worker not found: service-worker.js
content_scripts entry has no matches
content script file not found: content-script.jsmanifest_versionとversionを直し、service-worker.jsとcontent-script.jsを作り、matchesを足すとmanifest.json okで終了コード0になります。ファイルの有無までここで見ておけば、chrome://extensionsの読み込みダイアログまで待たずに済みます。JSONの括弧が欠けた場合はJSON.parseがSyntaxErrorで落ちるので、そのときはエラー位置を見て直すようClaude Codeに伝えます。
これを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で読み込みとデバッグをする
コードを書いたら、パッケージ化せずにローカルで動作確認できます。手順は次のとおりです。
パッケージ化せずに読み込む
- 1
拡張機能の管理ページを開く
chrome://extensionsを新しいタブで開きます。 - 2
デベロッパーモードを有効にする
右上のスイッチをオンにします。
- 3
ディレクトリを選ぶ
「パッケージ化されていない拡張機能を読み込む」をクリックし、
manifest.jsonがあるディレクトリを選びます。
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が出したエラーだけを絞り込めます。
よくあるつまずき
return trueをreturn 1のような値で済ませる: 非同期で応答するには、リスナーから返すのはtrueというリテラルです。truthyな値では足りず、sendResponseを引数なしで呼ぶと、受け取る側にはnullが届きます- devtoolsページがある拡張でPromise応答が効かない: devtoolsページを備えた拡張では、Chrome 148のPromise応答が有効でも使えません。
return trueは有効かどうかにかかわらず動くので、DevToolsを拡張する場合はこちらで書きます - 動的登録したcontent scriptの実行順を取り違える: 同じ読み込み段階では、静的宣言のcontent scriptが先に注入されます。動的登録などで入れたものはそのあとです。順序に依存する処理は、静的宣言のほうに置きます
Chrome拡張機能を自分で作る話ではなく、Claude公式のChrome拡張の使い方を知りたい場合は別記事があります。Claude Chrome拡張機能でできることを参照してください。ここで扱っているのは、自分で拡張機能を開発する側の手順です。
拡張機能を読み込んだブラウザでのコンソールログ取得やスクリーンショット取得を自動化したい場合は、ブラウザ操作の自動化が参考になります。詳しくはChrome DevTools MCPでClaude Codeのパフォーマンス計測を自動化するで扱っています。Excel・Word向けのアドインなら、manifest検証とサイドロードの手順をClaude CodeでOfficeアドインを開発する手順にまとめています。
まとめ
Manifest V3の拡張機能は、manifest.jsonのmanifest_version・name・versionと、service workerとcontent scriptの役割分担が決まれば形になります。Claude Codeには、この分担とCDN不使用をCLAUDE.mdで先に伝え、manifest.jsonの検証をhooksに載せておくと、読み込み前に誤りを見つけられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- developer.chrome.com/docs/extensions/get-started
- developer.chrome.com/docs/extensions/develop/concepts/service-workers
- developer.chrome.com/docs/extensions/develop/concepts/content-scripts
- developer.chrome.com/docs/extensions/develop/concepts/messaging
- developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3
- developer.chrome.com/docs/extensions/reference/manifest
- developer.chrome.com/docs/extensions/get-started/tutorial/hello-world
- developer.chrome.com/docs/extensions/get-started/tutorial/debug