Chrome拡張のクリック領域が行全体に広がるバグと、__pycache__ で拡張が無効化される罠
リポストを押したら、動画のダウンロードが走った
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.py と native_host_powershell.bat だった。
自分のディレクトリの話なら片付けたい。
ただし1コミットに丸めるのは違うので、中身を読んで粒度を決めろと指示した。
返ってきたのは native_host.py の9ハンクのうち8番目だけを切り離す案で、全体で3コミットになった。
| コミット | 内容 |
|---|---|
823217d | 起動バッチを PATH 非依存にし、PowerShell ラッパーを廃止 |
1f09ab2 | yt-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 した覚えがあるはずなのに、それがいつだったのか思い出せていない。