Claude CodeでFirefoxアドオンを作る — web-ext lintをhookで回す
Claude Codeの編集直後にweb-ext lintを走らせるPostToolUse hookと、Chrome向けManifest V3を流用したときFirefoxで出る指摘の読み方、web-ext runの分担をまとめます。
Firefoxアドオンのlintをhookに載せると何が変わるか
web-extは、Firefox向けの拡張機能(アドオン)開発を速くするコマンドラインツールです。web-ext lintはmanifestとソースを検査し、web-ext runはFirefoxを起動して拡張を一時的に読み込みます。
Chrome向けに書いたManifest V3の拡張をFirefoxに持ち込むと、動かす前にweb-ext lintが止めてくれる箇所がいくつかあります。これをClaude CodeのPostToolUse hookに載せると、Claudeがファイルを編集するたびに検査が走り、指摘がそのままClaudeに返ります。人がweb-ext runを起動してから気づく種類の誤りを、編集の直後に潰せます。
Chrome側の作り方はClaude CodeでChrome拡張をManifest V3で開発する手順にあります。本記事はそのmanifestをFirefoxで通すところから始めます。
- Chrome向けmanifestにlintをかけると何が出るか
- 指摘を直したmanifestはどう変わるか
- lintをhookにして、Claudeに直させる設定
web-ext runで人が確かめる範囲
Chrome向けmanifestにlintをかけると4件出る
次のmanifestは、Chrome向けのMV3拡張でよくある最小形です。permissionsに実在しない名前を1つ混ぜてあります。
{
"manifest_version": 3,
"name": "Demo",
"version": "1.0.0",
"permissions": ["tabs", "notARealPermission"],
"background": { "service_worker": "background.js" }
}web-ext 10.7.0でweb-ext lint -o jsonを実行すると、エラー2件・警告2件が返りました(空のホームディレクトリで実行した出力です)。
| 種別 | コード | メッセージの要点 |
|---|---|---|
| error | コードBACKGROUND_SERVICE_WORKER_NOFALLBACK | メッセージの要点service_workerを、Firefox互換のscriptsなしで使っている |
| error | コードADDON_ID_REQUIRED | メッセージの要点Manifest V3ではアドオンIDが必須 |
| warning | コードMANIFEST_PERMISSIONS | メッセージの要点notARealPermissionは無効な権限名 |
| warning | コードMISSING_DATA_COLLECTION_PERMISSIONS | メッセージの要点data_collection_permissionsキーがない |
どの指摘も、Chromeに読み込むだけでは見えません。4件のうち3件はChromeで通るmanifestがFirefoxでは通らない種類で、残る1件は権限名の誤記です。
4件を直すと、manifestはこうなる
バックグラウンド処理は、Firefoxがservice_workerをサポートしていない点が出発点です。MDNのbackgroundの説明は、FirefoxはService Workerを使わず、background.scripts(またはbackground.page)でイベントページを起動する、としています。
アドオンIDはbrowser_specific_settings.gecko.idに書きます。MDNは、Manifest V3の拡張をAMO(addons.mozilla.org)経由で配布する署名、または自己配布のための署名には、IDの指定が必須だと説明しています。
data_collection_permissionsは、収集して外部へ送るデータの種類の宣言です。requiredにはnoneか、authenticationInfo、browsingActivity、websiteContentなどの種類を並べます。2025年11月3日以降にAMOへ提出する新しい拡張には、この宣言が必要です。
{
"manifest_version": 3,
"name": "Demo",
"version": "1.0.0",
"permissions": ["tabs"],
"background": { "scripts": ["background.js"] },
"browser_specific_settings": {
"gecko": {
"id": "demo@example.org",
"data_collection_permissions": { "required": ["none"] }
}
}
}この形でweb-ext lintを実行すると、エラー0・警告0でした。
service_workerを残すと警告が1件出る
ChromeとFirefoxで1つのmanifestを共有したいときは、scriptsとservice_workerの両方を書く方法があります。MDNは、Service Workerに対応するブラウザはservice_workerを、イベントページを使うブラウザはscriptsを選ぶと説明しています。
ただしweb-ext 10.7.0のlintは、この共有形に対してBACKGROUND_SERVICE_WORKER_IGNOREDの警告を出しました。Firefoxはservice_workerを無視する、という内容です。エラーではないので、共有形を選ぶ意図があるなら、この警告は想定内です。
方針は2つに分かれます。
- Firefox専用の拡張なら、
scriptsだけにします。警告は出ません - Chromeと共有する拡張なら、この警告をhookの通知から外します(書き方はhookの節の最後にあります)
strict_min_versionを足すと別の警告が出る
strict_min_versionは、拡張が動くFirefoxの最低バージョンです。web-extのコマンド解説によれば、これを設定すると、使っている権限・manifestキー・APIがそのバージョンで使えるかをlintが報告します。
試しに"strict_min_version": "128.0"を足したところ、KEY_FIREFOX_UNSUPPORTED_BY_MIN_VERSIONが出ました。説明文は、data_collection_permissionsが対応したのはFirefox 140で、128はそれより前だという内容です。Android側も同様で、strict_min_versionを140にするとデスクトップ向けの警告は消え、Android向けの警告が残りました(142にすると消えます)。
data_collection_permissionsを宣言するなら、最低バージョンも140以上に上げる必要があります。古いFirefoxにも対応したいときは、この宣言とstrict_min_versionが噛み合わなくなるので、どちらを優先するかをmanifestを書く前に決めます。
web-extを入れて、lintの終了コードを知る
web-extはNodeベースのツールで、brew install web-extかnpm install --global web-extで入ります。コマンドリファレンスのバージョン10の項には、Node.js 22以降が必要と書かれています。プロジェクトに固定するなら、開発依存として入れます。
npm install --save-dev web-ext
npx web-ext --versionhookで使うlintのオプションは、次の4つです。
| オプション | 役割 |
|---|---|
--source-dir, -s | 役割拡張のソースディレクトリ。既定はカレントディレクトリ |
--output, -o | 役割出力形式。jsonかtextで、既定はtext |
--warnings-as-errors, -w | 役割警告があるときも非ゼロで終了する |
--self-hosted | 役割AMO以外で自己配布する拡張として扱い、AMO向けのメッセージを出さない |
終了コードは、実際に動かして確かめました。エラーがあると1、警告だけなら0、警告だけでも-wを付けると1でした。hookの通知に警告も含めたいなら、終了コードに頼らずJSONを読むほうが確実です。
テキスト出力は表形式で、メッセージが幅の狭い列に折り返されて切れます。Claudeに読ませるなら、-o jsonの結果を1行ずつに整えたほうが意味が伝わります。
PostToolUse hookで編集のたびにlintする
hookは、ソースをsrc/に置くプロジェクトを想定します。.claude/settings.jsonに次を書きます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/web-ext-lint.sh",
"args": [],
"timeout": 60
}
]
}
]
}
}PostToolUseの入力はJSONで、編集したファイルの絶対パスがtool_input.file_pathに入ります。スクリプトはsrc/の外のファイルなら何もせず終わり、src/の中ならweb-ext lintを実行します。
mkdir -p .claude/hooks
cat > .claude/hooks/web-ext-lint.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
case "$file_path" in
"$CLAUDE_PROJECT_DIR"/src/*) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 1
json=$(npx --no-install web-ext lint --source-dir src \
--output json 2>/dev/null)
if [ -z "$json" ]; then
echo "web-ext lint が結果を返しませんでした" >&2
exit 1
fi
report=$(echo "$json" | jq -r '
(.errors[] | "error \(.code): \(.message) [\(.file // "-")]"),
(.warnings[] | "warning \(.code): \(.message) [\(.file // "-")]")')
if [ -n "$report" ]; then
echo "$report" >&2
exit 2
fi
exit 0
SH
chmod +x .claude/hooks/web-ext-lint.sh最初のChrome向けmanifestをsrc/manifest.jsonに置いてこのスクリプトを動かすと、stderrには次のように出ます。
error BACKGROUND_SERVICE_WORKER_NOFALLBACK: Unsupported "/background/service_worker" manifest property used without "/background/scripts" property as Firefox-compatible fallback. [manifest.json]
error ADDON_ID_REQUIRED: The add-on ID is required in Manifest Version 3 and above. [manifest.json]
warning MANIFEST_PERMISSIONS: /permissions: Invalid permissions "notARealPermission" at 1. [manifest.json]
warning MISSING_DATA_COLLECTION_PERMISSIONS: The "data_collection_permissions" property is missing. [manifest.json]指摘がなければ何も出力せず、終了コード0です。
設計の要点は3つ
1つ目は、npx --no-installです。web-extが入っていない環境で、hookのたびにネットワークから取りに行く動きを避けます。入っていなければjsonが空になり、スクリプトは終了コード1で、原因が分かる文字列をstderrに出します。終了コード2以外の非ゼロはブロックしない通知になるため、Claudeの作業は止まりません。
2つ目は、終了コード2です。hooks仕様では、PostToolUseで終了コード2を返すと、ツールはすでに実行済みでも、stderrの内容がClaudeに届きます。終了コード0のときのstderrはClaudeに見えません。lintの指摘をClaudeに直させる経路は、この終了コード2だけです。
3つ目は、検査の対象をsrc/に絞る点です。README.mdのようなソース外のファイルを編集しても、lintは走りません。PostToolUseのEdit|Writeは、Bashコマンドが書き換えたファイルには反応しません。sed -iなどでmanifest.jsonを直した場合は、次の編集まで検査が走らないので、完了前に手動でnpx web-ext lint -s srcを1回回す手順をCLAUDE.mdに書いておきます。
Chromeと共有するときは警告を1つ外す
先ほどの共有形(scriptsとservice_workerの併記)を意図して使うなら、BACKGROUND_SERVICE_WORKER_IGNOREDが毎回返ってきてClaudeが直そうとします。スクリプトのjqに除外を足します。
report=$(echo "$json" | jq -r '
(.errors[] | "error \(.code): \(.message) [\(.file // "-")]"),
(.warnings[]
| select(.code != "BACKGROUND_SERVICE_WORKER_IGNORED")
| "warning \(.code): \(.message) [\(.file // "-")]")')除外するコードは、意図したものに限ります。警告をまとめて落とすと、MANIFEST_PERMISSIONSのような本物の誤記まで見えなくなります。
設定をconfigファイルに寄せて、hookとrunを揃える
hookでは--source-dir srcを毎回渡していますが、web-ext runやbuildでも同じ指定が要ります。web-extは、カレントディレクトリのweb-ext-config.mjsを自動で読みます。キーはオプション名からダッシュを外してキャメルケースにしたものです。
export default {
sourceDir: 'src',
ignoreFiles: ['package-lock.json'],
run: {
firefox: 'nightly',
},
};runの下に置いたオプションはrunにだけ効きます。firefoxはFirefoxのバイナリを指定するオプションで、nightlyのような名前でも渡せます。--no-config-discoveryを付けると自動読み込みは止まります。設定ファイルの有無に左右されないよう、hookのスクリプトには--source-dir srcを明示したままにしています。
Claudeに任せる範囲と、人がweb-ext runで確かめる範囲
lintで分かるのは、manifestとソースの静的な誤りです。実際にFirefoxで動くかは、web-ext runで確かめます。
npx web-ext run --source-dir srcこのコマンドはFirefoxを起動し、拡張を一時的に読み込みます。about:debuggingから読み込むのと同じで、権限の確認画面や再起動に関する制限も同じです。ソースを保存すると自動で再読み込みされ、ターミナルでrを押しても再読み込みできます。再読み込みを止めたいときは--no-reloadを付けます。
画面を見て確かめる作業は人の側に残します。Claudeがweb-ext runを起動することはできても、起動したFirefoxの画面を見て判断するのは人です。CLAUDE.mdには、役割分担を書いておくと迷いません。
## Firefoxアドオン開発のルール
- ソースは src/ にある。manifest は Manifest V3
- background は scripts で書く(Firefox は service_worker を使わない)
- アドオン ID は browser_specific_settings.gecko.id に書く
- src/ を編集したら web-ext lint の結果を確認し、error と warning を
すべて直す。意図した警告は、理由を添えて報告する
- manifest.json を Bash で書き換えたら、完了前に
npx web-ext lint -s src を実行する
- web-ext run は人が実行して動作を確認する。Claude は起動しない
- web-ext sign は実行しないここでsignを禁じたのは、AMOのAPIキーを使い、--channel=listedなら提出まで進む操作だからです。ルールを文章で書くだけでなく、permissionsのdenyでも止められます。
{
"permissions": {
"deny": [
"Bash(web-ext sign *)",
"Bash(npx web-ext sign *)"
]
}
}denyルールは*の前までを書いたとおりに照合します。呼び出し方ごとにルールが要るので、npx経由と直接実行の両方を並べています。APIキーのJWT issuerとsecretは、環境変数($AMO_JWT_ISSUER、$AMO_JWT_SECRET)で渡す形がweb-extの説明にあります。キーをmanifestやconfigファイルに書かないよう、CLAUDE.mdにも一行足しておくと安全です。
署名していないzipはFirefoxに入らない
つまずきやすいのが、web-ext buildで作ったzipを、そのままFirefoxにインストールしようとする場面です。署名されていないため、署名がないというエラーになります。web-ext runの一時インストールは、Firefoxを閉じるまで入っている形で、署名の制限には触れません。署名のないzipを入れたいときは、unbranded buildかdevelopment buildを使います。
もう1点、web-ext buildで作ったzipは、署名するかbrowser_specific_settings.gecko.idを入れないとFirefoxで動きません。hookでADDON_ID_REQUIREDを早めに潰しておく意味はここにもあります。
拡張がweb-ext runで動いているのか、署名済みの版なのかは、runtime.onInstalledのdetails.temporaryで判別できます。開発用だけの処理を入れたいときに使えます。
まとめ
Chrome向けに書いたMV3のmanifestは、そのままではFirefoxで通りません。web-ext lintは、静的に分かる食い違いを返してくれます。それをPostToolUse hookで編集のたびに走らせれば、Claudeは自分で直せます。
人が見る部分は、web-ext runでの動作確認と、署名・提出の操作です。lintがエラー0・警告0でも、権限の確認画面やFirefoxでの実際の挙動までは分かりません。
VS Codeのように別のホストに向けた拡張でも、同じ型で検証を組めます。Claude CodeでVS Code拡張を自作するは、雛形からvsceでの公開までを扱っています。言語ごとのlint自動修正の比較はPostToolUse hookのLint自動修正の早見表にあります。