拡張機能の自動化例では、次の2つのフラグがよく使われます。
--disable-extensions-except=/path/to/ext --load-extension=/path/to/ext
古い手順では、Puppeteer や Playwright から、すでにインストール済みの Chrome に向けてこの2つのフラグを渡します。
今回検証した標準の Google Chrome 150 ビルドでは、ブラウザ自体はコマンドラインエラーなしで起動しましたが、拡張機能サービスはこの2つのフラグをどちらも無視しました。自動化接続はいつも通り確立される一方で拡張機能は見当たらず、あとから出た症状はこのハーネスでのセレクタータイムアウトでした。
Chrome はその拒否理由を1行でログに出していましたが、stderr ログを有効にしたあとでないと見えませんでした。
主な発見は、ブラウザビルドの比較にあります。後半の2セクションははっきりハーネスのメモで、1つは拡張機能が引き起こすダウンロード、もう1つは file:// フィクスチャ上でのこのコマンドライン導入経路についてです。Thunderbit 自体も Chrome 拡張機能なので、この問題には直接関係があります。ただし、Thunderbit はテスト対象ではありません。
その一行
--enable-logging=stderr を追加して、標準の Chrome をそのフラグ付きで起動します。
公式参照: Chromium extension service source.
WARNING:chrome/browser/extensions/extension_service.cc:442]
--disable-extensions-except is not allowed in Google Chrome, ignoring.
--load-extension だけを渡すと、同じファイル内の別の行から、内容の違う警告が出ます。
WARNING:chrome/browser/extensions/extension_service.cc:420]
--load-extension is not allowed in Google Chrome, ignoring.
Chrome for Testing は、同じフラグを与えても、どちらの警告も出しません。
「Google Chrome では許可されていません。」 という警告文は、今回観測された拒否がブランド付きビルドのルールに由来している、という見立てをかなり強く裏づけます。標準の Google Chrome 150 ビルドがこのフラグを無視したこと、そしてその発生箇所を示していることは確かです。ただし、バージョンそのものが無関係だと独立に証明するものでもなければ、実装上の判定条件を明かすものでもありません。
以下は、その確認と結果です。
挙動で確認する
このテスト用に作った最小構成の MV3 拡張機能は、コンテンツスクリプト、ポップアップ、メッセージ往復、DOM 抽出、そしてローカルの3製品フィクスチャに対する chrome.downloads エクスポートを使います。ハーネスは6つのチェックを記録します。サービスワーカー登録、拡張機能マーカーに ID が付くこと、コンテンツスクリプトのマーカー一致、ポップアップボタンの表示、ポップアップから3行返ること、キャプチャした CSV に期待行が含まれることです。別途、サービスワーカーとページマーカーの独立したシグナルが一致するかも確認します。
3つのビルドは、同じ明示的な拡張機能フラグ、同じ拡張機能ディレクトリ、同じ HTTP フィクスチャ、ヘッド付きの永続コンテキストモード、そして各アームごとの新規プロファイルを使いました。標準アームは channel: 'chrome' 経由で、成功した2つのアームは明示的な実行ファイルパスを使っています。バージョン文字列は CDP 経由で読み戻しました。
| ビルド | ブラウザが報告したバージョン | 拡張機能サービスワーカー | コンテンツスクリプト注入 | 6項目の完全実行 |
|---|---|---|---|---|
| Chrome for Testing | Chrome/149.0.7827.55 | ✅ | ✅ | 6/6 成功 |
| 標準の Google Chrome | Chrome/150.0.7871.187 | ❌ 登録されず | ❌ | 0段階目で失敗 |
| Chrome for Testing | Chrome/151.0.7922.10 | ✅ | ✅ | 6/6 成功 |
生の要約: Chrome for Testing 149、標準 Chrome 150、Chrome for Testing 151、stderr 警告 を参照。
Chrome for Testing 149 をあえて入れているのは、比較対象の標準ビルドより 古い からです。もし機能がバージョン更新で削除されたのなら、古いビルドが動作側にいて、失敗するのは最新ビルドであるはずです。ところが実際には、失敗したビルドは動く2つのビルドの間に挟まれています。これは単純なバージョン進行では説明できません。
ただし、この表だけでゲートがビルド依存だと証明したことにはなりません。その理由はきちんと言う必要があります。 失敗したセルは、同時に唯一の標準ビルドであり、唯一の 150 系でもあります。つまり、この設計ではビルドとバージョンが完全に絡み合っています。3つのアームで排除できるのは 単調な削除 だけで、「150 で劣化し、151 で復活した」という可能性までは否定できません。挙動だけでそれを示すには、このマシンでは作れないセル、つまり Chrome for Testing 150、あるいは別バージョンのブランド付きビルドが必要です。
警告文が Google Chrome を明示しているため、ブランド付きビルドの解釈はさらに強まりますが、3アームの挙動が排除できるのは単純な単調削除だけです。バージョンに依存しない仕組みを証明するには、同一バージョンでのクロスビルド比較、あるいはソースや設定の引用がまだ必要です。
リンク先の各ビルドアーティファクトには、各アームの要約実行結果が入っています。この本文では、追加起動回数についての行ごとのマトリクスは公開していないので、その回数を独立した証拠としては使っていません。
1つのシグナルだけでは「読み込まれなかった」とは言えない
このテストの最初の版では、拡張機能が読み込まれたかどうかを、コンテンツスクリプトがページに書き込むマーカーだけで判断していました。しかも、それがコンテンツスクリプト実行の判定にも使われていました。1つの観測値を、2つの測定に流用していたわけです。マーカーが見えない場合、それが「拡張機能が一度も読み込まれていない」のか、「読み込まれたけれどコンテンツスクリプトが注入されなかった」のかは区別できません。しかも、この2つは直し方がまったく違います。
MV3 拡張機能はバックグラウンドサービスワーカーで動き、Playwright はサービスワーカーを直接扱えます。これは独立したシグナルです。ページには触れないので、注入の成否と混同されません。今のテストでは、この2つを読み取って一致するかを確認しています。
3回の実行では、どれも一致しました。標準 Chrome では サービスワーカーが一度も登録されませんでした。これはかなり強い主張です。Chrome for Testing の2ビルドでは、ページを開く前に chrome-extension://<id>/background.js でワーカーが立ち上がりました。
すでに持っているかもしれない Chrome for Testing を使う
成功した2アームでは、149.0.7827.55 と 151.0.7922.10 を報告する Chrome for Testing の実行ファイルを明示的に使いました。Playwright の executablePath には、channel: 'chrome' で標準 Chrome を解決するのではなく、固定した実行ファイルを指定します。npx playwright install chromium で入るのは Playwright 管理の Chromium ビルドで、これも自動化には役立ちますが、今回比較した2つの Chrome for Testing アームと同じ配布ラベルではなく、比較の第4アームでもありませんでした。
公式参照: Chrome for Testing announcement.
関連レビュー: Chrome extension permissions audit.
この不具合に限らない副次的な利点もあります。標準 Chrome は裏で自動更新されるので、今日通るテストが火曜日には何のコミットとも関係なく落ちることがあります。Chrome for Testing は固定できます。3か月後にも結果に意味を持たせたいなら、この違いは便利さ以上に重要です。
ハーネスのメモ: 拡張機能のエクスポートを捕まえる
スクレイピング用拡張機能では、エクスポートこそ本番です。そこで項目が落ち、エンコーディングが壊れ、ネストしたデータがうまく平坦化されます。そして、そこには文書化されていない点が2つあり、どちらも厄介です。
| エクスポート実行で記録した内容 | 値 |
|---|---|
playwright_download_event_fired | false |
suggestedFilename | null |
| 拡張機能が要求したファイル名 | probe-export.csv |
| ディレクトリに保存されたファイル名 | download.csv |
| ファイル内容 | ヘッダーと3行すべてがそのまま残る |
この MV3 / Playwright 1.56.0 ハーネスでは、chrome.downloads は Playwright の download イベントを発火しませんでした。 拡張機能が API 経由で CSV を出力しても、waitForEvent('download') は終わりませんでした。ハーネスは CDP セッションを開き、ダウンロード動作を明示的に設定し、出力ディレクトリを読み取ることでファイルを取得しました。
const cdp = await ctx.newCDPSession(page);
await cdp.send('Browser.setDownloadBehavior', {
behavior: 'allow', downloadPath: DIR, eventsEnabled: true,
});
同じハーネスでは、この CDP 取得経路は要求されたファイル名を保持しませんでした。 ヘッダーと3行はそのまま残りましたが、probe-export.csv は download.csv として保存されました。これは、今回テストしたブラウザビルドと構成で観測された挙動であって、すべての拡張機能ダウンロードに共通する文書上の不変条件ではありません。ファイル名だけでなく、中身でも確認してください。
ハーネスのメモ: この導入経路における file:// の挙動
この部分を確認している途中で、広く繰り返されていた主張の1つが誤りだと分かりました。そしてそれは、この同じ記事の初稿にも入っていました。つまり、「拡張機能にはデフォルトで file アクセスがないので、コンテンツスクリプトは file:// ページでは動かない」という主張です。
Chrome for Testing の2ビルドで測定したところ、拡張機能をコマンドラインから読み込み、そのまま file:///…/fixture/index.html に移動すると、コンテンツスクリプトは普通に注入されました。 マーカーは存在し、拡張機能 ID も正しく、どちらのビルドでも同じでした。コマンドラインで読み込んだ unpacked 拡張機能は file アクセスを持ちます。人々が覚えている「file アクセスを許可する」トグルは、別の導入経路向けのものです。
それでも、フィクスチャは HTTP で配信するほうが基本として望ましいです。file:// ページは実際の対象とはかなり違うからです。ただし、それは現実に近いという実務上の理由であって、技術的に必須だからではありません。そして、その理由としてよく挙げられる仕組みは誤っています。
これで証明できていないこと
- ゲートの実装は、警告文から読み取れる範囲に限られます。 Chrome は、このビルドではこれらのフラグが許可されず、しかもソースファイル名まで示しています。しかし、それがビルド設定なのか、ポリシーの配線なのか、あるいは別要因なのかまではソースから読み出していません。
- これは1台のマシンでの結果です。 macOS / arm64、標準の1つのパッチ版、Chrome for Testing の2ビルドです。Chrome は変化が速いので、これは引用するより再確認したほうがいい内容です。
- 対象は
--load-extension経路だけです。 パッケージ化された.crxのインストール、開発者モードでの読み込み、企業向けの許可リストポリシーはテストしていません。ここから「標準 Chrome では拡張機能が動かない」とは言えません。 - 拡張機能は目的特化のスタブです。 スクレイパー拡張機能が使う仕組みはひと通り試していますが、実際の拡張機能はもっと大きく、スタブでは起きない失敗もあります。
ここに至るまでに自分が誤っていたこと
この2つの誤りは、結果がそれらしく見えるとレビューをすり抜ける類のものなので、はっきり書いておく価値があります。
最初の版では、この失敗は静かに起き、ログ行は存在せず、仕組みは知りようがない、つまり仕組みを知っていると言う人は推測しているだけだ、と書いていました。ところが実際には、フラグを1つ変えるだけで見えるようになり、Chrome はずっと WARNING で出力していました。「見つけられなかった」が、「見つけようがない」にすり替わっていたのです。
2つ目は上の file:// の主張です。メモからそのまま持ち込んで、テスト前に確信ありきの仕組み説明まで添えてしまっていました。1回の実行でそれは否定されました。
どちらも構図は同じです。誰も疑わなさそうなもっともらしい主張を、そのまま確認せずに持ち越してしまうこと。直すべきは文章に対する慎重さを増やすことではなく、何を断言してよいかのルールです。実行結果にたどれない主張は、そのまま公開しない。
ローカルハーネスで使ったコマンド
バージョン付きの probe、拡張機能スタブ、フィクスチャ、および生の要約はここでリンクしていますが、まだ独立再現可能な公開バンドルにはなっていません。Chrome for Testing 149 と 151 の正確なダウンロード元とチェックサムは記事中に記録されておらず、下の実行ファイルパスもローカル入力です。完全な比較を独立再現可能として示す前に、それらのブラウザソースと安定したリポジトリコミットを公開してください。
関連レビュー: Playwright review.
cd harness
npm install playwright@1.56.0
npx playwright install chromium # Playwright Chromium。下の2つの CfT アームとは別
(cd fixture && python3 -m http.server 8731 &)
SP=$(pwd) OUT=cft-149.json LABEL=cft-149 EXE="<Chrome for Testing 149>" node probe_v2.mjs
SP=$(pwd) OUT=stock-150.json LABEL=stock-150 CHANNEL=chrome node probe_v2.mjs
SP=$(pwd) OUT=cft-151.json LABEL=cft-151 EXE="<Chrome for Testing 151>" node probe_v2.mjs
3つのアームすべてで同じスクリプトと明示的な拡張機能引数を使いましたが、実行ファイル解決は異なります。標準 Chrome では CHANNEL=chrome、2つの Chrome for Testing バイナリでは EXE を使いました。各出力ファイル中のバージョンは、ラベルではなくブラウザから読み取ったものです。
警告行を確認するだけなら、ハーネスは不要です。
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir=/tmp/p --enable-logging=stderr \
--load-extension=/path/to/ext about:blank 2>&1 | grep "not allowed"
2026-07-28 時点。
結論と注意点
今回の検証マトリクスでは、標準の Google Chrome 150 は --disable-extensions-except と --load-extension を拒否し、一方で Chrome for Testing 149 と 151 は拡張機能を読み込みました。Chrome は拒否を WARNING で記録し、文言は "--load-extension is not allowed in Google Chrome, ignoring." でした。この表現はブランド付きビルドの解釈を後押ししますし、3点のバージョン挟み込みは単純な単調削除を否定します。ただし、同一バージョンでのクロスビルドアームか、ソース/設定の証拠がない限り、ビルドとバージョンはまだ切り分けられていません。
拡張機能のハーネスを固定するなら、バージョンを明示したブラウザ実行ファイルを使い、サービスワーカーとコンテンツスクリプトのマーカーを確認してください。今回の Playwright 1.56.0 環境では、拡張機能のエクスポートは CDP によるディレクトリ取得が必要で、保存ファイル名も別物になりました。コマンドラインで読み込んだスタブは、テストした file:// フィクスチャでも注入されましたが、他の導入経路は未検証です。
Web データ抽出に Thunderbit を試す Get Started Free
FAQ
--load-extension は Chrome から完全に削除されたのですか?
いいえ。Chrome for Testing 149.0.7827.55 と 151.0.7922.10 はどちらも、このフラグで unpacked MV3 スタブを読み込み、6項目の評価チェックを完了しました。標準の Google Chrome 150.0.7871.187 はこれを拒否し、"--load-extension is not allowed in Google Chrome, ignoring" と記録しました。これはブランド付きビルドのゲートを示唆しますが、証明はしていません。挙動としては単純な単調削除では説明できず、Chrome 150 固有の回帰が 151 で戻った可能性も、この3アーム設計とは矛盾しません。
なぜエラーが見えないのですか? また、「一度も読み込まれていない」のか「読み込まれたが静かに失敗した」のか、どう見分ければいいのですか?
警告は通常の詳細度では表示されません。--enable-logging=stderr を付けて起動するとすぐ出ます。付けない場合、Chrome 自体は普通に立ち上がりますが、拡張機能サービスはフラグを無視し、ハーネスで最初に見える症状はセレクタータイムアウトです。「一度も読み込まれていない」ことと注入失敗を分けるには、2つの独立したシグナルを使ってください。MV3 のバックグラウンドサービスワーカーと、対象 DOM 内のコンテンツスクリプトマーカーです。今回の標準 Chrome アームでは両方とも出ず、Chrome for Testing の2アームでは両方出ました。
拡張機能の自動化には何を使うべきですか?
成功したアームでは、executablePath で固定した Chrome for Testing の実行ファイルを使いました。Playwright 管理の Chromium も別候補にはなりますが、今回のアームではなく、解決された実行ファイルを確認せずに同じ配布物だとは言うべきではありません。
拡張機能がファイルを出力しても、なぜ waitForEvent('download') は終わらないのですか?
この MV3 / Playwright 1.56.0 構成では、そのイベントは発火せず、推奨ファイル名も返りませんでした。ハーネスは CDP セッションを開き、Browser.setDownloadBehavior で保存先ディレクトリを指定し、その後ディスクからファイルを読みました。今回の実行では CSV のバイト列は無事でしたが、probe-export.csv は download.csv として保存されました。より広い拡張機能・ブラウザの組み合わせは未検証です。
これは何を言っていないのですか — file:// URL について、そして標準 Chrome 全般について?
2つの境界を、逆方向に置いています。コマンドラインで読み込んだ拡張機能では、file:// URL 上でもコンテンツスクリプトは動作します。これは Chrome for Testing の両ビルドで確認済みで、スクリプトは通常どおり注入され、拡張機能 ID も正しく報告されました。したがって、「拡張機能にはデフォルトで file アクセスがないから動かない」というよくある主張は、この導入経路では誤りです。HTTP でフィクスチャを配信するほうが実運用に近いので推奨されますが、file:// が注入を妨げるからではありません。逆方向については、ここから標準 Chrome が拡張機能を動かせないとは言えません。測定したのは --load-extension のコマンドライン経路だけです。パッケージ化された .crx のインストール、開発者モード読み込み、企業向けの許可リストポリシーは測っておらず、それらについては何も主張しません。対象範囲は、自動化チュートリアルが使うよう指示する、その1組のフラグです。


