Chrome拡張のクリック領域が行全体に広がるバグと、__pycache__ で拡張が無効化される罠

開発chrome-extension-x

リポストを押したら、動画のダウンロードが走った

X のタイムラインで、動画付きの投稿にリポストを押した。 動画のダウンロードが始まった。

自分で入れた拡張が、押してもいないボタンの仕事をしている。 ダウンロードボタンは右端のブックマークのすぐ左に1個だけ置いたはずで、指が触れたのはそこから離れたリポストの位置だった。 だとすれば当たりは1つしかない。 ボタンのクリック判定領域が、置いた場所を越えて広がっている。

スクリーンショットを撮り、その見立てと一緒に Claude Code へ渡した。 ボタン生成まわりのコードを読ませ、実機で領域を実測させる。

クリック判定はどこから広がっていたか

読ませてみると、ボタンの外枠には X のハッシュ化クラス(css-175oi2r など)をそのまま流用していた。 中には X のホバー用オーバーレイ div も1枚借りていた。 position: absolute; inset: 0; margin: -8px を当てて「ボタンより一回り大きい当たり判定」を作る部品で、外枠が position: relative であることを前提にしている。

その前提が、X 側のリリースで外れていた。 ベースのクラス名が css-175oi2r から css-g5y9jx へ変わり、借りていた外枠は position: static に戻る。 absolute の基準は最も近い配置済みの祖先へ移り、オーバーレイはボタンの周囲8pxではなく、アクション行そのものを覆った。 リポストのクリックは、そこで横取りされていた。

方針は、借りるのをやめることだった。 ハッシュ化クラスへの依存を捨て、オーバーレイの div を消し、外枠を自前のスタイルで閉じさせた。

button.className = "twitter-video-downloader-button";
button.style.position = "relative";
button.style.display = "flex";
// padding 8px + 20px の SVG だけで枠を決める

直したはずのボタンが、丸ごと消えた

拡張をリロードして、X のタブを読み直した。 ボタンが無い。

消えたのはダウンロードボタンだけではなかった。 背景を濃いグレーに置き換える CSS も、Spaces 用のボタンも、まとめて画面から落ちていた。

いったんは自分の修正を疑ったが、消え方が広すぎる。 ボタンを1つ作る関数を書き替えて、別機能の CSS まで巻き添えにする筋道はない。 拡張そのものが動いていないのではないかと見当をつけて、状態を調べさせた。

拡張は無効化されていた。 disableReasons.reloading: true を抱えたまま、DISABLED から戻ってこない。 runtime のエラー一覧には Unable to download all specified images. の1行しか出ておらず、これは今回と関係がなかった。

一覧ではなくロードエラーのダイアログを開かせたら、本文にそのまま書いてあった。

Cannot load extension with file or directory name __pycache__.
Filenames starting with "_" are reserved for use by the system.

拡張フォルダの直下に __pycache__/native_host.cpython-311.pyc が居座っていた。 生成されたのは6日前の 2026-08-13 である。 Chrome は拡張ルートの直下に _ で始まる名前があるとロードを拒否する(_locales_metadata だけが例外になる)。 しかもチェックがかかるのはトップレベルだけで、サブディレクトリの奥にある __pycache__ は何の影響も及ぼさない。

厄介なのは、この .pyc.gitignore 済みだった点だ。 git status は何も言わない。 置かれてから6日たった今日、リロードを押して踏んだだけだった。

rm -rf __pycache__ のあとダイアログの「再読み込み」を押させると、ENABLED に復帰した。 消えていた3つの機能も一緒に戻ってきた。

復帰した拡張で、あらためてクリック領域を実測させる。 ダウンロードボタンが拾うのは本体の 36×36px だけになり、リポストの位置を押しても拡張には届かなくなった。 ここでようやく、最初の症状の裏が取れた。

再発防止として残したもの

次に同じところで止まったときのために、2つ残させた。

1つは拡張リロードのスキルへの追記で、_ 始まりの名前が拡張ルート直下にあるとロードが拒否されるという注意点を入れた。 もう1つは issue ファイルで、症状とエラー文、rm -rf __pycache__ での復帰までを書かせた。

書き足したかったのは、原因そのものより読む順番のほうだった。 拡張が消えたときは runtime のエラー一覧を眺めても答えが出ない。 今回の犯人は、その一覧に1行も出ていなかった。

未コミットの変更をどの粒度で切るか

content.js と issue ファイルを 354a0cd でコミットさせた。 学習ゲートは、理由を付けてスキップさせた。

そのとき「元からあった native_host.* の未コミット変更は残します」と報告が来た。 どこの話なのか分からなかったので、聞き返した。 複数のリポジトリを行き来していると、こういう「元からあった変更」がどのディレクトリのものか見失う。 返ってきた答えは、拡張リポジトリ自身の native_host.pynative_host_powershell.bat だった。

自分のディレクトリの話なら片付けたい。 ただし1コミットに丸めるのは違うので、中身を読んで粒度を決めろと指示した。 返ってきたのは native_host.py の9ハンクのうち8番目だけを切り離す案で、全体で3コミットになった。

コミット内容
823217d起動バッチを PATH 非依存にし、PowerShell ラッパーを廃止
1f09ab2yt-dlp を PATH に頼らず解決し、起動時と失敗時のログを追加
549040cフォルダを開くときの explorer の出力を捕捉

前の2つは似て見えて層が違う。 Chrome から起動される native host は、Chrome 起動時点の PATH をそのまま引き継ぐ。 だから Python を PATH に登録するより前から Chrome が動いていると、python が見つからない。 そのとき拡張側に出るのは native messaging host との通信エラーだけで、何も分からない。 バッチ側は python.exe を絶対パスで解決する。 Python 側は shutil.which、同じ Python の Scripts、別バージョンの Python、最後に python -m yt_dlp の順で yt-dlp を探す。

3つ目を別にしたのは、原因が同じで対象が違うからだ。 native messaging は4バイトの長さと JSON でフレームを組むため、stdout に1バイトでも余計な出力が混ざると壊れる。 PowerShell ラッパーを捨てたのも、explorer の出力を capture_output=True で吸い込んだのも、同じ理由で別の穴を塞いだ話になる。

プッシュまで終えて、作業ツリーはクリーンになり、origin との差分も消えた。

学び

  • ホスト側のハッシュ化クラスに乗ると、相手のリリースがそのままこちらの不具合になる。今回はクラス名が1つ変わっただけで、当たり判定がアクション行の全体まで広がった
  • 「消えた」の原因が、直前に自分が触った箇所とは限らない。消えた範囲と、修正の影響が届く範囲を比べると早く切り分けられる
  • Chrome 拡張のリロードが通らないときは、runtime のエラー一覧よりロードエラーのダイアログを先に読む
  • .gitignore 済みのファイルは git status に出ない。拡張フォルダの直下だけは、git とは別の目で見る必要がある

__pycache__ が8月13日に生えた理由は、まだ分かっていない。 native_host.py はスクリプトとして直接叩くので、普段は .pyc を残さない。 どこかから import native_host した覚えがあるはずなのに、それがいつだったのか思い出せていない。