🧩 自動化ChromeがDockのChromeを殺す ― bundle id分離とad-hoc再署名で直した話 — リーダー×
🧩

自動化ChromeがDockのChromeを殺す ― bundle id分離とad-hoc再署名で直した話

#claudecode#macos#playwright#automation#chrome2026-09-07 · 約8

前回、レート制限からの自動再開を作った話を書きました。今回は「Claude Codeの自動化環境」シリーズの一本で、PlaywrightがChromeを直接execしたせいでDockのChromeが壊れた事故と、その恒久対策の話です。

困りごと:自動化だけ動いている時間にDockのChromeが死ぬ

2026-09-01、自動化スクリプトだけが走っている時間帯に、Dockから「Google Chrome」を開こうとすると失敗するようになりました。エラーは open -a "Google Chrome"error -600(procNotFound)。GUIのChromeは1枚も開いていないのに、macOSは「Chromeは起動中」と思い込んでいる状態です。

原因はPlaywrightでした。channel: 'chrome' を指定すると、Playwrightは /Applications/Google Chrome.app のバイナリを直接execします。これをLaunchServicesが「com.google.Chrome が起動した」と認識してしまい、自動化プロセスが終わった後もその認識が残留して、Dock/Spotlightからの起動が通らなくなります。

自動化用のChromeとユーザーが触るChromeを同じbundle idで動かしている限り、この手の干渉は消えません。プロセスをkillしてお茶を濁しても、次にPlaywrightが動けば再発します。

対策:自動化専用バンドルへ分離する

やったことは単純で、/Applications/Google Chrome.app を丸ごとコピーし、別のbundle idを持つ別アプリとして登録し直すことです。~/.claude/scripts/chrome-automation-repair.sh の中核部分がこれです。

SRC="/Applications/Google Chrome.app"
DST="/Applications/Chrome Automation.app"
BUNDLE_ID="com.google.ChromeAutomation"
AUTO_BIN="$DST/Contents/MacOS/Google Chrome"

src_ver="$(ver "$SRC")"
dst_ver="$(ver "$DST")"

if [ ! -d "$DST" ] || [ "$src_ver" != "$dst_ver" ]; then
  if pgrep -f "Chrome Automation.app/Contents/MacOS" >/dev/null 2>&1; then
    log "SKIP rebuild: 自動化Chromeが稼働中 (src=$src_ver dst=${dst_ver:-none})"
  else
    rm -rf "$DST"
    cp -R "$SRC" "$DST"
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$DST/Contents/Info.plist"
    /usr/libexec/PlistBuddy -c "Set :CFBundleName Chrome Automation" "$DST/Contents/Info.plist"
    codesign --force --deep --sign - "$DST" >/dev/null 2>&1 || { log "ERROR: 署名失敗"; exit 1; }
    rebuilt=1
  fi
fi

CFBundleIdentifiercom.google.ChromeAutomation に書き換えるだけで、LaunchServices上は通常Chromeとは別アプリとして登録されます(lsappinfo list で確認可能)。これで自動化側が何をしていようと、Dock側の com.google.Chrome には干渉しません。

自動化Chromeが稼働中なら再構築をスキップして次回に回す ―― コピー中に自分自身のバイナリを差し替えることになるので、これは安全のための必須分岐です。

落とし穴1:Info.plistを書き換えると署名が壊れる

コピーしてbundle idを変えるだけでは動きません。Info.plistを書き換えた時点でGoogleの署名は無効になり、Library ValidationがヘルパープロセスをSIGKILL(exit 137)で即殺します。これを回避するのが最後の一行です。

codesign --force --deep --sign - "$DST" >/dev/null 2>&1

--deep でバンドル全体(Chromeのヘルパー群含む)をad-hoc署名し直すことで、内部の整合性を揃えます。ad-hoc署名(-sign -)なので当然Appleの正式署名ではありませんが、ローカルで自分が使う分にはこれで十分起動します。

落とし穴2:node_modulesパッチはnpm installで揮発する

bundle idを分離しただけでは、Playwright自体はまだ本体Chromeを指したままです。channel:'chrome' の解決先は playwright-core/lib/coreBundle.jsパスとしてハードコードされていて、環境変数での上書きは効きません。なので、node_modules配下を直接sedで書き換えています。

while IFS= read -r f; do
  if /usr/bin/grep -q "$SRC/Contents/MacOS/Google Chrome" "$f" 2>/dev/null; then
    /usr/bin/sed -i '' "s|$SRC/Contents/MacOS/Google Chrome|$AUTO_BIN|g" "$f" && patched=$((patched+1))
  elif /usr/bin/grep -q "$AUTO_BIN" "$f" 2>/dev/null; then
    already=$((already+1))
  fi
done < <(list_core_bundles)

当然ながら、npm installplaywright install をやり直せばこのパッチは消えます。node_modules配下は原理的に揮発するものだと割り切り、6時間ごとのlaunchdで貼り直す運用にしました。

<key>StartInterval</key><integer>21600</integer>
<key>RunAtLoad</key><true/>

~/Library/LaunchAgents/(実際のファイル名は伏せています)に置いたplistで、6時間おき(21600秒)+ログイン時に即実行。スクリプト自体は1秒で終わるので、頻度を上げるコストはほぼありません。

1件もパッチ対象が見つからない場合はスキャンパスが壊れているサインなので、黙って成功扱いにせず即エラーにしています。

if [ $((patched + already)) -eq 0 ]; then
  log "ERROR: playwright-core が1件も見つからない。SCAN_ROOTS を確認せよ。"
  exit 1
fi

副産物:Keychainの「Chrome Safe Storage」ダイアログ

分離対策を入れた翌日(2026-09-02)、今度は別の症状が出ました。ログインセッション中に 「Google ChromeがChrome Safe Storageを使用しようとしています」 というKeychainのパスワードダイアログが不意に出るようになったのです。

原因は、自動化バンドルを shell/pythonから直接 --headless で叩く一発スクリプト(毎日12:44に走るサムネイル生成ジョブ)に --use-mock-keychain が付いていなかったこと。fresh profileを作る際にPlaywright経由なら常にmock keychainを使うのですが、直接execする素朴なスクリプトはそれを付け忘れていました。

厄介なのは、Keychainの「常に許可」が効かないことです。自動化バンドルはad-hoc署名なので、ACLはcdhash固定で紐付きます。Chromeが更新されてバンドルを作り直すたびにcdhashが変わり、「常に許可」設定ごと無効化されて振り出しに戻ります。対策は「Keychainに触らせない」の一択でした。

そこで、直接起動しているスクリプトを静的に検知するゲートをchrome-automation-repair.shに足しました。

keychain_gate() {
  local hits
  hits=$(grep -rIl --include='*.sh' --include='*.py' --include='*.js' --include='*.mjs' \
      --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=profiles --exclude-dir=.profiles \
      --exclude-dir=logs --exclude-dir=state --exclude-dir=.build --exclude-dir=venv --exclude-dir=tests \
      -E 'Chrome Automation\.app/Contents/MacOS|Google Chrome\.app/Contents/MacOS' \
      "${SCAN_ROOTS[@]}" "${HOME}/.claude/scripts" 2>/dev/null \
    | xargs -I{} grep -l -e '--headless' {} 2>/dev/null \
    | xargs -I{} grep -L -e 'use-mock-keychain' {} 2>/dev/null \
    | grep -v 'scent-media/scripts/ensure_chrome.sh' || true)
  if [[ -n "$hits" ]]; then
    log "WARNING: --use-mock-keychain 無しでChromeをheadless起動しているファイルがある(Keychainダイアログ再発の元):"
    echo "$hits" | sed 's/^/  /'
  else
    log "keychain_gate: OK(mock無しの直接起動なし)"
  fi
}

やっているのは3段のパイプです。①Chromeバイナリを直接execしている行を持つファイルを探す→②その中で--headlessが付いているものに絞る→③さらに--use-mock-keychain付いていないものだけを残す。自動修正はせず、WARNINGを出すだけに留めています。

理由は例外があるからです。scent-media/scripts/ensure_chrome.sh はInstagram用の永続プロファイルを使っていて、そこは実Keychainの鍵でCookieを暗号化済みです。ここをmockに切り替えると鍵が変わってCookieが読めなくなり、ログインセッションごと飛びます。だからこのファイルだけはgrepの結果から明示的に除外し、ゲートは「検知して警告する」までに留めて、直すかどうかの判断は残しています。

踏んだ落とし穴

  • Playwrightが本体Chromeを直接exec → LaunchServicesが誤認しDockからの起動がerror -600で失敗。原因究明までに時間がかかった
  • Info.plistのCFBundleIdentifierを書き換えると署名が壊れるcodesign --force --deep --sign - で再署名しないとヘルパーがexit 137で即死
  • 自動化Chrome稼働中に再構築するとバイナリの自己書き換えになるpgrepで稼働チェックしてskipし、次回サイクルに回す
  • node_modulesへの直接sedパッチはnpm installで揮発する → 揮発前提で6時間ごとlaunchdで貼り直す運用にする
  • 「常に許可」はad-hoc署名バンドルには効かない → cdhashが再ビルドのたびに変わるため。Keychainに触らせない設計以外に解はない
  • grepゲートを機械的に全適用すると壊れるプロファイルがある → 実Keychain鍵に依存する永続プロファイルは明示的に除外リストへ

まとめ

  • Playwrightのchannel:'chrome'直execは、Dockから使う通常Chromeとbundle idを分離しないと共存できない
  • 分離は「コピー→CFBundleIdentifier書き換え→codesign --deepで再署名」の3手順。署名し直さないとヘルパーが即死する
  • Playwrightの解決先パッチはnode_modulesへの直接sedなのでnpm installで揮発する前提で、launchdの定期実行で維持する
  • 副産物のKeychainダイアログは「常に許可」では直らない。mock keychain未使用のheadless起動を静的にgrepで検知して潰す
  • 一律の自動修正は禁物。永続プロファイルなど例外は明示的に除外する

Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!