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-js、cdp-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を参照。