見出し画像

Claude Codeでブラウザ操作するならPlaywright CLI + skills一択だった

Playwright CLI で Chrome を操作する仕組みの全体像

📌 2026-05-05 追記・全面改訂

公開後、この手順を再利用可能な Claude Code Skills に落とし込もうとしたところ、Chrome 147 以降では旧記事の手順だと attach がそもそも成立しないことが判明しました。半日格闘した末に「専用 user-data-dir + Cookie コピー方式」に切り替えたら一瞬で動いたので、その実運用版に全面書き換えしています。

加えて、本記事公開後に踏んだ落とし穴も追記しました:

  • Chrome のクラッシュ復元バブルでフリーズ問題

  • reload 時の `beforeunload` ダイアログでフリーズ問題

  • 複数タスクを並行で動かしたい時の `--instance` 機能

旧版の手順は現バージョンの Chrome では動かず混乱を招くため削除しました。残してあるのは「なぜ MCP から乗り換えたか」と「メリット」の部分だけです。


1. Playwright MCP からの卒業

皆さん、こんにちは。普段 Claude Code で色んな自動化を書いているのですが、最近 Playwright MCP を卒業して、Playwright CLI + 公式 Skills に乗り換えました。

結論から言うと、トークン消費は減る・普段使い Chrome のログイン状態を引き継げる・Skills の書きやすさも段違いで、もう MCP には戻れないです。

Playwright MCP で何が辛かったか

Playwright MCP は `@playwright/mcp` を Claude Code の MCP サーバーとして登録するタイプ。`browser_navigate` / `browser_click` / `browser_snapshot` みたいな専用 tool がエージェントから使える仕組みです。

悪くはないんですが、運用してて引っかかる点がいくつか:

  • tool スキーマでコンテキストを食う。会話の最初から数千トークン持っていかれる

  • 普段使い Chrome に繋ぐのが面倒。`--extension` モード+専用拡張という組み合わせでは繋がるものの、ブリッジタブを誤操作で吹っ飛ばすと接続死ぬ

  • ログイン情報を毎回入れ直す必要がある。Bot 検知のリスクもあるし、単純に手間

Playwright CLI + Skills とは

公式の Playwright CLI という別パッケージがあり、`attach --cdp=...` で 自分が立ち上げた Chrome に繋ぎに行く 運用にできます。普段使いの Cookie / LocalStorage をそのまま流用可能。

加えて、CLI には `playwright-cli install --skills` という agent 最適化された Skills を自動配置するコマンド があり、`~/.claude/skills/playwright-cli/SKILL.md` と references/ が一式展開されます。Claude Code は 必要な時だけ Skills を読み込む 形になるので、MCP のような常駐 tool スキーマが不要 — これが「トークン効率の差」の正体です。

2. `/chrome` Skills が解決する 4 つの問題

ここから本題。最終的にこの記事で語るのは `/chrome` という自作 Skills で、これ 1 本で以下 4 つの問題を全部潰しています:

  1. Chrome 147+ で素朴に attach できない問題 — デフォルトの user-data-dir では `--remote-debugging-port` が完全無視される

  2. クラッシュ復元バブルでフリーズ問題 — Chrome を kill して再起動すると「セッションを復元しますか?」が出てフリーズ

  3. `reload` 時の `beforeunload` ダイアログでフリーズ問題 — note や GAS エディタ等の dirty な状態で reload すると確認ダイアログで止まる

  4. 単一 Chrome では別タスクを並行できない問題 — Studio と note を同時に別エージェントで操作したい

冒頭の図がこの skill の全体像です。中央が「専用 Chrome」で、左から普段使い Chrome の Cookie がコピーされ、右から Claude Code が CDP attach。クラッシュバブルや dialog 抑止のキーワードが小さく散らばっています。下半分は並行起動 (`--instance=studio` / `--instance=note`) の図。

3. 実運用版コード

`~/.claude/skills/chrome/setup-and-attach.sh` の中身(要点だけ抜粋):

#!/usr/bin/env bash
set -euo pipefail

INSTANCE="default"
REFRESH_COOKIES=false
for arg in "$@"; do
  case "$arg" in
    --refresh) REFRESH_COOKIES=true ;;
    --instance=*) INSTANCE="${arg#--instance=}" ;;
  esac
done

# instance 名から決定的に値を決める
if [ "$INSTANCE" = "default" ]; then
  SESSION_NAME="chrome"
  PROFILE_DIR="$HOME/.playwright-chrome-profile"
  PORT=9222
else
  SESSION_NAME="chrome-${INSTANCE}"
  PROFILE_DIR="$HOME/.playwright-chrome-profile-${INSTANCE}"
  HASH=$(printf '%s' "$INSTANCE" | cksum | awk '{print $1}')
  PORT=$((9223 + (HASH % 8)))
fi
SRC_PROFILE="$HOME/Library/Application Support/Google/Chrome/Profile 2"

# Step 0. 既に attach 済みなら何もしない
if [ "$REFRESH_COOKIES" = false ] && \
   playwright-cli list 2>&1 | grep -A 3 "^- ${SESSION_NAME}:" | grep -q "(attached)"; then
  exit 0
fi

# Step 1. 当該 instance の Chrome を SIGTERM 先行で正常終了 → 必要なら SIGKILL
if pgrep -af "Google Chrome.*--user-data-dir=${PROFILE_DIR}" > /dev/null; then
  pkill -TERM -f "Google Chrome.*--user-data-dir=${PROFILE_DIR}" 2>/dev/null || true
  for i in 1 2 3 4 5; do
    sleep 1
    pgrep -af "Google Chrome.*--user-data-dir=${PROFILE_DIR}" > /dev/null || break
  done
  pkill -9 -f "Google Chrome.*--user-data-dir=${PROFILE_DIR}" 2>/dev/null || true
fi

# Step 2. 初回 or --refresh 時のみ: 普段使い Profile から Cookie 等をコピー
if [ ! -d "$PROFILE_DIR/Default" ] || [ "$REFRESH_COOKIES" = true ]; then
  mkdir -p "$PROFILE_DIR/Default"
  for f in "Cookies" "Cookies-journal" "Local Storage" "Login Data" "Login Data-journal" \
           "Web Data" "Web Data-journal" "Preferences" "Secure Preferences" "Sessions" \
           "Bookmarks" "Local Extension Settings"; do
    [ -e "$SRC_PROFILE/$f" ] && cp -R "$SRC_PROFILE/$f" "$PROFILE_DIR/Default/" 2>/dev/null
  done
fi

# Step 2.5. Preferences のクラッシュフラグを Normal に書き換え
PREF="$PROFILE_DIR/Default/Preferences"
if [ -f "$PREF" ]; then
  TMP=$(mktemp)
  jq '.profile.exit_type = "Normal"
    | .profile.exited_cleanly = true
    | .session.restore_on_startup = 5
    | .session.startup_urls = []' "$PREF" > "$TMP" && mv "$TMP" "$PREF"
fi

# Step 3. Chrome を起動(バブル抑止フラグ群を付与)
rm -f "$PROFILE_DIR/SingletonLock" "$PROFILE_DIR/SingletonCookie" \
      "$PROFILE_DIR/SingletonSocket" "$PROFILE_DIR/DevToolsActivePort"
nohup "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --user-data-dir="$PROFILE_DIR" \
  --remote-debugging-port="$PORT" \
  --remote-allow-origins='*' \
  --no-first-run \
  --no-default-browser-check \
  --hide-crash-restore-bubble \
  --disable-session-crashed-bubble \
  --restore-last-session=false \
  > "/tmp/chrome-${SESSION_NAME}.log" 2>&1 &
disown

# Step 4. port が listen するまで待つ
for i in $(seq 1 15); do
  curl -sS -m 1 "http://127.0.0.1:${PORT}/json/version" > /dev/null 2>&1 && break
  sleep 1
done

# Step 5. playwright-cli attach
playwright-cli -s="${SESSION_NAME}" attach --cdp="http://127.0.0.1:${PORT}"

# Step 6. JS dialog の auto-accept ハンドラを context レベルで仕込む
playwright-cli -s="${SESSION_NAME}" run-code "async page => {
  const ctx = page.context();
  const accept = (d) => d.accept().catch(() => {});
  ctx.on('page', (p) => p.on('dialog', accept));
  for (const p of ctx.pages()) p.on('dialog', accept);
}" > /dev/null 2>&1 || true

何が起きているか各ステップの仕組みを次の §4 で 1 つずつ解説します。

4. 仕組みの詳細解説

4-1. Chrome 147+ 制約と user-data-dir / Cookie コピー戦略

Chrome 147 以降、デフォルトの user-data-dir では `--remote-debugging-port` フラグが完全無視されるようになりました。Chrome の stderr に以下が出る:

DevTools remote debugging requires a non-default data directory.
Specify this using --user-data-dir.

`chrome://inspect/#remote-debugging` の「Allow remote debugging for this browser instance」を ON にしても、この制約は解除されません。専用の user-data-dir を別ディレクトリに作る必要があります(旧記事ではここが書けてなかった)。

ただ専用 dir で起動すると今度はログイン状態が空なので、普段使い Chrome の Profile(私の場合 `~/Library/Application Support/Google/Chrome/Profile 2`)から Cookie / LocalStorage / Login Data などを 初回のみ コピーしておきます。これで playwright 用 Chrome 側でも YouTube / Google / note 等にログイン済みの状態でいきなり始められる。

Cookie コピーは初回 1 回だけ。毎回コピーすると playwright 側で増えた状態(履歴・追加 Cookie)が消えるので、`--refresh` フラグで明示的に再コピーする時だけ更新する設計にしてあります。

4-2. クラッシュ復元バブルの撲滅、3 段構えの理由

旧版の skill では `killall -9 "Google Chrome"` で Chrome を強制終了していました。これだと Chrome 側が「異常終了」と判定して、次回起動時に 「セッションを復元しますか?」のバブル が出てフリーズします(私はここで 30 分溶かしました)。

経験的に、終了側と起動側の両方を抑えに行かないと潰せません。3 段構え:

  1. 終了側: SIGTERM で Chrome に正常終了の機会(5 秒)を与える。Chrome 内部で `Last Session` ファイルへの書き出しが完了するので、起動時に「ちゃんと終わった」と判定される

  2. 記録側: 念のため `Preferences` の `profile.exit_type` を `"Normal"`、`profile.exited_cleanly` を `true` に書き換える。Chrome が「前回ちゃんと終了したか」を判定する場所

  3. 表示側: 万一 1, 2 のすり抜けがあってもバブルが画面に出ないように、起動引数 `--hide-crash-restore-bubble` `--disable-session-crashed-bubble` `--restore-last-session=false` で表示自体を抑止

`--hide-crash-restore-bubble` だけだと内部状態としてはクラッシュ判定のままで、`Last Session` の自動復元が走って妙なタブが開きます。なので「内部状態をクラッシュ扱いにさせない」3 段構えのほうが綺麗です。

4-3. JS dialog の自動処理 — playwright-cli と SDK のレイヤー構造

note エディタや GAS エディタなど `beforeunload` を使うサイトを自動操作中、`playwright-cli reload` を叩くと「変更が保存されていません」のダイアログでフリーズします。これは `alert()` / `confirm()` と同類の JS dialog で、誰かが accept か dismiss するまでブラウザが止まる仕組み。

レイヤー構造を見ると分かりやすい

`playwright-cli goto URL` を実行すると、内部では SDK の `page.goto(URL)` が呼ばれているだけ。

Chrome から Claude までの 4 層構造。それぞれが上の層を経由して通信する

Playwright SDK では `page.on('dialog')` で捌ける

Playwright SDK だと、事前にハンドラを登録しておけば dialog が出てもイベントとして捌けます:

page.on('dialog', d => d.accept());
// 以降、ダイアログが出てもイベントハンドラで即 accept される

参考: Playwright Page.on('dialog') / Dialog class

`playwright-cli` は安全側に倒して止める設計

`playwright-cli` は人間が手で叩く前提のラッパなので、ダイアログが出ると modal state という独自フラグを立てて、後続の `evaluate` / `click` / `snapshot` を全部「modal 中は動きません」と弾きます。`alert()` の中身を読まずに勝手に進めると事故るので、安全のために `dialog-accept` / `dialog-dismiss` を人間が打って明示的に処理する建て付けです。

罠: `playwright-cli reload` だけ pre-register が効かない

`page.on('dialog')` は SDK レイヤーのイベントハンドラ。一方 `playwright-cli reload` は wrapper 側で modal state 検出を SDK のイベント発火より先に走らせる仕様。結果、reload を叩くと:

  1. `playwright-cli reload` 開始

  2. Chrome が `beforeunload` を発火 → ダイアログ表示

  3. wrapper が modal state 検出 → 「modal 中」フラグを立てる

  4. 後続コマンドが全部弾かれる → フリーズ

解決: `run-code` で SDK レイヤーに直接降りる

`playwright-cli` には `run-code` という公式コマンドがあり、任意の SDK コードをワンライナーで実行できるエスケープハッチ になっています:

playwright-cli -s=chrome run-code "async page => {
  page.on('dialog', d => d.accept());
  await page.reload({ waitUntil: 'domcontentloaded' });
}"

ポイントは dialog handler 登録と reload を 1 つの run-code 内に閉じ込める こと。wrapper を経由せずに SDK の `page` オブジェクトを直接触っているので、modal state が立たず、ハンドラが先に発火して dialog を accept、reload が完走します。

Skills 側で context レベルに仕込んでおくと 99% 自動化できる

`setup-and-attach.sh` の Step 6 でやっているのは context レベルでの dialog handler 登録:

const ctx = page.context();
const accept = (d) => d.accept().catch(() => {});
ctx.on('page', (p) => p.on('dialog', accept));   // 将来開く page にもハンドラ自動付与
for (const p of ctx.pages()) p.on('dialog', accept);  // 既存 page にもハンドラ付与

これがあると `goto` / `click` / `fill` 等で出る alert / confirm は完全自動処理 されます。残るのは `reload` の例外だけで、それは `run-code` 経由で書く運用にすれば全部自動化できる。

まとめ表

自動処理できるダイアログと、run-code 経由が必要なダイアログの対応表

`run-code` は他にも、`page.context().addInitScript(...)`(新 page で必ず実行されるスクリプト)、`page.waitForResponse(...)`(特定 API 応答の待機)など、ラッパに用意されていない SDK API が必要な時の万能エスケープになります。

4-4. 並行起動(`--instance`)— 別タスクを別 Chrome で同時に

ここがもう一段の応用ですが、`/chrome` Skills を `--instance=<name>` オプション付きで呼ぶと、完全に独立した別 Chrome プロセス を立ち上げられます。

~/.claude/skills/chrome/setup-and-attach.sh --instance=studio
# → session=chrome-studio, port=9223〜9230 (hash 採番), dir=~/.playwright-chrome-profile-studio

~/.claude/skills/chrome/setup-and-attach.sh --instance=note
# → session=chrome-note, port=別、dir=~/.playwright-chrome-profile-note

playwright-cli -s=chrome-studio goto "https://studio.youtube.com/..."
playwright-cli -s=chrome-note   goto "https://note.com/..."

私は Claude Code 内で 2 つのサブエージェントを並列起動して、片方が YouTube Studio、もう片方が note の編集画面を同時に操作する運用に使っています。同じ Chrome の 2 タブだと playwright-cli のコマンドが serialize されて待ちが入りますが、別 instance なら完全並行。

注意点:

  • port は instance 名の hash で決定的に採番(衝突しない)。9223〜9230 の 8 枠なので、極端に多くの instance を立てる用途では衝突を別解決する必要あり

  • user-data-dir も instance ごとに別。Cookie は普段使い Profile から個別にコピーするので、playwright が増やした状態は分離される

  • 引数なしの呼び出し(既存単体運用)はデフォルト = `session=chrome / port=9222 / dir=~/.playwright-chrome-profile` で動作。後方互換あり

5. 応用例 — Skills が積み重なる構造

`/chrome` Skills(接続レイヤー)ができると、その上に サイト固有の自動化 Skills を載せていけます。

私が動かしている例: `/youtube-link-shorts` という Skills で、YouTube Studio の Shorts 一覧を回って「対応する横動画を関連動画として一括セット」する用途。これ、YouTube Data API には該当エンドポイントが無いんですよね(context7 で公式ドキュメントを叩いて確認済み)。なので Studio Web UI を Playwright で叩く以外に手段がない。

設計として綺麗なのは、`/chrome`(接続)と `/youtube-link-shorts`(操作)が分離していて、接続 Skills が他の用途にも使い回せること。Studio の他の作業(アナリティクス取得、サムネ差し替え、概要欄一括編集)も、同じ session を attach し直さずにそのまま追加できます。同じ感覚で `/note-update`、`/gas-new` のように サイト固有の Skills を `/chrome` の上に積み重ねる 構造ができあがる。

ハマりやすいポイントを Studio 操作で書いておくと:

  • 編集ページに直接 URL navigate しない。`goto /video/$ID/edit` を 30 本ぶん回したら、Studio 側で関連動画の値が他の動画にも伝染するバグを踏みました。一覧サムネを click → 編集へ遷移 → `go-back` で一覧に戻る、を 1 タブ内で淡々と繰り返すのが正解

  • Studio のリストはページネーション。スクロールで全件出ると思ったら 30 件/ページの pagination でした

6. 既存 MCP ベースの Skills 移行ガイド

自作 Skills が `browser_navigate` 系 MCP tool を前提に書かれてる場合、ここを `playwright-cli` コマンドに置換します。

  • browser_navigate(url) → `playwright-cli -s=chrome goto <url>`

  • browser_snapshot → `playwright-cli -s=chrome snapshot --filename=foo.yml`

  • browser_click(ref) → `playwright-cli -s=chrome click <ref>`

  • browser_fill(ref, val) → `playwright-cli -s=chrome fill <ref> "<val>"`

  • browser_press_key(key) → `playwright-cli -s=chrome press <key>`

  • browser_evaluate(js) → `playwright-cli -s=chrome eval "<js>"`

`-s=chrome` を付けることで複数の Chrome を session 名で使い分けられます。`/chrome` Skills 経由なら `chrome` 固定で運用するのがシンプル。

ついでに 自動ログイン用の credentials.json 読み取りを削除 できます。普段使い Chrome の Cookie がそのまま乗ってる前提にしてしまえば、パスワードをスクリプトから触らなくていい。セキュリティ的にも精神衛生的にも良い。

7. 実際に試して感じたメリット

乗り換え後、Claude 経由で note.com の下書き投稿や YouTube Studio のアナリティクス取得を走らせていますが、体感が全然違います:

  • 速い — クリーンブラウザ起動なし、attach 済みなら即座に操作開始

  • 静か — 日常の Chrome のタブがそのまま見える

  • 事故が少ない — 自分がログイン済みの状態を Claude が使うので、認証エラーで止まる心配なし

  • ログ出力が綺麗 — CLI の標準出力だけなので、何をしたか後から追いやすい

8. 注意点

  • 権限スコープが広い — attach した Chrome は Claude から完全に制御可能。保存データ・Cookie・任意 URL への遷移も含む。信用できるタスクだけに使う

  • 機密情報が乗ってるタブを開いた状態は避ける — 銀行とかパスワード管理ツールのタブは閉じておく

  • 「Allow remote debugging」を ON にしただけでは動かない — Chrome DevTools MCP 用の設定で、外部 CLI の attach には効かない。起動引数 `--remote-debugging-port` + 専用 user-data-dir が必須

  • Cookie コピーは初回のみが原則 — 毎回コピーすると playwright 側 Chrome で増えた Cookie やセッションが上書きで消える

9. まとめ

  • Playwright を Claude から使うなら、今のデフォルト選択肢は CLI + Skills。MCP のほうが筋が通る用途もまだ残ってるとは思うけど、日常の自動化用途としては CLI 側に完全に軍配

  • ただし Chrome 147+ では 「専用 user-data-dir + 普段使い Profile からの Cookie コピー + `--remote-debugging-port` + `--remote-allow-origins='*'`」 が必要

  • 一度 Skills 化してしまえば毎回ノークリックで attach 状態に入れる

  • クラッシュバブル抑止は 「SIGTERM 先行 + Preferences 書き換え + 起動引数」 の 3 段構えで

  • JS dialog は context レベルで `page.on('dialog')` を仕込み + `reload` だけ `run-code` 経由

  • 並行起動が必要なら `--instance=<name>` で別 Chrome を独立に立ち上げ

トークン消費が減ってるのも地味に嬉しくて、朝から晩まで Claude Code 起動しっぱなしにしてる身からすると、このベースライン削減は効きます。

同じく Playwright MCP で消耗してる人、ぜひ乗り換えてみてください。

10. 技術スタック・参考リンク

技術スタック

  • @playwright/cli v0.1.8

  • Claude Code (CLI 版)

  • Chrome 147+ (remote debugging 制約あり、本記事の回避策必須)

参考リンク

いいなと思ったら応援しよう!

さくらおかひろのり@AIと遊ぶおじさん さらなるスキルアップへの投資、もしくはスタバのおしゃれフリーランスへの変身に活用させて頂きます。