🤖 AIとつくる

AIのブラウザ操作にフォーカスを奪われない環境を作る — WSLg + Xvfb + VNC

(更新: )約4,200字
左に Claude Code で作業中のターミナル、右に pw-peek (TigerVNC) の窓。仮想ディスプレイ :99 で動いているブラウザがブログを表示している様子が窓越しに見えている

毎朝、Claude Code にアプリとブログのアクセス集計を任せています。データ収集の一部は管理画面をブラウザで開く必要があるため、Playwright でのブラウザ操作も Claude Code がやってくれるのですが、ひとつ困ったことがありました。操作のたびにブラウザウィンドウが前面に出てきて、フォーカスを奪われるのです。

AI が裏で作業してくれている間、私は別の作業をしています。文章を打っている途中でいきなり入力先が切り替わるので、タイピングが中断される。それだけではなく、打っていた文字がそのまま Playwright 側のブラウザに入力されてしまいます。うっかり何か操作してしまいそうですし、もしかすると、もうしていたかもしれません。AI と人間が安心して並走できない状態でした。

環境の整理を先にしておくと、Windows 11 + WSL2 で、Claude Code と Playwright は WSL 側で動いています。Playwright が操作するブラウザ (WSL 側の Chromium) は、WSLg (WSL の GUI アプリを Windows のデスクトップに表示する機能) 経由でウィンドウとして表示されます。一方、私が普段の作業に使っているのは Windows 側の Chrome。この2つは別のブラウザで、以降も区別して書きます。

この記事は、フォーカスが奪われる契機を実験で特定して、Playwright のブラウザを仮想ディスプレイに「隠す」ことで解決し、「見たいときだけ覗く」仕組みも作った記録です。調査・実装は Claude Code に任せ、私は方針の決定と、実験の観察役・動作確認を担当しました。

まず、いつ奪われるのかを特定する

対策の前に、フォーカスが奪われる瞬間を特定することにしました。Claude Code に実験スクリプトを作ってもらい、15秒間隔で次の5つの操作を順に実行してもらいます。その間、私は Windows 側の Chrome でひたすらタイピングを続けて、入力が中断された瞬間を観察しました。

フェーズ操作 (Playwright のブラウザ側)フォーカス奪取
1ブラウザを新規起動奪われた
2同一タブでページ遷移奪われない
3新規タブを開く奪われない
4bringToFront() 呼び出し奪われない
5ウィンドウを閉じる奪われない

結果ははっきりしていて、奪われるのは「Playwright のブラウザを新規起動した」瞬間だけでした。ページ遷移や新規タブでは奪われません。契機が1つに絞れたので、対策は「Playwright のブラウザのウィンドウを、起動時から Windows のデスクトップに出さなければよい」ことになります。

対策の選択肢

Claude Code に調べてもらった選択肢と、うちの条件での判断です。

判断
headless モード見送り — 操作先に Google の管理画面が含まれ、headless だと自動化検知に引っかかりやすくなるため headed を維持したい
Windows 側の設定でフォアグラウンド奪取を抑止見送り — WSLg のウィンドウを描画する msrdc.exe 自体がフォーカスを奪う既知バグを抱えており (microsoft/wslg#894)、設定で抑え込む方向は期待薄
Playwright 側のオプション存在しない — 新規ページを自動でアクティブにする挙動は止められないという要望 issue が公式リポジトリに出たまま (microsoft/playwright#16009)
Xvfb (仮想ディスプレイ)採用 — headed のまま、Playwright のブラウザをメモリ上の仮想ディスプレイに出す。Windows のデスクトップには一切現れない

Xvfb は「画面をメモリ上にだけ持つ X サーバ」です。Playwright のブラウザ自身は普通のウィンドウ付きモード (headed) で動いているつもりなので、headless を嫌うサイト対策と両立できます。スクリーンショットも Playwright の API 経由なので仮想ディスプレイのまま撮れて、AI が「スクショを見ながら操作する」流れも損なわれません。

pw-hidden — 起動を仮想ディスプレイに包むラッパー

実装はシェルスクリプト1枚で、核はこの1行です。

exec env -u WAYLAND_DISPLAY xvfb-run -a -s "-screen 0 1920x1080x24" "$@"

これを pw-hidden というコマンド名で置いて、pw-hidden node script.cjs のように Playwright スクリプトを包んで実行します。

ポイントは env -u WAYLAND_DISPLAY です。xvfb-run は仮想ディスプレイを指す DISPLAY を設定してくれますが、WSLg 環境では Wayland 用の WAYLAND_DISPLAY も設定されており、これが残っていると Chromium が Wayland 側 (= WSLg の実画面) に接続してウィンドウを出す経路が残ります。unset して X11 の仮想ディスプレイだけを見せるのが確実でした。

実運用のデータ収集 (Google 系の管理画面 2 種と楽天の管理画面) をこのラッパー経由で動かして検証したところ、ログイン状態は保たれたまま、ウィンドウは一切表示されず、データ取得もスクリーンショットも正常でした。毎朝の集計もこの方式に切り替えて、タイピングを中断されない朝になりました。

見えないのは、それはそれで不安 — pw-peek

解決してみると、今度は別の感覚が出てきました。操作画面がまったく見えないのは、それはそれで不安なのです。ちゃんと動いているか時々見たいし、作業の途中でログインが必要になったら、私がIDとパスワードを入れる必要もあります。

そこで、仮想ディスプレイを「見たいときだけ」覗ける pw-peek というコマンドも作ってもらいました。仕組みは VNC で、x11vnc が仮想ディスプレイの画面を配信し、それを VNC ビューアの窓 (これは WSLg の普通のウィンドウ) で表示します。

  • 覗くだけでなく、マウス・キーボード操作もそのまま効く — 途中でログイン画面に出会っても、覗いてその場で入力できます
  • 窓を閉じると VNC だけが終了し、裏の Playwright 作業はそのまま続く
  • 常駐なしの呼び出し型 — 見ている間だけ数十MBのメモリを使い、閉じれば解放されます (x11vnc の -once オプションで、切断時に自動終了)

「AI の作業を邪魔しないが、人間がいつでも介入できる」という、ちょうどよい距離感になりました。

WSLg ならではの罠が3つ

pw-peek を動かすまでに、WSLg 特有のハマりどころが3つありました。同じ構成を作る方のために書いておきます。

  1. Xvfb のソケットが /tmp/.X11-unix/ に現れない — WSLg ではこのディレクトリがシステム側の読み取り専用マウントになっていて、Xvfb がソケットを置けません (抽象ソケットで動作します)。稼働中の仮想ディスプレイ検出は /tmp/.X99-lock のようなロックファイルで行う必要がありました
  2. x11vnc が Xauthority で接続拒否される — xvfb-run は認証ファイルを発行して Xvfb を起動するので、x11vnc 側にも -auth で同じファイルを渡す必要があります。Xvfb プロセスの引数から拾えます
  3. x11vnc が「Wayland セッションだ」と誤検知して即終了する — 環境変数 WAYLAND_DISPLAY が見えるだけで、X の仮想ディスプレイに接続しようとしているのに終了してしまいます。ここでも unset が必要でした

テストで起きた、ちいさな文字化け

動作確認には「動く時計」を表示するテストページを使いました。pw-peek の窓で時計が動いて見えたので成功だったのですが、ページ内の日本語が全部文字化けしていました。

文字化けした時計テストページ。タイトルと説明文の日本語が記号の羅列になっているが、中央の時計の数字は正常に表示されている

一瞬 VNC を疑いましたが、原因は Claude Code がテストページの data URL に charset=utf-8 を付け忘れていただけで、実際のサイトの日本語表示は問題ありませんでした。自動化の話に限らず、文字化けを見たら経路のどこで化けたかを切り分ける、の小さな実例でした。

運用ルールにして仕組みで忘れない

最後に、この使い分けを Claude Code の全セッション共通の指示ファイル (CLAUDE.md) に書いておきました。

  • AI が操作する headed Playwright は必ず pw-hidden で実行する
  • 私が「見たいから表でやって」と言ったときや、手動ログインが前提のときは、従来どおり WSLg に表示する
  • 実行中に覗きたくなったら pw-peek (私がいつでも勝手に実行してよい。声かけ不要)

私が覚えておくのは「見たければ pw-peek」だけで、あとは AI 側が毎回このルールを読んで使い分けてくれます。

導入してからは、フォーカスを奪われることから解放されて、作業が止まらずに済んでいます。これまでは「Playwright に作業してもらっている間は、掃除や洗濯でもしよう」という時間の使い方だったのが、普通に自分の作業を並行できてしまう。

AI にブラウザ作業を任せる場面はこれからも増えていくと思います。同じように「AI と自分の作業がぶつかって困っている」方の参考になればうれしいです。

参考リンク

公式・一次情報

  • microsoft/playwright#16009 — 新規ページが自動でフォーカスされる挙動を止めたいという要望 issue。抑止オプションは提供されていません
  • microsoft/wslg#894 — WSLg の描画プロセス msrdc.exe がフォーカスを奪う既知バグの報告 (修正予定ラベルのまま Open)

同じ問題に取り組まれた方の記事

  • Playwrightの画面乗っ取り問題をheadlessモードで解決する (note) — headless モードの採用で解決されています。読んで最初に思ったのは「headless でよかったやん!」でした。ただうちの場合は、Claude Code いわく headless だと自動化検知に引っかかりやすいとのことで headed のままにしています (作業は全部お任せしたので、そこは信じています)。そういえば「headless はどう?」と聞かれた記憶がない…と思って読み返したら、対策の比較表にちゃんと1行ありました。読み流していたのは私でした