Claude Codeでブラウザ操作するならPlaywright CLI + Skills一択だった
2026/04/22 07:27 公開
2026/05/04 19:03 更新
📌 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 の中身(要点だけ抜粋):
bash#!/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 に以下が出る:
codeDevTools 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 が出てもイベントとして捌けます:
jspage.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 コードをワンライナーで実行できるエスケープハッチ になっています:
bashplaywright-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 登録:
jsconst 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 プロセス を立ち上げられます。
bash~/.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 制約あり、本記事の回避策必須)
参考リンク
- Playwright SDK 公式ドキュメント
- Page.on('dialog') / Dialog class
- @playwright/cli (npm)
- Chrome DevTools Protocol — playwright と Chrome の通信プロトコル
Lステップ × AI を同時に使いたい方へ
LINE公式のWebhook、1つしか設定できなくて困っていませんか?
LINE公式アカウントでは、Webhookの送信先URLを1つしか設定できません。そのため、Lステップを導入している企業が「DifyでAIチャットボットも動かしたい」「自社システムにもイベントを飛ばしたい」と思っても、Webhookの奪い合いになってしまいます。
結果として、片方を諦めるか、中継サーバーを自前で構築するか——どちらにしても時間とコストがかかる選択を迫られます。
L-Proxyなら、1つのWebhookを複数サービスに同時転送
LINE公式アカウントとサービスの間にL-Proxyを挟むだけで、Webhookを好きな数だけ分岐できます。
- Lステップはそのまま、AIチャットボットを追加導入
- 自社の顧客管理システムにもリアルタイムでイベント連携
- 設定はURL貼り替えだけ。コード不要、最短5分で導入
今なら2週間の無料トライアル実施中。クレジットカード不要で、すべての機能をお試しいただけます。