author: GeneralD

Claude Code スキルを作って、5つつまずいた話


5つの罠を踏み越えるエンジニア — ヒーローイラスト

Astro + Cloudflare Workersランディングページ自動デプロイする Claude Code スキル——launch-site——を作りました。作り終えてみると、5つの罠を踏んでいました。ドキュメント書いていない罠思い込み罠、「それ仕様ですか」という罠手元確かめ順書きます。


1つめ: Workers Builds にトリガー API はない

最初、こう考えていました——Workers Builds のトリガーを REST API で叩けばpush 後に自動デプロイ完了する、と。

Cloudflare の API リファレンス探しました。見つかりませんでした。GitHub Actions経由ならあります。ダッシュボードには「Connect to Git」ボタンがあります。でも「ビルドトリガーする」RESTエンドポイント存在しません。

flowchart LR
    A["期待\nPOST /workers/builds/trigger"] -->|そんな API ない| B["404"]
    C["実際の仕組み"] --> D["Git push を検知"]
    D --> E["Cloudflare が自動でビルド"]
    C --> F["ダッシュボードで手動トリガー\n(UI のみ)"]

Workers Builds は「push で自動接続」という設計作られていて、外部からトリガー叩く口意図的作っていないようです。

回避策は **git pushデプロイトリガーとして割り切る**ことでした。スキルは Astroプロジェクト生成してpushする。ビルドは Cloudflare が引き受ける。それ以上の制御諦めました——というか、要りませんでした。API がない分かった時点で設計振り切れたので、むしろシンプルになりました。


2つめ: headless Chromium は Cloudflare ダッシュボードに弾かれる

Workers Builds を使うには、まず Gitリポジトリとの接続ダッシュボード設定しなければなりません。スキルに「ダッシュボード自動操作させよう」と思って playwright-cli起動しました。

dash.cloudflare.comナビしたら、こうなりました。

Just a moment...
Performing security verification...

Cloudflare自身が、headless Chromium のボットフィンガープリント弾きました。自社ダッシュボード自社ボット検知火を噴く——なかなか面白い構図です。

headless Chromium が Cloudflare のセキュリティ検証で弾かれるシーンheadlessモード諦め--headed起動しても、クッキーがなければ同じ画面止まります。

突破口は browser session transplant——普段使いブラウザからクッキー持ち込む方式です。セッション移植してから --headed起動したら、チャレンジ通過して認証済みのダッシュボード到達しました。

移植核心はこうです。既存ブラウザクッキーstorageState として書き出し、次回起動時に読み込む

# ブラウザの cookie を storageState として書き出す
~/.config/bin/pw-import-session --browser arc \
  --domain dash.cloudflare.com --domain .cloudflare.com \
  --out "${XDG_STATE_HOME:-$HOME/.local/state}/playwright-cli/state/cloudflare.json" \
  --load-session cloudflare
// Playwright 起動時に storageState を読み込む
const context = await browser.newContext({
  storageState: `${process.env.XDG_STATE_HOME ?? `${process.env.HOME}/.local/state`}/playwright-cli/state/cloudflare.json`,
});

一度仕組み作れば、どのダッシュボードにも使い回せます。認証まわりは「手元で一度きちんと確かめる」と、あとが楽になります。


3つめ: bunx wrangler がファントムバージョンを引く

Wrangler は Cloudflare Workers のデプロイ CLI です。スキルbunx wrangler deploy叩いていました。これが予想外バージョン引きました。

$ bunx wrangler --version
 ⛅️ wrangler 4.14.0 ...

表示は 4.14.0。でも実際に動いていたのはプロジェクトnode_modules/.bin/wrangler ではなく、bunx独自キャッシュした別バージョンでした。package.json書いた "wrangler": "^3.x"無視して、bunxネットから 4.x を引いてきます。

bunxnpx の Bun版で、package.jsonバージョン制約縛られません。ローカルインストール済みのバイナリを優先するはずですが、キャッシュ状態によっては node_modules/.bin/wrangler すら無視します。

直し方単純で、**node_modules/.bin/wrangler を直接呼ぶ**か、bun runpackage.json の scripts経由にします。

# NG: ファントムバージョンを引く可能性がある
bunx wrangler deploy

# OK: package.json に固定したバージョンが確実に動く
./node_modules/.bin/wrangler deploy

あるいは wranglerdevDependencies明示して bun install を必ず先に走らせるbunx の「便利さ」に乗ると、バージョン揺れます。手元確かめた時にバージョンずれていると、再現するまで時間がかかります。


4つめ: playwright-cli の三か所

Playwright でダッシュボード叩き始めたら、三か所で転びました。独立した問題なので一つずつ書きます。

ひとつ: goto の戻りタイミング

page.goto(url)loadイベント後に resolve します。SPA は load が来ても JS の初期化終わっていません。ダッシュボードの「Git に接続ボタンが DOM に現れる前に次のステップ進んで要素見つから止まる

// NG: ボタンが DOM に現れる前に進んでしまう
await page.goto('https://dash.cloudflare.com/...');
await page.click('[data-testid="connect-git"]');

// OK: 目標要素の出現を待つ
await page.goto('https://dash.cloudflare.com/...');
await page.waitForSelector('[data-testid="connect-git"]', { state: 'visible' });
await page.click('[data-testid="connect-git"]');

waitForSelector目標要素出現待つ。これだけです。goto完了を「準備完了」と信じないことが起点です。

ふたつ: SPA のナビゲーションタイムアウト

接続フローの途中、Cloudflare が GitHub OAuth にリダイレクトします。OAuth認可後に Cloudflare側戻るまで、waitForNavigation がデフォルト 30秒止まりました。

timeout を 120秒まで伸ばし解決しました。OAuth のリダイレクト人間操作必要な場合もあるので、スキルは「認可待ち確認」を挟む設計にしました。待ち時間余裕を持って設定したほうが安全です。

みっつ: window.confirm が自動キャンセルされる

GitHub連携フローの最後、「本当に接続しますか」確認ダイアログwindow.confirm で出ます。Playwright はデフォルトでネイティブダイアログを自動 dismiss します——つまり「キャンセル」を押し続ける

// ダイアログを自動承認する
page.on('dialog', async (dialog) => {
  await dialog.accept();
});

これを仕込まないと、承認したつもりが永遠にキャンセルされます。dialog イベントのハンドラは、ネイティブダイアログ出そうフローでは必ず仕込む——そう習慣づけました。


5つめ: Git アカウントセレクターのサイレント失敗

GitHub連携フローで「どのアカウント接続するか」を選ぶドロップダウンがあります。スキル自動化組織アカウント選択しようとしました。

問題は、選択しただけでは永続されないことです。

null アカウントが選ばれたドロップダウン — 困惑するエンジニアのイラスト

ドロップダウンアカウント選ぶ → 「次へ」 → 次のフォーム進む。ここまでは動きます。しかし「次へ」の内部で非同期保存処理走っており、その完了前に進むとアカウントnull のまま後続の API が呼ばれます。ログには何も出ませんエラーもありません。ただ null進みます。

page.waitForResponse でネットワークリクエスト完了待つことで解決しました。

// アカウント選択後、保存 API のレスポンスを待つ
const [response] = await Promise.all([
  page.waitForResponse((resp) =>
    resp.url().includes('/api/accounts') && resp.status() === 200
  ),
  page.selectOption('#account-selector', orgAccountId),
]);

ログに何も出ないまま処理進む状態は、手元確かめるのに一番時間がかかりました。「動いてるのに動いてない」を見たら、まずネットワーク見る——これが手元分かったことです。


やってみて分かったこと

5つ踏んで、5つ這い出しました。

自動化組むとき、「API がある前提」で設計しないほうが安全です。Workers Builds のように、意図的に API がない仕様もあります。push = デプロイ割り切れると、設計はむしろシンプルになりました。

ブラウザ自動化は、「見えた」を信じないgoto完了要素出現ではなく、ダイアログハンドラがなければ黙って消えて保存非同期走る仕込み忘れサイレント通過してサイレント壊れます。

動かしてみて分かったのは、小さく転がすと、どこが効いているのかが手触りで分かる——という一点でした。