0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WSL2からChromeを自動操作するdaemonを作った話

0
Last updated at Posted at 2026-03-19

Claude Codeを始めたばかりの方へ: この記事はやや上級者向けです。WSL2(Windows上のLinux環境)からブラウザを自動操作する仕組みを作った話です。Claude Codeの基本的な使い方とは別のトピックなので、興味がある方だけどうぞ。

やりたいこと

WSL2上のClaude CodeからWindows側のChromeを操作したい。ページを開く、ボタンを押す、スクショを撮る。自律AIループのブラウザ操作基盤として使う。

問題: WSL2からChromeに到達できない

:::details 初心者向け: CDPとは
CDP(Chrome DevTools Protocol)は、プログラムからChromeブラウザを遠隔操作するための仕組みです。ブラウザの開発者ツール(F12で開くあれ)が内部で使っているのと同じ技術で、「このページを開け」「このボタンを押せ」「スクリーンショットを撮れ」といった命令をプログラムから送れます。
:::

Chrome DevTools Protocol(CDP)はWebSocket経由でブラウザを操作できる。:::details 初心者向け: WebSocketとは
WebSocketは、ブラウザとサーバーが常時接続して双方向にデータをやり取りする通信方式です。通常のHTTPが「手紙のやり取り」だとすると、WebSocketは「電話回線」のようなもの。一度つなげば、いつでもリアルタイムにやり取りできます。
:::

--remote-debugging-port=9222でChromeを起動すると、localhost:9222にCDPエンドポイントが立つ。

ただしWSL2では動かない。

# WSL2から実行 → 失敗
$ curl http://localhost:9222/json/version
curl: (7) Failed to connect to localhost port 9222: Connection refused

WSL2とWindowsは別のネットワーク名前空間(同じPCの中にいるのに、別々の部屋にいるようなもの)。WSL2のlocalhostはWindows側に到達しない。--remote-debugging-address=0.0.0.0を付けても、Chromeは127.0.0.1にしかバインドしない(netstat -an | findstr :9222で確認済み)。

# Windows側(PowerShell)から実行 → 成功
PS> Invoke-RestMethod -Uri "http://localhost:9222/json/version"
Browser : Chrome/145.0.7632.160

PowerShellはWindows側で動くからChromeに到達できる。

解決策: Unix socket daemon + PowerShell fallback

:::details 初心者向け: Unix socketとdaemonとは
Unix socketは、同じコンピューター内のプログラム同士が高速にデータをやり取りするための仕組みです。daemon(デーモン)は裏方で常に動いているプログラムのこと。レストランでいえば、daemonは厨房のシェフ(常にスタンバイ)、Unix socketは厨房と客席をつなぐ窓口です。
:::

毎回PowerShellを起動すると5-10秒かかる。常駐daemonを作って接続管理・コマンドルーティング・ログをNode.jsで処理し、CDP通信部分だけPowerShellを使う。

アーキテクチャ

bridge click "#submit" -p 9222      ← シェルから1コマンド
     │
     ├─ daemon未起動? → 自動起動(flock排他ロック)
     │
     └─ /tmp/bridge-9222.sock       ← Unix socketで通信
           │
     bridge-daemon.js (Node.js常駐)
           │
           ├─ WebSocket直接接続(Linux native環境)
           └─ PowerShell経由(WSL2環境、自動検出)
                 └─ Chrome CDP (localhost:9222)

daemon実装のポイント

1. WSL2自動検出

function isWsl() {
  if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
  try {
    const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
    return version.includes('microsoft');
  } catch (_) {
    return false;
  }
}

3段階で判定する。環境変数 → /proc/version。未設定時はWSL2ならPowerShellフォールバックを自動有効化。BRIDGE_POWERSHELL_FALLBACK=0で強制無効化も可能。

2. ポート別daemon管理

/tmp/bridge-9222.sock   # daemon通信ソケット
/tmp/bridge-9222.pid    # プロセスID
~/logs/bridge-9222.log  # 監査ログ(/tmpではなく永続パス)

ポートごとに独立したdaemonが動く。複数Chromeプロファイルの同時操作が可能。

bridge title -p 9222    # 自律ループ用Chrome
bridge title -p 9224    # 別プロファイル(Twitter bot用)

3. auto-start + flock排他

CLIラッパー(~/bin/bridge)がdaemonを自動起動する。複数プロセスが同時に起動を試みる場合をflockで排他制御。

:::details 初心者向け: flock(排他ロック)とは
flockは、「今このファイルを使っているので、他の人は待ってください」という仕組みです。トイレのドアの鍵に例えると分かりやすいです。中に人がいるときは鍵がかかっていて入れない——プログラムも同じで、2つのプロセスが同時にdaemonを起動しようとすると壊れるので、flockで「1つずつ」を保証します。
:::

# CLIラッパーの起動フロー(簡略化)
if ! ping_daemon "$PORT"; then
  cleanup_stale_files "$PORT"
  flock -w 5 "/tmp/bridge-${PORT}.lock" bash -c "
    start_daemon $PORT
    wait_for_ready $PORT 5  # 最大5秒、0.5秒間隔でping
  "
fi
send_command "$PORT" "$PAYLOAD"

4. PowerShell経由のCDP通信

daemonがCDPコマンドをPowerShellに委譲する。ペイロードはBase64エンコードしてWSL2→Windows間の引用符問題を回避。

:::details 初心者向け: Base64エンコードとは
Base64は、どんなデータでも英数字と記号だけで表現する変換方式です。荷物を梱包材で包むようなもので、中身(JSON)にダブルクォートなどの特殊文字が含まれていても、Base64で包めば途中で壊れずに届きます。受け取った側で「開封」すれば元のデータが取り出せます。
:::

function sendCDPViaPowerShell(method, params = {}, timeoutMs = 15000) {
  const payload = JSON.stringify({ id: 1, method, params });
  const payloadB64 = Buffer.from(payload, 'utf8').toString('base64');

  const script = `
$payload = [System.Text.Encoding]::UTF8.GetString(
  [System.Convert]::FromBase64String('${payloadB64}'))
$ws = New-Object System.Net.WebSockets.ClientWebSocket
$ws.ConnectAsync([Uri]$tab.webSocketDebuggerUrl, $cts.Token)
# ... send/receive ...
`;

  const proc = spawnSync('powershell.exe', [
    '-NoProfile', '-NonInteractive',
    '-ExecutionPolicy', 'Bypass',
    '-Command', script
  ], { encoding: 'utf8' });

  return JSON.parse(proc.stdout.trim());
}

5. ナビゲーション完了待機

WebSocket直接接続時はCDPイベントで待機。PowerShellフォールバック時はポーリング。

// WebSocket mode: イベント駆動
async function waitForNavigation(timeoutMs) {
  return waitForAnyEvents([
    'Page.loadEventFired',
    'Page.navigatedWithinDocument',
    'Page.frameStoppedLoading'
  ], timeoutMs);
}

// PowerShell mode: ポーリング
async function waitForNavigationByPolling(timeoutMs) {
  while (Date.now() - started <= timeoutMs) {
    const state = await evaluate('document.readyState');
    if (state === 'complete' || state === 'interactive') {
      await sleep(200);
      return;
    }
    await sleep(200);
  }
}

6. 入力エスケープ

セレクタやテキストをJavaScript文字列連結しない。JSON.stringify()で安全にエスケープ。

function jsonLiteral(value) {
  return JSON.stringify(value);
}

// セレクタ指定のクリック
const expression = `(() => {
  const el = document.querySelector(${jsonLiteral(selector)});
  if (!el) return { status: 'not_found' };
  const rect = el.getBoundingClientRect();
  return { status: 'ok', x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
})()`;

7. セキュリティ

process.umask(0o077);  // 作成ファイルは所有者のみ

server.listen(SOCKET_PATH, () => {
  fs.chmodSync(SOCKET_PATH, 0o600);  // ソケット: 所有者のみ
});

// symlinkによるソケット乗っ取り防止
function safeUnlinkIfSymlink(targetPath) {
  const stat = fs.lstatSync(targetPath);
  if (stat.isSymbolicLink()) {
    fs.unlinkSync(targetPath);
  }
}

コマンド一覧

# JavaScript実行
bridge js "document.title"
bridge js --raw "document.title"       # CDP生JSON出力
bridge js -f script.js                 # ファイル実行

# ナビゲーション(完了待機付き)
bridge go "https://example.com"
bridge back
bridge reload

# 画面情報
bridge screenshot
bridge screenshot -o ./capture.png
bridge title
bridge text "#content"
bridge html "#content"

# DOM操作
bridge click "#submit-button"          # セレクタ→座標解決→クリック
bridge click 400 300                   # 座標直接
bridge type "#input" "テキスト"
bridge key Enter
bridge scroll "#element"
bridge wait "#element" --timeout 5000

# タブ・フレーム
bridge tabs
bridge tab 2
bridge frames

# ファイル
bridge upload cover.png "#file-input"

# 生CDP
bridge cdp "Page.reload" '{"ignoreCache":true}'

# 管理
bridge status
bridge start
bridge stop

# ポート指定(どの位置でもOK)
bridge screenshot -p 9224
bridge -p 9224 screenshot

監査ログ

daemonがすべてのコマンドを構造化ログで記録する。

[bridge-daemon 2026-03-10T12:34:56Z] OK action=click port=9222 ms=145
[bridge-daemon 2026-03-10T12:34:57Z] FAIL action=wait port=9222 ms=10001 err="Timeout"

Soakゲート(7日間で50コマンド以上、成功率95%以上)の判定にこのログを使う。

# ゲート判定スクリプト
$ bridge-soak-check
=== Bridge Soak Gate Assessment ===
Period:       2026-03-10 → 2026-03-10 (1 days)
Commands:     179 (OK: 149, FAIL: 30)
Success rate: 83%
Overall: FAIL

速度

方式 速度
毎回PowerShell起動(旧) 5-10秒
daemon + PowerShell transport(現在) 約2.4秒
daemon + WebSocket直接(Linux native) ~100ms

WSL2環境では2.4秒。旧方式の半分以下。ボトルネックはPowerShellプロセス起動。Linux native環境なら100ms以下。

背景: なぜ作り直したか

最初は別の方法で動いていた。Chrome拡張 + WebSocketサーバーという構成。20+コマンド。速度も問題なかった。

だがClaude Codeを自律で回し始めたら、AIがこのツールの存在を忘れて独自にPowerShellスクリプトを作った。そのスクリプトに17本の自動化ツールが依存する状態になった。

統合にあたって必要だったのは、既存17本のスクリプトを壊さずに中身だけ差し替えること。互換ラッパー(bridge-jscdp-click)を挟んで、旧インターフェースのまま新daemonに接続する設計にした。

まとめ

WSL2からChromeを自動操作する場合、直接のWebSocket接続はネットワーク名前空間の壁で阻まれる。daemon + PowerShell fallbackのハイブリッド構成で、接続管理のオーバーヘッドを吸収しつつ実用的な速度を実現できた。

ソース: chrome-claude-bridge(公開予定)


🛠 Claude Codeの自律運用に使っているhooks・テンプレート一式: cc-safe-setup

Claude Codeの自律運行を安全にする Claude Code Ops Kit
🛡 npx cc-safe-setup — 637種の安全フックを10秒でインストール
🛠 全フック: cc-safe-setup — 637種の安全フック。npx cc-safe-setupで10秒インストール


関連記事: Anthropic公式「Skills完全ガイド」 — CLAUDE.mdとSkillsの設計パターン


Claude Codeを使っている方へ — セットアップの安全性を20項目で無料診断できます: npx cc-health-check

「AIに任せて大丈夫なのか」という不安を持ったまま800時間使い続けた記録は非エンジニアがClaude Codeを800時間走らせた——失敗と学びの全記録(¥800・第2章まで無料)に書いた。


📖 トークン消費に困っているならClaude Codeのトークン消費を半分にする——800時間の運用データから見つけた実践テクニック(¥2,500・はじめに+第1章 無料)


⚠️ Opus 4.7をお使いの方へ(2026年4月)
Opus 4.7で安全分類器の不具合・トークン消費急増が報告されています。Safety Scannerで設定を無料チェック。対策はSurvival Guideを参照。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?