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

📌 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 つの問題を全部潰しています:
Chrome 147+ で素朴に attach できない問題 — デフォルトの user-data-dir では `--remote-debugging-port` が完全無視される
クラッシュ復元バブルでフリーズ問題 — Chrome を kill して再起動すると「セッションを復元しますか?」が出てフリーズ
`reload` 時の `beforeunload` ダイアログでフリーズ問題 — note や GAS エディタ等の dirty な状態で reload すると確認ダイアログで止まる
単一 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 段構え:
終了側: SIGTERM で Chrome に正常終了の機会(5 秒)を与える。Chrome 内部で `Last Session` ファイルへの書き出しが完了するので、起動時に「ちゃんと終わった」と判定される
記録側: 念のため `Preferences` の `profile.exit_type` を `"Normal"`、`profile.exited_cleanly` を `true` に書き換える。Chrome が「前回ちゃんと終了したか」を判定する場所
表示側: 万一 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)` が呼ばれているだけ。

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 を叩くと:
`playwright-cli reload` 開始
Chrome が `beforeunload` を発火 → ダイアログ表示
wrapper が modal state 検出 → 「modal 中」フラグを立てる
後続コマンドが全部弾かれる → フリーズ
解決: `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` は他にも、`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 制約あり、本記事の回避策必須)
参考リンク
Chrome DevTools Protocol — playwright と Chrome の通信プロトコル
いいなと思ったら応援しよう!
さらなるスキルアップへの投資、もしくはスタバのおしゃれフリーランスへの変身に活用させて頂きます。