Claude Media
Claude CodeでFirefoxアドオンを作る — web-ext lintをhookで回す

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 --version

hookで使う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自動修正の早見表にあります。

この記事を共有:XはてブLinkedIn