[{"rendered_body":"\u003cp data-sourcepos=\"1:1-2:108\"\u003e前にボトムシートで入力して追加する画面を作りました。\u003cbr\u003e\nその時に開いたときにキーボードを自動表示したら楽だと思い、実装しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"4:1-4:49\"\u003e\n\u003cspan id=\"1-autofocus-でキーボードを自動表示\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-autofocus-%E3%81%A7%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%82%92%E8%87%AA%E5%8B%95%E8%A1%A8%E7%A4%BA\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. autofocus でキーボードを自動表示\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dart\" data-sourcepos=\"6:1-16:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003eTextField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003econtroller:\u003c/span\u003e \u003cspan class=\"n\"\u003econtroller\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eautofocus:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"c1\"\u003e// ← これだけ\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003edecoration:\u003c/span\u003e \u003cspan class=\"n\"\u003eInputDecoration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003elabelText:\u003c/span\u003e \u003cspan class=\"s\"\u003e'商品名'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eborder:\u003c/span\u003e \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"n\"\u003eOutlineInputBorder\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003eerrorText:\u003c/span\u003e \u003cspan class=\"n\"\u003eerrorText\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"18:1-18:112\"\u003eautofocus: true を付けると、ボトムシート表示と同時にキーボードが立ち上がります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"20:1-20:57\"\u003e\n\u003cspan id=\"2-キーボードに隠れないよう余白を確保\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AB%E9%9A%A0%E3%82%8C%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E4%BD%99%E7%99%BD%E3%82%92%E7%A2%BA%E4%BF%9D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. キーボードに隠れないよう余白を確保\u003c/h2\u003e\n\u003cp data-sourcepos=\"21:1-21:118\"\u003eキーボード表示時、入力欄やボタンが隠れないように viewInsets.bottom で余白を足します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dart\" data-sourcepos=\"22:1-39:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ePadding\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003epadding:\u003c/span\u003e \u003cspan class=\"n\"\u003eEdgeInsets\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003efromLTRB\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eMediaQuery\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eviewInsets\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ebottom\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e// ↑ キーボードの高さ分 + 16px\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e),\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003echild:\u003c/span\u003e \u003cspan class=\"n\"\u003eColumn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"nl\"\u003echildren:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n      \u003cspan class=\"n\"\u003eTextField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003eautofocus:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e...),\u003c/span\u003e\n      \u003cspan class=\"n\"\u003eDropdownButtonFormField\u003c/span\u003e\u003cspan class=\"p\"\u003e(...),\u003c/span\u003e\n      \u003cspan class=\"n\"\u003eRow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003echildren:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"cm\"\u003e/* ボタン */\u003c/span\u003e\u003cspan class=\"p\"\u003e]),\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e],\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"41:1-41:39\"\u003e\n\u003cspan id=\"3-showmodalbottomsheet-側の設定\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-showmodalbottomsheet-%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. showModalBottomSheet 側の設定\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dart\" data-sourcepos=\"42:1-48:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eshowModalBottomSheet\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eAddItemResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003econtext:\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003eisScrollControlled:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"c1\"\u003e// キーボード表示時にシートを押し上げる\u003c/span\u003e\n  \u003cspan class=\"nl\"\u003ebuilder:\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"n\"\u003eAddItemSheet\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"49:1-49:136\"\u003eisScrollControlled: true がないと、キーボード表示時にシートが正しくリサイズされないことがあります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"51:1-51:18\"\u003e\n\u003cspan id=\"動作の流れ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8B%95%E4%BD%9C%E3%81%AE%E6%B5%81%E3%82%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e動作の流れ\u003c/h2\u003e\n\u003col data-sourcepos=\"52:1-56:0\"\u003e\n\u003cli data-sourcepos=\"52:1-52:49\"\u003e「追加」ボタン → showModalBottomSheet\u003c/li\u003e\n\u003cli data-sourcepos=\"53:1-53:22\"\u003eAddItemSheet 表示\u003c/li\u003e\n\u003cli data-sourcepos=\"54:1-54:52\"\u003eTextField の autofocus → キーボード表示\u003c/li\u003e\n\u003cli data-sourcepos=\"55:1-56:0\"\u003eviewInsets.bottom → ボタンがキーボードに隠れない\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"57:1-57:12\"\u003e\n\u003cspan id=\"注意点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%B3%A8%E6%84%8F%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e注意点\u003c/h2\u003e\n\u003cp data-sourcepos=\"58:1-58:51\"\u003eTextEditingController は dispose() で破棄する\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"dart\" data-sourcepos=\"59:1-65:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nd\"\u003e@override\u003c/span\u003e\n\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003edispose\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"n\"\u003econtroller\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003edispose\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n  \u003cspan class=\"k\"\u003esuper\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003edispose\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"67:1-67:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"68:1-68:158\"\u003eautofocus: true + viewInsets.bottom + isScrollControlled: true の3点で、ボトムシート表示時にすぐ入力できる UX を実現できました。\u003c/p\u003e\n","body":"前にボトムシートで入力して追加する画面を作りました。\nその時に開いたときにキーボードを自動表示したら楽だと思い、実装しました。\n\n## 1. autofocus でキーボードを自動表示\n\n```dart\nTextField(\n  controller: controller,\n  autofocus: true, // ← これだけ\n  decoration: InputDecoration(\n    labelText: '商品名',\n    border: const OutlineInputBorder(),\n    errorText: errorText,\n  ),\n),\n```\n\nautofocus: true を付けると、ボトムシート表示と同時にキーボードが立ち上がります。\n\n## 2. キーボードに隠れないよう余白を確保\nキーボード表示時、入力欄やボタンが隠れないように viewInsets.bottom で余白を足します。\n```dart\nPadding(\n  padding: EdgeInsets.fromLTRB(\n    16,\n    16,\n    16,\n    MediaQuery.of(context).viewInsets.bottom + 16,\n    // ↑ キーボードの高さ分 + 16px\n  ),\n  child: Column(\n    children: [\n      TextField(autofocus: true, ...),\n      DropdownButtonFormField(...),\n      Row(children: [/* ボタン */]),\n    ],\n  ),\n)\n```\n\n## 3. showModalBottomSheet 側の設定\n```dart\nawait showModalBottomSheet\u003cAddItemResult\u003e(\n  context: context,\n  isScrollControlled: true, // キーボード表示時にシートを押し上げる\n  builder: (_) =\u003e const AddItemSheet(),\n);\n```\nisScrollControlled: true がないと、キーボード表示時にシートが正しくリサイズされないことがあります。\n\n## 動作の流れ\n1. 「追加」ボタン → showModalBottomSheet\n2. AddItemSheet 表示\n3. TextField の autofocus → キーボード表示\n4. viewInsets.bottom → ボタンがキーボードに隠れない\n\n## 注意点\nTextEditingController は dispose() で破棄する\n```dart\n@override\nvoid dispose() {\n  controller.dispose();\n  super.dispose();\n}\n```\n\n## まとめ\nautofocus: true + viewInsets.bottom + isScrollControlled: true の3点で、ボトムシート表示時にすぐ入力できる UX を実現できました。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:56:28+09:00","group":null,"id":"5755265453c701b88eca","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Flutter","versions":[]}],"title":"【Flutter】ボトムシートを開いたときにキーボードを自動表示する","updated_at":"2026-09-27T11:56:28+09:00","url":"https://qiita.com/yuta317/items/5755265453c701b88eca","user":{"description":"","facebook_id":"","followees_count":1,"followers_count":5,"github_login_name":null,"id":"yuta317","items_count":252,"linkedin_id":"","location":"","name":"kiku may","organization":"","permanent_id":274615,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/274615/7757e15d7a47cfd348c046eef22914edf85e2a9e/x_large.png?1670835736","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":"infreed","slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:120\"\u003eCSVデータを扱っていると、いつの間にか同じデータが複数行入っていることがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-3:131\"\u003e数十行ならExcelで確認できますが、数千〜数万行になると目視での確認は現実的ではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:121\"\u003ePythonの \u003ccode\u003epandas\u003c/code\u003e を使えば、CSVの重複確認から削除、結果の保存まで簡単に自動化できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:21\"\u003eこの記事では、\u003c/p\u003e\n\u003cul data-sourcepos=\"9:1-15:0\"\u003e\n\u003cli data-sourcepos=\"9:1-9:38\"\u003eCSVに重複があるか確認する\u003c/li\u003e\n\u003cli data-sourcepos=\"10:1-10:35\"\u003e完全に同じ行を削除する\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-11:47\"\u003e特定の列を基準に重複を削除する\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-12:44\"\u003e複数列を使って重複を判定する\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:38\"\u003e元データを壊さず保存する\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-15:0\"\u003eCSVの文字コード問題に対応する\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"16:1-16:60\"\u003eという実務で使いやすい方法を紹介します。\u003c/p\u003e\n\u003chr data-sourcepos=\"18:1-19:0\"\u003e\n\u003ch2 data-sourcepos=\"20:1-20:39\"\u003e\n\u003cspan id=\"1-pandasをインストールする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-pandas%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. pandasをインストールする\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-22:99\"\u003eまだpandasをインストールしていない場合は、次のコマンドを実行します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"24:1-26:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003epip \u003cspan class=\"nb\"\u003einstall \u003c/span\u003epandas\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"28:1-28:33\"\u003ePythonから読み込みます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"30:1-32:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003epandas\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003chr data-sourcepos=\"34:1-35:0\"\u003e\n\u003ch2 data-sourcepos=\"36:1-36:36\"\u003e\n\u003cspan id=\"2-csvファイルを読み込む\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-csv%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. CSVファイルを読み込む\u003c/h2\u003e\n\u003cp data-sourcepos=\"38:1-38:33\"\u003eまずCSVを読み込みます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"40:1-47:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003epandas\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003edf\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eread_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003einput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ehead\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e行数:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"49:1-49:99\"\u003e処理前の行数を記録しておくと、後で何行削除されたのか確認できます。\u003c/p\u003e\n\u003chr data-sourcepos=\"51:1-52:0\"\u003e\n\u003ch2 data-sourcepos=\"53:1-53:42\"\u003e\n\u003cspan id=\"3-重複している行を確認する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E9%87%8D%E8%A4%87%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E8%A1%8C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 重複している行を確認する\u003c/h2\u003e\n\u003cp data-sourcepos=\"55:1-55:75\"\u003eいきなり削除するのではなく、まず重複を確認します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"57:1-61:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003eduplicates\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eduplicated\u003c/span\u003e\u003cspan class=\"p\"\u003e()]\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eduplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"63:1-63:48\"\u003e重複行の件数だけ確認する場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"65:1-67:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e重複行数:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eduplicated\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"nf\"\u003esum\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"69:1-69:24\"\u003eで確認できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"71:1-71:114\"\u003e実務データでは、削除前に「何が重複しているのか」を確認しておく方が安全です。\u003c/p\u003e\n\u003chr data-sourcepos=\"73:1-74:0\"\u003e\n\u003ch2 data-sourcepos=\"75:1-75:39\"\u003e\n\u003cspan id=\"4-完全に同じ行を削除する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E5%AE%8C%E5%85%A8%E3%81%AB%E5%90%8C%E3%81%98%E8%A1%8C%E3%82%92%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 完全に同じ行を削除する\u003c/h2\u003e\n\u003cp data-sourcepos=\"77:1-77:107\"\u003eすべての列が同じ行を重複として削除する場合は、\u003ccode\u003edrop_duplicates()\u003c/code\u003e を使います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"79:1-85:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e処理前:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e処理後:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e削除数:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"87:1-87:69\"\u003e例えば1000行のCSVから50行の重複が削除された場合、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"89:1-93:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e処理前: 1000\n処理後: 950\n削除数: 50\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"95:1-95:18\"\u003eとなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"97:1-97:130\"\u003e元の \u003ccode\u003edf\u003c/code\u003e を直接変更せず、\u003ccode\u003ecleaned\u003c/code\u003e という別の変数に結果を入れておくと確認しやすくなります。\u003c/p\u003e\n\u003chr data-sourcepos=\"99:1-100:0\"\u003e\n\u003ch2 data-sourcepos=\"101:1-101:51\"\u003e\n\u003cspan id=\"5-特定の列を基準に重複を削除する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E7%89%B9%E5%AE%9A%E3%81%AE%E5%88%97%E3%82%92%E5%9F%BA%E6%BA%96%E3%81%AB%E9%87%8D%E8%A4%87%E3%82%92%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 特定の列を基準に重複を削除する\u003c/h2\u003e\n\u003cp data-sourcepos=\"103:1-103:161\"\u003e実際の業務では「行全体が同じか」ではなく、顧客IDやメールアドレスなどを基準に重複を判定したい場合があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"105:1-105:36\"\u003e例えば次のようなCSVです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"107:1-112:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecustomer_id,name,email\n1001,田中太郎,tanaka@example.com\n1002,山田花子,yamada@example.com\n1001,田中太郎,tanaka@example.com\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"114:1-114:59\"\u003e\u003ccode\u003ecustomer_id\u003c/code\u003e を基準に重複を削除する場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"116:1-121:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"n\"\u003esubset\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecustomer_id\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ekeep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003efirst\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"123:1-123:15\"\u003eとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"125:1-125:66\"\u003e\u003ccode\u003ekeep=\"first\"\u003c/code\u003e は最初に出てきたデータを残します。\u003c/p\u003e\n\u003cp data-sourcepos=\"127:1-127:45\"\u003e最後のデータを残したい場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"129:1-134:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"n\"\u003esubset\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecustomer_id\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ekeep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003elast\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"136:1-136:15\"\u003eとします。\u003c/p\u003e\n\u003chr data-sourcepos=\"138:1-139:0\"\u003e\n\u003ch2 data-sourcepos=\"140:1-140:48\"\u003e\n\u003cspan id=\"6-複数の列を使って重複判定する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E8%A4%87%E6%95%B0%E3%81%AE%E5%88%97%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E9%87%8D%E8%A4%87%E5%88%A4%E5%AE%9A%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. 複数の列を使って重複判定する\u003c/h2\u003e\n\u003cp data-sourcepos=\"142:1-142:111\"\u003e名前だけを基準にすると、同姓同名の別人まで削除してしまう可能性があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"144:1-144:45\"\u003eその場合は複数列を指定します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"146:1-151:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"n\"\u003esubset\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ename\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eemail\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ekeep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003efirst\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"153:1-153:15\"\u003eこれなら、\u003c/p\u003e\n\u003cp data-sourcepos=\"155:1-155:64\"\u003e\u003cstrong\u003e名前とメールアドレスの両方が一致した場合\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"157:1-157:36\"\u003eだけ重複として扱えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"159:1-159:66\"\u003eCSVの重複削除では、プログラムそのものよりも、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"161:1-161:59\"\u003e\n\u003cp data-sourcepos=\"161:3-161:59\"\u003e何をもって「同じデータ」と判断するのか\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"163:1-163:45\"\u003eを最初に決めることが重要です。\u003c/p\u003e\n\u003chr data-sourcepos=\"165:1-166:0\"\u003e\n\u003ch2 data-sourcepos=\"167:1-167:45\"\u003e\n\u003cspan id=\"7-重複データをすべて確認する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-%E9%87%8D%E8%A4%87%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%81%99%E3%81%B9%E3%81%A6%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. 重複データをすべて確認する\u003c/h2\u003e\n\u003cp data-sourcepos=\"169:1-169:81\"\u003e削除対象になりそうなデータをまとめて確認したい場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"171:1-180:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003eduplicates\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\n    \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eduplicated\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n        \u003cspan class=\"n\"\u003esubset\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecustomer_id\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n        \u003cspan class=\"n\"\u003ekeep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eduplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"182:1-182:15\"\u003eとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"184:1-184:94\"\u003e\u003ccode\u003ekeep=False\u003c/code\u003e を指定すると、重複しているグループ全体を確認できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"186:1-186:42\"\u003e削除前のチェックに便利です。\u003c/p\u003e\n\u003chr data-sourcepos=\"188:1-189:0\"\u003e\n\u003ch2 data-sourcepos=\"190:1-190:33\"\u003e\n\u003cspan id=\"8-元csvを上書きしない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-%E5%85%83csv%E3%82%92%E4%B8%8A%E6%9B%B8%E3%81%8D%E3%81%97%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. 元CSVを上書きしない\u003c/h2\u003e\n\u003cp data-sourcepos=\"192:1-192:111\"\u003e業務データを扱う場合、いきなり元ファイルを上書きするのはおすすめしません。\u003c/p\u003e\n\u003cp data-sourcepos=\"194:1-194:42\"\u003e別ファイルとして保存します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"196:1-202:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eto_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eoutput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eindex\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eencoding\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eutf-8-sig\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"204:1-204:15\"\u003eこれなら、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"206:1-209:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003einput.csv   ← 元データ\noutput.csv  ← 重複削除後\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"211:1-211:39\"\u003eという形で両方を残せます。\u003c/p\u003e\n\u003chr data-sourcepos=\"213:1-214:0\"\u003e\n\u003ch2 data-sourcepos=\"215:1-215:45\"\u003e\n\u003cspan id=\"9-csvの文字コードにも注意する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9-csv%E3%81%AE%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AB%E3%82%82%E6%B3%A8%E6%84%8F%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. CSVの文字コードにも注意する\u003c/h2\u003e\n\u003cp data-sourcepos=\"217:1-217:66\"\u003eCSV処理で意外と多いのが文字コードの問題です。\u003c/p\u003e\n\u003cp data-sourcepos=\"219:1-219:142\"\u003e例えばWindowsやExcelから出力されたCSVでは、UTF-8ではなくCP932（Shift_JIS系）が使われていることがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"221:1-221:38\"\u003eUTF-8で読み込めない場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"223:1-228:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eread_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003einput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eencoding\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003ecp932\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"230:1-230:18\"\u003eを試せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"232:1-232:74\"\u003e逆に、処理後のCSVをExcelでも開きやすくしたい場合は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"234:1-240:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eto_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eoutput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eindex\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eencoding\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eutf-8-sig\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"242:1-242:33\"\u003eのように保存できます。\u003c/p\u003e\n\u003chr data-sourcepos=\"244:1-245:0\"\u003e\n\u003ch2 data-sourcepos=\"246:1-246:61\"\u003e\n\u003cspan id=\"10-実務では重複削除だけで終わらない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10-%E5%AE%9F%E5%8B%99%E3%81%A7%E3%81%AF%E9%87%8D%E8%A4%87%E5%89%8A%E9%99%A4%E3%81%A0%E3%81%91%E3%81%A7%E7%B5%82%E3%82%8F%E3%82%89%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. 実務では「重複削除だけ」で終わらない\u003c/h2\u003e\n\u003cp data-sourcepos=\"248:1-248:60\"\u003e実際のCSVデータでは、単純な重複以外にも、\u003c/p\u003e\n\u003cul data-sourcepos=\"250:1-258:0\"\u003e\n\u003cli data-sourcepos=\"250:1-250:26\"\u003e文字コードの違い\u003c/li\u003e\n\u003cli data-sourcepos=\"251:1-251:17\"\u003e不要な空白\u003c/li\u003e\n\u003cli data-sourcepos=\"252:1-252:8\"\u003e空欄\u003c/li\u003e\n\u003cli data-sourcepos=\"253:1-253:6\"\u003eNULL\u003c/li\u003e\n\u003cli data-sourcepos=\"254:1-254:14\"\u003e列の不足\u003c/li\u003e\n\u003cli data-sourcepos=\"255:1-255:14\"\u003e表記揺れ\u003c/li\u003e\n\u003cli data-sourcepos=\"256:1-256:25\"\u003eExcel由来のデータ\u003c/li\u003e\n\u003cli data-sourcepos=\"257:1-258:0\"\u003e同じ顧客なのに表記が違う\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"259:1-259:45\"\u003eなどが混在することがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"261:1-261:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"263:1-268:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e株式会社ABC\n(株)ABC\n株式会社ＡＢＣ\n株式会社ABC \n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"270:1-270:108\"\u003eは、人間には同じ会社に見えても、そのままでは別の文字列として扱われます。\u003c/p\u003e\n\u003cp data-sourcepos=\"272:1-272:27\"\u003eそのため実務では、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"274:1-288:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eCSV読み込み\n↓\n文字コード確認\n↓\nデータ整形\n↓\n重複判定\n↓\n重複削除\n↓\n件数確認\n↓\n別ファイルへ保存\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"290:1-290:51\"\u003eという順番で処理した方が安全です。\u003c/p\u003e\n\u003chr data-sourcepos=\"292:1-293:0\"\u003e\n\u003ch2 data-sourcepos=\"294:1-294:52\"\u003e\n\u003cspan id=\"11-最小構成ならこれだけでも使える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#11-%E6%9C%80%E5%B0%8F%E6%A7%8B%E6%88%90%E3%81%AA%E3%82%89%E3%81%93%E3%82%8C%E3%81%A0%E3%81%91%E3%81%A7%E3%82%82%E4%BD%BF%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e11. 最小構成ならこれだけでも使える\u003c/h2\u003e\n\u003cp data-sourcepos=\"296:1-296:105\"\u003e単純に完全一致する重複行を削除するだけなら、次のコードで処理できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"298:1-322:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003epandas\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003einput_file\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003einput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"n\"\u003eoutput_file\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eoutput.csv\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003edf\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eread_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einput_file\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003ebefore\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003eafter\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eto_csv\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eoutput_file\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eindex\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"bp\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eencoding\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eutf-8-sig\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e処理前:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebefore\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e処理後:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eafter\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e削除数:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebefore\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003eafter\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nf\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e出力:\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eoutput_file\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"324:1-324:87\"\u003eこれだけでも、手作業で重複行を探すよりかなり楽になります。\u003c/p\u003e\n\u003chr data-sourcepos=\"326:1-327:0\"\u003e\n\u003ch2 data-sourcepos=\"328:1-328:49\"\u003e\n\u003cspan id=\"12-定型的なcsv作業は自動化できる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#12-%E5%AE%9A%E5%9E%8B%E7%9A%84%E3%81%AAcsv%E4%BD%9C%E6%A5%AD%E3%81%AF%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%A7%E3%81%8D%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e12. 定型的なCSV作業は自動化できる\u003c/h2\u003e\n\u003cp data-sourcepos=\"330:1-330:63\"\u003e毎日・毎週同じようなCSV整理をしている場合、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"332:1-342:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eCSVを受け取る\n↓\n内容をチェック\n↓\n重複を削除\n↓\nCSVを生成\n↓\n結果を返す\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"344:1-344:30\"\u003eまで自動化できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"346:1-346:105\"\u003e私自身も、この考え方を使ってCSV処理を自動化する仕組みを構築しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"348:1-348:162\"\u003ePython環境を用意せず、CSVファイルをアップロードして重複削除を行いたい場合は、こちらでも試せるようにしています。\u003c/p\u003e\n\u003cp data-sourcepos=\"350:1-350:70\"\u003e\u003ciframe id=\"qiita-embed-content__60dfa377aab82a0810a89246fb64a2ed\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__60dfa377aab82a0810a89246fb64a2ed\" data-content=\"https%3A%2F%2Fxs669270.xsrv.jp%2Fautoworks%2Farticles%2Fcsv-duplicate-removal.html\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003chr data-sourcepos=\"352:1-353:0\"\u003e\n\u003ch2 data-sourcepos=\"354:1-354:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"356:1-356:66\"\u003eCSVの重複削除自体は、pandasなら非常に簡単です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"358:1-360:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003ecleaned\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edf\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003edrop_duplicates\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"362:1-362:24\"\u003eただし実務では、\u003c/p\u003e\n\u003cul data-sourcepos=\"364:1-369:0\"\u003e\n\u003cli data-sourcepos=\"364:1-364:32\"\u003e何を重複と判断するか\u003c/li\u003e\n\u003cli data-sourcepos=\"365:1-365:29\"\u003eどのデータを残すか\u003c/li\u003e\n\u003cli data-sourcepos=\"366:1-366:35\"\u003e文字コードをどうするか\u003c/li\u003e\n\u003cli data-sourcepos=\"367:1-367:35\"\u003e元ファイルを保護するか\u003c/li\u003e\n\u003cli data-sourcepos=\"368:1-369:0\"\u003e処理前後の件数を確認するか\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"370:1-370:39\"\u003eまで考えることが重要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"372:1-372:72\"\u003e特に業務データでは、いきなり削除するのではなく、\u003c/p\u003e\n\u003cp data-sourcepos=\"374:1-374:81\"\u003e\u003cstrong\u003e確認 → 重複判定 → 削除 → 件数確認 → 別ファイル保存\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"376:1-376:48\"\u003eという流れにしておくと安全です。\u003c/p\u003e\n","body":"CSVデータを扱っていると、いつの間にか同じデータが複数行入っていることがあります。\n\n数十行ならExcelで確認できますが、数千〜数万行になると目視での確認は現実的ではありません。\n\nPythonの `pandas` を使えば、CSVの重複確認から削除、結果の保存まで簡単に自動化できます。\n\nこの記事では、\n\n- CSVに重複があるか確認する\n- 完全に同じ行を削除する\n- 特定の列を基準に重複を削除する\n- 複数列を使って重複を判定する\n- 元データを壊さず保存する\n- CSVの文字コード問題に対応する\n\nという実務で使いやすい方法を紹介します。\n\n---\n\n## 1. pandasをインストールする\n\nまだpandasをインストールしていない場合は、次のコマンドを実行します。\n\n```bash\npip install pandas\n```\n\nPythonから読み込みます。\n\n```python\nimport pandas as pd\n```\n\n---\n\n## 2. CSVファイルを読み込む\n\nまずCSVを読み込みます。\n\n```python\nimport pandas as pd\n\ndf = pd.read_csv(\"input.csv\")\n\nprint(df.head())\nprint(\"行数:\", len(df))\n```\n\n処理前の行数を記録しておくと、後で何行削除されたのか確認できます。\n\n---\n\n## 3. 重複している行を確認する\n\nいきなり削除するのではなく、まず重複を確認します。\n\n```python\nduplicates = df[df.duplicated()]\n\nprint(duplicates)\n```\n\n重複行の件数だけ確認する場合は、\n\n```python\nprint(\"重複行数:\", df.duplicated().sum())\n```\n\nで確認できます。\n\n実務データでは、削除前に「何が重複しているのか」を確認しておく方が安全です。\n\n---\n\n## 4. 完全に同じ行を削除する\n\nすべての列が同じ行を重複として削除する場合は、`drop_duplicates()` を使います。\n\n```python\ncleaned = df.drop_duplicates()\n\nprint(\"処理前:\", len(df))\nprint(\"処理後:\", len(cleaned))\nprint(\"削除数:\", len(df) - len(cleaned))\n```\n\n例えば1000行のCSVから50行の重複が削除された場合、\n\n```text\n処理前: 1000\n処理後: 950\n削除数: 50\n```\n\nとなります。\n\n元の `df` を直接変更せず、`cleaned` という別の変数に結果を入れておくと確認しやすくなります。\n\n---\n\n## 5. 特定の列を基準に重複を削除する\n\n実際の業務では「行全体が同じか」ではなく、顧客IDやメールアドレスなどを基準に重複を判定したい場合があります。\n\n例えば次のようなCSVです。\n\n```text\ncustomer_id,name,email\n1001,田中太郎,tanaka@example.com\n1002,山田花子,yamada@example.com\n1001,田中太郎,tanaka@example.com\n```\n\n`customer_id` を基準に重複を削除する場合は、\n\n```python\ncleaned = df.drop_duplicates(\n    subset=[\"customer_id\"],\n    keep=\"first\"\n)\n```\n\nとします。\n\n`keep=\"first\"` は最初に出てきたデータを残します。\n\n最後のデータを残したい場合は、\n\n```python\ncleaned = df.drop_duplicates(\n    subset=[\"customer_id\"],\n    keep=\"last\"\n)\n```\n\nとします。\n\n---\n\n## 6. 複数の列を使って重複判定する\n\n名前だけを基準にすると、同姓同名の別人まで削除してしまう可能性があります。\n\nその場合は複数列を指定します。\n\n```python\ncleaned = df.drop_duplicates(\n    subset=[\"name\", \"email\"],\n    keep=\"first\"\n)\n```\n\nこれなら、\n\n**名前とメールアドレスの両方が一致した場合**\n\nだけ重複として扱えます。\n\nCSVの重複削除では、プログラムそのものよりも、\n\n\u003e 何をもって「同じデータ」と判断するのか\n\nを最初に決めることが重要です。\n\n---\n\n## 7. 重複データをすべて確認する\n\n削除対象になりそうなデータをまとめて確認したい場合は、\n\n```python\nduplicates = df[\n    df.duplicated(\n        subset=[\"customer_id\"],\n        keep=False\n    )\n]\n\nprint(duplicates)\n```\n\nとします。\n\n`keep=False` を指定すると、重複しているグループ全体を確認できます。\n\n削除前のチェックに便利です。\n\n---\n\n## 8. 元CSVを上書きしない\n\n業務データを扱う場合、いきなり元ファイルを上書きするのはおすすめしません。\n\n別ファイルとして保存します。\n\n```python\ncleaned.to_csv(\n    \"output.csv\",\n    index=False,\n    encoding=\"utf-8-sig\"\n)\n```\n\nこれなら、\n\n```text\ninput.csv   ← 元データ\noutput.csv  ← 重複削除後\n```\n\nという形で両方を残せます。\n\n---\n\n## 9. CSVの文字コードにも注意する\n\nCSV処理で意外と多いのが文字コードの問題です。\n\n例えばWindowsやExcelから出力されたCSVでは、UTF-8ではなくCP932（Shift_JIS系）が使われていることがあります。\n\nUTF-8で読み込めない場合は、\n\n```python\ndf = pd.read_csv(\n    \"input.csv\",\n    encoding=\"cp932\"\n)\n```\n\nを試せます。\n\n逆に、処理後のCSVをExcelでも開きやすくしたい場合は、\n\n```python\ncleaned.to_csv(\n    \"output.csv\",\n    index=False,\n    encoding=\"utf-8-sig\"\n)\n```\n\nのように保存できます。\n\n---\n\n## 10. 実務では「重複削除だけ」で終わらない\n\n実際のCSVデータでは、単純な重複以外にも、\n\n- 文字コードの違い\n- 不要な空白\n- 空欄\n- NULL\n- 列の不足\n- 表記揺れ\n- Excel由来のデータ\n- 同じ顧客なのに表記が違う\n\nなどが混在することがあります。\n\n例えば、\n\n```text\n株式会社ABC\n(株)ABC\n株式会社ＡＢＣ\n株式会社ABC \n```\n\nは、人間には同じ会社に見えても、そのままでは別の文字列として扱われます。\n\nそのため実務では、\n\n```text\nCSV読み込み\n↓\n文字コード確認\n↓\nデータ整形\n↓\n重複判定\n↓\n重複削除\n↓\n件数確認\n↓\n別ファイルへ保存\n```\n\nという順番で処理した方が安全です。\n\n---\n\n## 11. 最小構成ならこれだけでも使える\n\n単純に完全一致する重複行を削除するだけなら、次のコードで処理できます。\n\n```python\nimport pandas as pd\n\ninput_file = \"input.csv\"\noutput_file = \"output.csv\"\n\ndf = pd.read_csv(input_file)\n\nbefore = len(df)\n\ncleaned = df.drop_duplicates()\n\nafter = len(cleaned)\n\ncleaned.to_csv(\n    output_file,\n    index=False,\n    encoding=\"utf-8-sig\"\n)\n\nprint(\"処理前:\", before)\nprint(\"処理後:\", after)\nprint(\"削除数:\", before - after)\nprint(\"出力:\", output_file)\n```\n\nこれだけでも、手作業で重複行を探すよりかなり楽になります。\n\n---\n\n## 12. 定型的なCSV作業は自動化できる\n\n毎日・毎週同じようなCSV整理をしている場合、\n\n```text\nCSVを受け取る\n↓\n内容をチェック\n↓\n重複を削除\n↓\nCSVを生成\n↓\n結果を返す\n```\n\nまで自動化できます。\n\n私自身も、この考え方を使ってCSV処理を自動化する仕組みを構築しています。\n\nPython環境を用意せず、CSVファイルをアップロードして重複削除を行いたい場合は、こちらでも試せるようにしています。\n\nhttps://xs669270.xsrv.jp/autoworks/articles/csv-duplicate-removal.html\n\n---\n\n## まとめ\n\nCSVの重複削除自体は、pandasなら非常に簡単です。\n\n```python\ncleaned = df.drop_duplicates()\n```\n\nただし実務では、\n\n- 何を重複と判断するか\n- どのデータを残すか\n- 文字コードをどうするか\n- 元ファイルを保護するか\n- 処理前後の件数を確認するか\n\nまで考えることが重要です。\n\n特に業務データでは、いきなり削除するのではなく、\n\n**確認 → 重複判定 → 削除 → 件数確認 → 別ファイル保存**\n\nという流れにしておくと安全です。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:55:28+09:00","group":null,"id":"66455edca4a755e5c54b","likes_count":0,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"Python","versions":[]},{"name":"pandas","versions":[]},{"name":"CSV","versions":[]},{"name":"自動化","versions":[]},{"name":"業務効率化","versions":[]}],"title":"PythonでCSVの重複行を安全に削除する方法｜pandasで完全一致・キー重複を処理","updated_at":"2026-09-27T11:55:28+09:00","url":"https://qiita.com/autoworks2/items/66455edca4a755e5c54b","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":0,"github_login_name":null,"id":"autoworks2","items_count":1,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4508086,"profile_image_url":"https://secure.gravatar.com/avatar/ced490defa9b7fbe75af2dfb734499d5","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"2:1-2:9\"\u003e\n\u003cspan id=\"環境\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%92%B0%E5%A2%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e環境\u003c/h2\u003e\n\u003cul data-sourcepos=\"4:1-7:0\"\u003e\n\u003cli data-sourcepos=\"4:1-4:31\"\u003eOS: Ubuntu 24.04 LTS (x86_64)\u003c/li\u003e\n\u003cli data-sourcepos=\"5:1-5:25\"\u003eDevice: M5StickC Plus 2\u003c/li\u003e\n\u003cli data-sourcepos=\"6:1-7:0\"\u003ePlatformIO: version 6.2.0\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"8:1-8:9\"\u003e\n\u003cspan id=\"問題\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%95%8F%E9%A1%8C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e問題\u003c/h2\u003e\n\u003cp data-sourcepos=\"10:1-10:104\"\u003eOSがUSBデバイスとして認識していることを \u003ccode\u003elsusb\u003c/code\u003e コマンドで確認しましたが、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"12:1-17:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% lsusb\n︙\nBus 001 Device 005: ID 1a86:55d4 QinHeng Electronics USB Single Serial\n︙\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"19:1-20:72\"\u003eデバイスへのファームウェアのアップロードには失敗します。\u003cbr\u003e\nどうすればアップロードできる様になるでしょうか？\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"22:1-83:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% pio run \u003cspan class=\"nt\"\u003e-e\u003c/span\u003e m5stick-c-plus2 \u003cspan class=\"nt\"\u003e-t\u003c/span\u003e upload\nProcessing m5stick-c-plus2 \u003cspan class=\"o\"\u003e(\u003c/span\u003eplatform: espressif32@6.10.0\u003cspan class=\"p\"\u003e;\u003c/span\u003e board: m5stick-c-plus2\u003cspan class=\"p\"\u003e;\u003c/span\u003e framework: arduino\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003cspan class=\"nt\"\u003e--------------------------------------------------------------------------------\u003c/span\u003e\nTool Manager: Installing platformio/tool-mkspiffs @ ~2.230.0\nDownloading  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nUnpacking  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nTool Manager: tool-mkspiffs@2.230.0 has been installed!\nTool Manager: Installing platformio/tool-mklittlefs @ ~1.203.0\nDownloading  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nUnpacking  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nTool Manager: tool-mklittlefs@1.203.210628 has been installed!\nTool Manager: Installing platformio/tool-mkfatfs @ ~2.0.0\nDownloading  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nUnpacking  \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"c\"\u003e####################################]  100%\u003c/span\u003e\nTool Manager: tool-mkfatfs@2.0.1 has been installed!\nVerbose mode can be enabled via \u003cspan class=\"sb\"\u003e`\u003c/span\u003e\u003cspan class=\"nt\"\u003e-v\u003c/span\u003e, \u003cspan class=\"nt\"\u003e--verbose\u003c/span\u003e\u003cspan class=\"sb\"\u003e`\u003c/span\u003e option\nCONFIGURATION: https://docs.platformio.org/page/boards/espressif32/m5stick-c-plus2.html\nPLATFORM: Espressif 32 \u003cspan class=\"o\"\u003e(\u003c/span\u003e6.10.0\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e M5StickC Plus2 \u003cspan class=\"o\"\u003e(\u003c/span\u003eESP32-PICO-V3-02\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nHARDWARE: ESP32 240MHz, 320KB RAM, 8MB Flash\nDEBUG: Current \u003cspan class=\"o\"\u003e(\u003c/span\u003ecmsis-dap\u003cspan class=\"o\"\u003e)\u003c/span\u003e External \u003cspan class=\"o\"\u003e(\u003c/span\u003ecmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nPACKAGES: \n - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b \n - tool-esptoolpy @ 1.40501.0 \u003cspan class=\"o\"\u003e(\u003c/span\u003e4.5.1\u003cspan class=\"o\"\u003e)\u003c/span\u003e \n - tool-mkfatfs @ 2.0.1 \n - tool-mklittlefs @ 1.203.210628 \u003cspan class=\"o\"\u003e(\u003c/span\u003e2.3\u003cspan class=\"o\"\u003e)\u003c/span\u003e \n - tool-mkspiffs @ 2.230.0 \u003cspan class=\"o\"\u003e(\u003c/span\u003e2.30\u003cspan class=\"o\"\u003e)\u003c/span\u003e \n - toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5\nLDF: Library Dependency Finder -\u0026gt; https://bit.ly/configure-pio-ldf\nLDF Modes: Finder ~ chain, Compatibility ~ soft\nFound 36 compatible libraries\nScanning dependencies...\nDependency Graph\n|-- M5Unified @ 0.2.23\nBuilding \u003cspan class=\"k\"\u003ein \u003c/span\u003erelease mode\nRetrieving maximum program size .pio/build/m5stick-c-plus2/firmware.elf\nChecking size .pio/build/m5stick-c-plus2/firmware.elf\nAdvanced Memory Usage is available via \u003cspan class=\"s2\"\u003e\"PlatformIO Home \u0026gt; Project Inspect\"\u003c/span\u003e\nRAM:   \u003cspan class=\"o\"\u003e[=\u003c/span\u003e         \u003cspan class=\"o\"\u003e]\u003c/span\u003e   7.8% \u003cspan class=\"o\"\u003e(\u003c/span\u003eused 25660 bytes from 327680 bytes\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nFlash: \u003cspan class=\"o\"\u003e[=\u003c/span\u003e         \u003cspan class=\"o\"\u003e]\u003c/span\u003e  14.5% \u003cspan class=\"o\"\u003e(\u003c/span\u003eused 484589 bytes from 3342336 bytes\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nConfiguring upload protocol...\nAVAILABLE: cmsis-dap, esp-bridge, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa\nCURRENT: upload_protocol \u003cspan class=\"o\"\u003e=\u003c/span\u003e esptool\nLooking \u003cspan class=\"k\"\u003efor \u003c/span\u003eupload port...\n\nWarning! Please \u003cspan class=\"nb\"\u003einstall\u003c/span\u003e \u003cspan class=\"sb\"\u003e`\u003c/span\u003e99-platformio-udev.rules\u003cspan class=\"sb\"\u003e`\u003c/span\u003e\u003cspan class=\"nb\"\u003e.\u003c/span\u003e \nMore details: https://docs.platformio.org/en/latest/core/installation/udev-rules.html\n\nAuto-detected: /dev/ttyACM0\nUploading .pio/build/m5stick-c-plus2/firmware.bin\nesptool.py v4.5.1\nSerial port /dev/ttyACM0\n\nA fatal error occurred: Could not open /dev/ttyACM0, the port doesn\u003cspan class=\"s1\"\u003e't exist\n*** [upload] Error 2\n========================= [FAILED] Took 11.28 seconds =========================\n\nEnvironment      Status    Duration\n---------------  --------  ------------\nm5stick-c-plus2  FAILED    00:00:11.278\n==================== 1 failed, 0 succeeded in 00:00:11.278 ====================\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"86:1-86:15\"\u003e\n\u003cspan id=\"解決方法\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解決方法\u003c/h2\u003e\n\u003cp data-sourcepos=\"88:1-88:119\"\u003eWarning が出力されている様に、\u003ccode\u003e99-platformio-udev.rules\u003c/code\u003e への書き込みによって解決しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"90:1-93:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eWarning! Please install 99-platformio-udev.rules.\nMore details: https://docs.platformio.org/en/latest/core/installation/udev-rules.html\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"95:1-95:178\"\u003e\u003ca href=\"https://docs.platformio.org/en/latest/core/installation/udev-rules.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003e記載されたリンク\u003c/a\u003e に記載されている、以下のコマンドを実行するだけです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"97:1-101:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% curl \u003cspan class=\"nt\"\u003e-fsSL\u003c/span\u003e https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | \u003cspan class=\"nb\"\u003esudo tee\u003c/span\u003e /etc/udev/rules.d/99-platformio-udev.rules\n\n% \u003cspan class=\"nb\"\u003esudo \u003c/span\u003eservice udev restart\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"103:1-103:69\"\u003eそして再度アップロードを試すと、成功しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"105:1-121:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% pio run \u003cspan class=\"nt\"\u003e-e\u003c/span\u003e m5stick-c-plus2 \u003cspan class=\"nt\"\u003e-t\u003c/span\u003e upload\n⋮\nWriting at 0x0007e2c3... \u003cspan class=\"o\"\u003e(\u003c/span\u003e94 %\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nWriting at 0x00083b79... \u003cspan class=\"o\"\u003e(\u003c/span\u003e100 %\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nWrote 484960 bytes \u003cspan class=\"o\"\u003e(\u003c/span\u003e269745 compressed\u003cspan class=\"o\"\u003e)\u003c/span\u003e at 0x00010000 \u003cspan class=\"k\"\u003ein \u003c/span\u003e4.3 seconds \u003cspan class=\"o\"\u003e(\u003c/span\u003eeffective 903.6 kbit/s\u003cspan class=\"o\"\u003e)\u003c/span\u003e...\nHash of data verified.\n\nLeaving...\nHard resetting via RTS pin...\n\u003cspan class=\"o\"\u003e=========================\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003eSUCCESS] Took 8.27 seconds \u003cspan class=\"o\"\u003e=========================\u003c/span\u003e\n\nEnvironment      Status    Duration\n\u003cspan class=\"nt\"\u003e---------------\u003c/span\u003e  \u003cspan class=\"nt\"\u003e--------\u003c/span\u003e  \u003cspan class=\"nt\"\u003e------------\u003c/span\u003e\nm5stick-c-plus2  SUCCESS   00:00:08.267\n\u003cspan class=\"o\"\u003e=========================\u003c/span\u003e 1 succeeded \u003cspan class=\"k\"\u003ein \u003c/span\u003e00:00:08.267 \u003cspan class=\"o\"\u003e=========================\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"123:1-123:9\"\u003e\n\u003cspan id=\"解説\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E8%AA%AC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解説\u003c/h2\u003e\n\u003cp data-sourcepos=\"125:1-125:348\"\u003eUbuntu 24.04 など、近年の Linux OS は \u003ccode\u003eudev\u003c/code\u003e という仕組みによってデバイスのアクセス権限を管理します。既定では、non-root ユーザによるシリアル通信デバイスへのアクセスは禁止されています。なので、OSが機器を認識していても、一般ユーザは通信できません。\u003c/p\u003e\n\u003cp data-sourcepos=\"127:1-127:292\"\u003eまた、バックグラウンドでは \u003ccode\u003eModemManager\u003c/code\u003e が通信モデムの検出を行なっており、シリアル通信デバイスとの接続を占有することがありまます。専有されている間は、別のアプリケーションとデバイスが通信できません。\u003c/p\u003e\n\u003cp data-sourcepos=\"129:1-129:103\"\u003eなので、\u003ccode\u003eudev.rules\u003c/code\u003e を適用することで、これらを適切な設定に切り替えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"131:1-131:266\"\u003e\u003ca href=\"https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules\" rel=\"nofollow noopener\" target=\"_blank\"\u003eコマンドの記載リンク\u003c/a\u003eを開くと、デバイス別の設定一覧を見ることができます。各項目は以下の通りです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"133:1-139:86\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"133:1-133:19\"\u003e\n\u003cth data-sourcepos=\"133:2-133:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"133:11-133:18\"\u003e概要\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"135:1-135:47\"\u003e\n\u003ctd data-sourcepos=\"135:2-135:18\"\u003eATTRS{idVendor}\u003c/td\u003e\n\u003ctd data-sourcepos=\"135:20-135:46\"\u003eデバイスの Vendor ID\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"136:1-136:49\"\u003e\n\u003ctd data-sourcepos=\"136:2-136:19\"\u003eATTRS{idProduct}\u003c/td\u003e\n\u003ctd data-sourcepos=\"136:21-136:48\"\u003eデバイスの Product ID\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"137:1-137:116\"\u003e\n\u003ctd data-sourcepos=\"137:2-137:8\"\u003eMODE:\u003c/td\u003e\n\u003ctd data-sourcepos=\"137:10-137:115\"\u003eデバイスに対するアクセス権限\u003cbr\u003e※0666=任意のユーザに対するread/writeの許可\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"138:1-138:92\"\u003e\n\u003ctd data-sourcepos=\"138:2-138:27\"\u003eENV{ID_MM_DEVICE_IGNORE}\u003c/td\u003e\n\u003ctd data-sourcepos=\"138:29-138:91\"\u003eModemManager による検出対象デバイスからの除外\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"139:1-139:86\"\u003e\n\u003ctd data-sourcepos=\"139:2-139:24\"\u003eENV{ID_MM_PORT_IGNORE}\u003c/td\u003e\n\u003ctd data-sourcepos=\"139:26-139:85\"\u003eModemManager による検出対象ポートからの除外\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"142:1-142:115\"\u003eつまり、udev.rules を追加することで、PlatformIO が取り扱うデバイスとの接続に関して、\u003c/p\u003e\n\u003col data-sourcepos=\"143:1-145:0\"\u003e\n\u003cli data-sourcepos=\"143:1-143:79\"\u003enon-root環境によるアクセス（例. upload / monitor) を許可する\u003c/li\u003e\n\u003cli data-sourcepos=\"144:1-145:0\"\u003eアクセスの妨げになる処理を無効化する\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"146:1-146:27\"\u003eというわけでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"148:1-148:82\"\u003e例えば、M5StickC Plus 2 に該当するエントリを見つけてみます。\u003c/p\u003e\n\u003cp data-sourcepos=\"150:1-150:103\"\u003e\u003ccode\u003elsusb\u003c/code\u003e コマンドを見ると、M5StickC は vid=1a86, pid=55d4 として認識されています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"152:1-154:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eBus 001 Device 005: ID 1a86:55d4 QinHeng Electronics USB Single Serial\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"156:1-156:125\"\u003eこれに対応するエントリを 99-platformio-udev.rules から検索すると、見つけることができました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"158:1-161:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e# QinHeng Electronics CH9102 USB-Serial adapter\nATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d4\", MODE:=\"0666\", ENV{ID_MM_DEVICE_IGNORE}=\"1\", ENV{ID_MM_PORT_IGNORE}=\"1\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n","body":"\n## 環境\n\n* OS: Ubuntu 24.04 LTS (x86_64)\n* Device: M5StickC Plus 2\n* PlatformIO: version 6.2.0\n\n## 問題\n\nOSがUSBデバイスとして認識していることを `lsusb` コマンドで確認しましたが、\n\n```bash\n% lsusb\n︙\nBus 001 Device 005: ID 1a86:55d4 QinHeng Electronics USB Single Serial\n︙\n```\n\nデバイスへのファームウェアのアップロードには失敗します。\nどうすればアップロードできる様になるでしょうか？\n\n```bash\n% pio run -e m5stick-c-plus2 -t upload\nProcessing m5stick-c-plus2 (platform: espressif32@6.10.0; board: m5stick-c-plus2; framework: arduino)\n--------------------------------------------------------------------------------\nTool Manager: Installing platformio/tool-mkspiffs @ ~2.230.0\nDownloading  [####################################]  100%\nUnpacking  [####################################]  100%\nTool Manager: tool-mkspiffs@2.230.0 has been installed!\nTool Manager: Installing platformio/tool-mklittlefs @ ~1.203.0\nDownloading  [####################################]  100%\nUnpacking  [####################################]  100%\nTool Manager: tool-mklittlefs@1.203.210628 has been installed!\nTool Manager: Installing platformio/tool-mkfatfs @ ~2.0.0\nDownloading  [####################################]  100%\nUnpacking  [####################################]  100%\nTool Manager: tool-mkfatfs@2.0.1 has been installed!\nVerbose mode can be enabled via `-v, --verbose` option\nCONFIGURATION: https://docs.platformio.org/page/boards/espressif32/m5stick-c-plus2.html\nPLATFORM: Espressif 32 (6.10.0) \u003e M5StickC Plus2 (ESP32-PICO-V3-02)\nHARDWARE: ESP32 240MHz, 320KB RAM, 8MB Flash\nDEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)\nPACKAGES: \n - framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b \n - tool-esptoolpy @ 1.40501.0 (4.5.1) \n - tool-mkfatfs @ 2.0.1 \n - tool-mklittlefs @ 1.203.210628 (2.3) \n - tool-mkspiffs @ 2.230.0 (2.30) \n - toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5\nLDF: Library Dependency Finder -\u003e https://bit.ly/configure-pio-ldf\nLDF Modes: Finder ~ chain, Compatibility ~ soft\nFound 36 compatible libraries\nScanning dependencies...\nDependency Graph\n|-- M5Unified @ 0.2.23\nBuilding in release mode\nRetrieving maximum program size .pio/build/m5stick-c-plus2/firmware.elf\nChecking size .pio/build/m5stick-c-plus2/firmware.elf\nAdvanced Memory Usage is available via \"PlatformIO Home \u003e Project Inspect\"\nRAM:   [=         ]   7.8% (used 25660 bytes from 327680 bytes)\nFlash: [=         ]  14.5% (used 484589 bytes from 3342336 bytes)\nConfiguring upload protocol...\nAVAILABLE: cmsis-dap, esp-bridge, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa\nCURRENT: upload_protocol = esptool\nLooking for upload port...\n\nWarning! Please install `99-platformio-udev.rules`. \nMore details: https://docs.platformio.org/en/latest/core/installation/udev-rules.html\n\nAuto-detected: /dev/ttyACM0\nUploading .pio/build/m5stick-c-plus2/firmware.bin\nesptool.py v4.5.1\nSerial port /dev/ttyACM0\n\nA fatal error occurred: Could not open /dev/ttyACM0, the port doesn't exist\n*** [upload] Error 2\n========================= [FAILED] Took 11.28 seconds =========================\n\nEnvironment      Status    Duration\n---------------  --------  ------------\nm5stick-c-plus2  FAILED    00:00:11.278\n==================== 1 failed, 0 succeeded in 00:00:11.278 ====================\n```\n\n\n## 解決方法\n\nWarning が出力されている様に、`99-platformio-udev.rules` への書き込みによって解決しました。\n\n```\nWarning! Please install 99-platformio-udev.rules.\nMore details: https://docs.platformio.org/en/latest/core/installation/udev-rules.html\n```\n\n[記載されたリンク](https://docs.platformio.org/en/latest/core/installation/udev-rules.html) に記載されている、以下のコマンドを実行するだけです。\n\n```zsh\n% curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules\n\n% sudo service udev restart\n```\n\nそして再度アップロードを試すと、成功しました。\n\n```zsh\n% pio run -e m5stick-c-plus2 -t upload\n⋮\nWriting at 0x0007e2c3... (94 %)\nWriting at 0x00083b79... (100 %)\nWrote 484960 bytes (269745 compressed) at 0x00010000 in 4.3 seconds (effective 903.6 kbit/s)...\nHash of data verified.\n\nLeaving...\nHard resetting via RTS pin...\n========================= [SUCCESS] Took 8.27 seconds =========================\n\nEnvironment      Status    Duration\n---------------  --------  ------------\nm5stick-c-plus2  SUCCESS   00:00:08.267\n========================= 1 succeeded in 00:00:08.267 =========================\n```\n\n## 解説\n\nUbuntu 24.04 など、近年の Linux OS は `udev` という仕組みによってデバイスのアクセス権限を管理します。既定では、non-root ユーザによるシリアル通信デバイスへのアクセスは禁止されています。なので、OSが機器を認識していても、一般ユーザは通信できません。\n\nまた、バックグラウンドでは `ModemManager` が通信モデムの検出を行なっており、シリアル通信デバイスとの接続を占有することがありまます。専有されている間は、別のアプリケーションとデバイスが通信できません。\n\nなので、`udev.rules` を適用することで、これらを適切な設定に切り替えます。\n\n[コマンドの記載リンク](https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules)を開くと、デバイス別の設定一覧を見ることができます。各項目は以下の通りです。\n\n| 項目 | 概要 |\n| --- | --- |\n| ATTRS{idVendor} | デバイスの Vendor ID |\n| ATTRS{idProduct} | デバイスの Product ID |\n| MODE: | デバイスに対するアクセス権限\u003cbr/\u003e※0666=任意のユーザに対するread/writeの許可 |\n| ENV{ID_MM_DEVICE_IGNORE} | ModemManager による検出対象デバイスからの除外 |\n|ENV{ID_MM_PORT_IGNORE} | ModemManager による検出対象ポートからの除外 |\n\n\nつまり、udev.rules を追加することで、PlatformIO が取り扱うデバイスとの接続に関して、\n1. non-root環境によるアクセス（例. upload / monitor) を許可する\n1. アクセスの妨げになる処理を無効化する\n\nというわけでした。\n\n例えば、M5StickC Plus 2 に該当するエントリを見つけてみます。\n\n`lsusb` コマンドを見ると、M5StickC は vid=1a86, pid=55d4 として認識されています。\n\n```\nBus 001 Device 005: ID 1a86:55d4 QinHeng Electronics USB Single Serial\n```\n\nこれに対応するエントリを 99-platformio-udev.rules から検索すると、見つけることができました。\n\n```\n# QinHeng Electronics CH9102 USB-Serial adapter\nATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d4\", MODE:=\"0666\", ENV{ID_MM_DEVICE_IGNORE}=\"1\", ENV{ID_MM_PORT_IGNORE}=\"1\"\n```\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:53:22+09:00","group":null,"id":"52f6402bcb26cb0a40f6","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Ubuntu","versions":[]},{"name":"PlatformIO","versions":[]},{"name":"M5stack","versions":[]},{"name":"M5StickC","versions":[]},{"name":"ESP32","versions":[]}],"title":"Ubuntu × PlatformIOのシリアルポート接続エラーとudev設定","updated_at":"2026-09-27T11:53:22+09:00","url":"https://qiita.com/isseikz/items/52f6402bcb26cb0a40f6","user":{"description":"趣味でプログラム書いたりハンダごて握ったりしています。\r\n仕事ではAndroidアプリのプログラム書いたり設計したりしています。\r\n","facebook_id":"","followees_count":16,"followers_count":16,"github_login_name":"isseikz","id":"isseikz","items_count":15,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":96165,"profile_image_url":"https://qiita-image-store.s3.amazonaws.com/0/96165/profile-images/1473706797","team_only":false,"twitter_screen_name":"mackyrollis","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:51\"\u003e\u003ciframe id=\"qiita-embed-content__8679a1aab7e3bce35e80c805ad9fecb8\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__8679a1aab7e3bce35e80c805ad9fecb8\" data-content=\"https%3A%2F%2Fqiita.com%2Fnak435%2Fitems%2F5606b7b5de9fc02d4d68\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-3:79\"\u003e今回は、現在使っている\u003ccode\u003eM4 Pro Macmini\u003c/code\u003eの環境についてです。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:184\"\u003e通常、Macには、OSにバンドルされているPython、XcodeにバンドルされているPython、そして、自分で追加インストールしたPythonが存在します。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:111\"\u003eそれらPythonのパスを調べる。\u003csub\u003e（\u003ccode\u003etype\u003c/code\u003eコマンドは \u003ccode\u003ezsh\u003c/code\u003eの組み込みコマンド ）\u003c/sub\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"9:1-11:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nb\"\u003etype\u003c/span\u003e \u003cspan class=\"nt\"\u003e-a\u003c/span\u003e python3\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"15:1-20:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"go\"\u003e% type -a python3\npython3 is an alias for /opt/homebrew/bin/python3.13\npython3 is /opt/homebrew/bin/python3\npython3 is /usr/bin/python3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"22:1-22:49\"\u003e\u003cbr\u003eそれぞれのバージョンを調べる。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"24:1-29:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% \u003cspan class=\"nb\"\u003etype\u003c/span\u003e \u003cspan class=\"nt\"\u003e-a\u003c/span\u003e python3 | \u003cspan class=\"nb\"\u003eawk\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{print $NF}'\u003c/span\u003e | xargs \u003cspan class=\"nt\"\u003e-I\u003c/span\u003e\u003cspan class=\"o\"\u003e{}\u003c/span\u003e sh \u003cspan class=\"nt\"\u003e-c\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{} -V'\u003c/span\u003e\nPython 3.13.9\nPython 3.14.0\nPython 3.9.6\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"31:1-31:34\"\u003e\u003cbr\u003eパスと一緒に欲しい。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"33:1-39:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e% \u003cspan class=\"nb\"\u003etype\u003c/span\u003e \u003cspan class=\"nt\"\u003e-a\u003c/span\u003e python3 | \u003cspan class=\"nb\"\u003eawk\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{print $NF}'\u003c/span\u003e | \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  xargs \u003cspan class=\"nt\"\u003e-I\u003c/span\u003e\u003cspan class=\"o\"\u003e{}\u003c/span\u003e sh \u003cspan class=\"nt\"\u003e-c\u003c/span\u003e \u003cspan class=\"s1\"\u003e'printf \"%-30s\\t\" \"{}\"; {} -V | cut -d\" \" -f2'\u003c/span\u003e\n/opt/homebrew/bin/python3.13    3.13.9\n/opt/homebrew/bin/python3       3.14.0\n/usr/bin/python3                3.9.6\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"41:1-41:36\"\u003e\u003cbr\u003e\u003ccode\u003ePython3.9\u003c/code\u003eの実態を発見。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"43:1-52:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"go\"\u003e% ls -l /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3\n\u003c/span\u003e\u003cspan class=\"gp\"\u003elrwxr-xr-x  1 root  wheel  9  1月 14  2026 /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3@ -\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003epython3.9\n\u003cspan class=\"go\"\u003e\n% /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -V\nPython 3.9.6\n\n% ls -l /usr/bin/python3\n-rwxr-xr-x  78 root  wheel  118640  8月 13 11:51 /usr/bin/python3*\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"54:1-54:102\"\u003e\u003ccode\u003e/usr/bin/python3\u003c/code\u003eは、\u003ccode\u003eXcode\u003c/code\u003e内の\u003ccode\u003epython3.9\u003c/code\u003eを起動するラッパープログラムらしい。\u003c/p\u003e\n\u003chr data-sourcepos=\"56:1-57:0\"\u003e\n\u003cp data-sourcepos=\"58:1-58:32\"\u003e普段は、\u003ccode\u003e3.13.9\u003c/code\u003eを使用。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"60:1-66:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"go\"\u003e% python -V\nPython 3.13.9\n\n% python3 -V\nPython 3.13.9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"68:1-68:20\"\u003e\u003cbr\u003e\u003cbr\u003eしかし、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"70:1-73:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"go\"\u003e% /usr/bin/env python3 -V\nPython 3.14.0\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"75:1-75:109\"\u003e\u003ccode\u003ealias\u003c/code\u003eで\u003ccode\u003ePython@3.13.9\u003c/code\u003eにしているのがダメで、ちゃんと設定する必要がありますね。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"77:1-83:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# .zshrc から aliasを削除\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#alias python=\"...\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#alias python3=\"...\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#alias pip=\"...\"\u003c/span\u003e\n\u003cspan class=\"c\"\u003e#alias pip3=\"...\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"85:1-130:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"gp\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003eシンボリックリンクを張る\n\u003cspan class=\"go\"\u003e% ln -sf /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python3\n% ln -sf /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python\n% ln -sf /opt/homebrew/bin/python3.13-config /opt/homebrew/bin/python3-config\n\n% ln -sf /opt/homebrew/bin/pip3.13 /opt/homebrew/bin/pip3\n% ln -sf /opt/homebrew/bin/pip3.13 /opt/homebrew/bin/pip\n\n% python -V\nPython 3.13.9\n\n% python3 -V\nPython 3.13.9\n\n% python3.14 -V\nPython 3.14.0\n\n% pip -V\npip 25.3 from /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)\n\n% pip3 -V\npip 25.3 from /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)\n\n% /usr/bin/env python3 -V\nPython 3.13.9\n\n\n\u003c/span\u003e\u003cspan class=\"gp\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e3.9はどこに？ \u003cspan class=\"nt\"\u003e---------------------------\u003c/span\u003e\n\u003cspan class=\"go\"\u003e% type -a python3.9 pip3.9\npython3.9 not found\npip3.9 not found\n\n\u003c/span\u003e\u003cspan class=\"gp\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003epython3.9 pip3.9 のリンクを張る\n\u003cspan class=\"go\"\u003e% sudo ln -sf /usr/bin/python3 /usr/local/bin/python3.9\n% sudo ln -sf /usr/bin/pip3 /usr/local/bin/pip3.9\n\n% python3.9 -V\nPython 3.9.6\n\n% pip3.9 -V\npip 21.2.4 from /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)\n\n% python3.9 -m pip -V\npip 21.2.4 from /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"132:1-132:42\"\u003e期待通りの結果になりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"136:1-136:42\"\u003e\u003cbr\u003e\u003cbr\u003e\u003cstrong\u003e以上\u003c/strong\u003e、備忘録でした。\u003c/p\u003e\n","body":"https://qiita.com/nak435/items/5606b7b5de9fc02d4d68\n\n今回は、現在使っている`M4 Pro Macmini`の環境についてです。\n\n通常、Macには、OSにバンドルされているPython、XcodeにバンドルされているPython、そして、自分で追加インストールしたPythonが存在します。\n\nそれらPythonのパスを調べる。\u003csub\u003e（`type`コマンドは `zsh`の組み込みコマンド ）\u003c/sub\u003e\n\n```bash\ntype -a python3\n```\n\n\n\n```terminal\n% type -a python3\npython3 is an alias for /opt/homebrew/bin/python3.13\npython3 is /opt/homebrew/bin/python3\npython3 is /usr/bin/python3\n```\n\n\u003cbr\u003eそれぞれのバージョンを調べる。\n\n```zsh\n% type -a python3 | awk '{print $NF}' | xargs -I{} sh -c '{} -V'\nPython 3.13.9\nPython 3.14.0\nPython 3.9.6\n```\n\n\u003cbr\u003eパスと一緒に欲しい。\n\n```zsh\n% type -a python3 | awk '{print $NF}' | \\\n  xargs -I{} sh -c 'printf \"%-30s\\t\" \"{}\"; {} -V | cut -d\" \" -f2'\n/opt/homebrew/bin/python3.13    3.13.9\n/opt/homebrew/bin/python3       3.14.0\n/usr/bin/python3                3.9.6\n```\n\n\u003cbr\u003e`Python3.9`の実態を発見。\n\n```terminal\n% ls -l /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3\nlrwxr-xr-x  1 root  wheel  9  1月 14  2026 /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3@ -\u003e python3.9\n\n% /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -V\nPython 3.9.6\n\n% ls -l /usr/bin/python3\n-rwxr-xr-x  78 root  wheel  118640  8月 13 11:51 /usr/bin/python3*\n```\n\n`/usr/bin/python3`は、`Xcode`内の`python3.9`を起動するラッパープログラムらしい。\n\n---\n\n普段は、`3.13.9`を使用。\n\n```terminal\n% python -V\nPython 3.13.9\n\n% python3 -V\nPython 3.13.9\n```\n\n\u003cbr\u003e\u003cbr\u003eしかし、\n\n```terminal\n% /usr/bin/env python3 -V\nPython 3.14.0\n```\n\n`alias`で`Python@3.13.9`にしているのがダメで、ちゃんと設定する必要がありますね。\n\n```bash\n# .zshrc から aliasを削除\n#alias python=\"...\"\n#alias python3=\"...\"\n#alias pip=\"...\"\n#alias pip3=\"...\"\n```\n\n```terminal\n# シンボリックリンクを張る\n% ln -sf /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python3\n% ln -sf /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python\n% ln -sf /opt/homebrew/bin/python3.13-config /opt/homebrew/bin/python3-config\n\n% ln -sf /opt/homebrew/bin/pip3.13 /opt/homebrew/bin/pip3\n% ln -sf /opt/homebrew/bin/pip3.13 /opt/homebrew/bin/pip\n\n% python -V\nPython 3.13.9\n\n% python3 -V\nPython 3.13.9\n\n% python3.14 -V\nPython 3.14.0\n\n% pip -V\npip 25.3 from /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)\n\n% pip3 -V\npip 25.3 from /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)\n\n% /usr/bin/env python3 -V\nPython 3.13.9\n\n\n# 3.9はどこに？ ---------------------------\n% type -a python3.9 pip3.9\npython3.9 not found\npip3.9 not found\n\n# python3.9 pip3.9 のリンクを張る\n% sudo ln -sf /usr/bin/python3 /usr/local/bin/python3.9\n% sudo ln -sf /usr/bin/pip3 /usr/local/bin/pip3.9\n\n% python3.9 -V\nPython 3.9.6\n\n% pip3.9 -V\npip 21.2.4 from /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)\n\n% python3.9 -m pip -V\npip 21.2.4 from /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)\n```\n\n期待通りの結果になりました。\n\n\n\n\u003cbr\u003e\u003cbr\u003e**以上**、備忘録でした。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:48:12+09:00","group":null,"id":"aae736437da156732427","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Mac","versions":[]},{"name":"macOS","versions":[]},{"name":"Python","versions":[]}],"title":"【備忘録】myMacのPython環境（2）","updated_at":"2026-09-27T11:48:12+09:00","url":"https://qiita.com/nak435/items/aae736437da156732427","user":{"description":"プログラミングと工作が好きです。よろしくお願いします！\r\nI like programming and crafting. Nice to meet you!!\r\n","facebook_id":"","followees_count":3,"followers_count":67,"github_login_name":"nak435","id":"nak435","items_count":352,"linkedin_id":"","location":"","name":"","organization":"qiita@nak435.com","permanent_id":283293,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/283293/5a09b105262b617041a193807baa4b2fb9ca0d1e/large.png?1619156408","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:36\"\u003eMacで成果物を作ったあと、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"3:1-3:33\"\u003e\n\u003cp data-sourcepos=\"3:3-3:33\"\u003eWindows側へどう渡すか？\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"5:1-5:33\"\u003eという問題があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:15\"\u003eもちろん、\u003c/p\u003e\n\u003cul data-sourcepos=\"9:1-13:0\"\u003e\n\u003cli data-sourcepos=\"9:1-9:14\"\u003eUSBメモリ\u003c/li\u003e\n\u003cli data-sourcepos=\"10:1-10:29\"\u003eクラウドストレージ\u003c/li\u003e\n\u003cli data-sourcepos=\"11:1-11:11\"\u003eメール\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-13:0\"\u003eチャット\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"14:1-14:27\"\u003eなどでも渡せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:140\"\u003eただ、バージョン付きの成果物として残したいなら、GitHub Releaseを使うとかなり整理しやすくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"18:1-18:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"20:1-32:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eMacで成果物を作成\n↓\nZIP化\n↓\nGit tag\n↓\nGitHub Release\n↓\nZIPをRelease Assetとして添付\n↓\nWindowsでダウンロード\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"34:1-34:24\"\u003eという流れです。\u003c/p\u003e\n\u003cp data-sourcepos=\"36:1-36:21\"\u003eこの記事では、\u003c/p\u003e\n\u003cul data-sourcepos=\"38:1-43:0\"\u003e\n\u003cli data-sourcepos=\"38:1-38:5\"\u003etag\u003c/li\u003e\n\u003cli data-sourcepos=\"39:1-39:9\"\u003eRelease\u003c/li\u003e\n\u003cli data-sourcepos=\"40:1-40:15\"\u003eRelease Asset\u003c/li\u003e\n\u003cli data-sourcepos=\"41:1-41:29\"\u003eSource code ZIPとの違い\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-43:0\"\u003e別PCで受け取る方法\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"44:1-44:24\"\u003eまで整理します。\u003c/p\u003e\n\u003chr data-sourcepos=\"46:1-47:0\"\u003e\n\u003ch2 data-sourcepos=\"48:1-48:47\"\u003e\n\u003cspan id=\"1-clone--pull-とreleaseは役割が違う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-clone--pull-%E3%81%A8release%E3%81%AF%E5%BD%B9%E5%89%B2%E3%81%8C%E9%81%95%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. clone / pull とReleaseは役割が違う\u003c/h2\u003e\n\u003cp data-sourcepos=\"50:1-50:18\"\u003e前提として、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"52:1-55:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit clone\ngit pull\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"57:1-57:44\"\u003eとGitHub Releaseは用途が違います。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"59:1-59:16\"\u003e\n\u003cspan id=\"clone--pull\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#clone--pull\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eclone / pull\u003c/h3\u003e\n\u003cp data-sourcepos=\"61:1-61:21\"\u003eこちらは主に、\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-63:85\"\u003e\u003cstrong\u003eソースコードを開発環境へ持ってきて、続きを編集するため\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"65:1-65:18\"\u003eに使います。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"67:1-67:11\"\u003e\n\u003cspan id=\"release\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#release\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eRelease\u003c/h3\u003e\n\u003cp data-sourcepos=\"69:1-69:15\"\u003eこちらは、\u003c/p\u003e\n\u003cp data-sourcepos=\"71:1-71:73\"\u003e\u003cstrong\u003e完成した成果物を、バージョン付きで配布するため\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"73:1-73:18\"\u003eに使います。\u003c/p\u003e\n\u003cp data-sourcepos=\"75:1-75:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"77:1-83:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eソースをWindowsでも編集したい\n→ clone / pull\n\nMacで作った完成ZIPをWindowsで使いたい\n→ Release\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"85:1-85:24\"\u003eという違いです。\u003c/p\u003e\n\u003cp data-sourcepos=\"87:1-87:204\"\u003eGitHub公式でも、cloneはGit履歴を含むローカルコピーを作る操作、Releaseは利用可能なソフトウェアの版を配布するための仕組みとして整理されています。\u003c/p\u003e\n\u003chr data-sourcepos=\"89:1-90:0\"\u003e\n\u003ch2 data-sourcepos=\"91:1-91:33\"\u003e\n\u003cspan id=\"2-まず成果物zipを作る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%81%BE%E3%81%9A%E6%88%90%E6%9E%9C%E7%89%A9zip%E3%82%92%E4%BD%9C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. まず成果物ZIPを作る\u003c/h2\u003e\n\u003cp data-sourcepos=\"93:1-93:21\"\u003e例えばMac側で、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"95:1-97:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emy-tool-v1.0.0.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"99:1-99:45\"\u003eという成果物を作ったとします。\u003c/p\u003e\n\u003cp data-sourcepos=\"101:1-101:30\"\u003e重要なのは、このZIPを\u003c/p\u003e\n\u003cp data-sourcepos=\"103:1-103:105\"\u003e\u003cstrong\u003e開発用リポジトリそのものではなく、利用者や別PCで使う配布物として作る\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"105:1-105:15\"\u003eことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"107:1-107:21\"\u003e例えば中身は、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"109:1-114:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eアプリ本体\nREADME\n設定ファイル\n必要な素材\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"116:1-116:15\"\u003eなどです。\u003c/p\u003e\n\u003chr data-sourcepos=\"118:1-119:0\"\u003e\n\u003ch2 data-sourcepos=\"120:1-120:45\"\u003e\n\u003cspan id=\"3-リリース対象commitを確認する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E5%AF%BE%E8%B1%A1commit%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. リリース対象commitを確認する\u003c/h2\u003e\n\u003cp data-sourcepos=\"122:1-122:57\"\u003etagを付ける前に、まず状態を確認します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"124:1-129:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit status\ngit branch \u003cspan class=\"nt\"\u003e--show-current\u003c/span\u003e\ngit log \u003cspan class=\"nt\"\u003e--oneline\u003c/span\u003e \u003cspan class=\"nt\"\u003e-5\u003c/span\u003e\ngit tag \u003cspan class=\"nt\"\u003e--list\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"131:1-131:24\"\u003e確認したいのは、\u003c/p\u003e\n\u003cul data-sourcepos=\"133:1-137:0\"\u003e\n\u003cli data-sourcepos=\"133:1-133:20\"\u003e正しいbranchか\u003c/li\u003e\n\u003cli data-sourcepos=\"134:1-134:25\"\u003eworking treeがcleanか\u003c/li\u003e\n\u003cli data-sourcepos=\"135:1-135:41\"\u003e正式版commitが確定しているか\u003c/li\u003e\n\u003cli data-sourcepos=\"136:1-137:0\"\u003e同じtagがすでに存在していないか\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"138:1-138:9\"\u003eです。\u003c/p\u003e\n\u003chr data-sourcepos=\"140:1-141:0\"\u003e\n\u003ch2 data-sourcepos=\"142:1-142:28\"\u003e\n\u003cspan id=\"4-annotated-tagを作る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-annotated-tag%E3%82%92%E4%BD%9C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. annotated tagを作る\u003c/h2\u003e\n\u003cp data-sourcepos=\"144:1-144:67\"\u003e正式版ならannotated tagを使うと分かりやすいです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"146:1-148:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit tag \u003cspan class=\"nt\"\u003e-a\u003c/span\u003e v1.0.0 \u003cspan class=\"nt\"\u003e-m\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"v1.0.0\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"150:1-150:18\"\u003e確認します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"152:1-154:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit show v1.0.0\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"156:1-156:174\"\u003eGit公式ではannotated tagは、作成日時・tagger・メッセージなどを持つtag objectで、Release用途を想定したものとして説明されています。\u003c/p\u003e\n\u003chr data-sourcepos=\"158:1-159:0\"\u003e\n\u003ch2 data-sourcepos=\"160:1-160:31\"\u003e\n\u003cspan id=\"5-tagをgithubへpushする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-tag%E3%82%92github%E3%81%B8push%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. tagをGitHubへpushする\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"162:1-164:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egit push origin v1.0.0\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"166:1-166:27\"\u003eこれでGitHub側にも、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"168:1-170:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev1.0.0\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"172:1-172:24\"\u003eが登録されます。\u003c/p\u003e\n\u003chr data-sourcepos=\"174:1-175:0\"\u003e\n\u003ch2 data-sourcepos=\"176:1-176:29\"\u003e\n\u003cspan id=\"6-github-releaseを作る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-github-release%E3%82%92%E4%BD%9C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. GitHub Releaseを作る\u003c/h2\u003e\n\u003cp data-sourcepos=\"178:1-178:39\"\u003eGitHubのリポジトリ画面から、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"180:1-184:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eReleases\n↓\nDraft a new release\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"186:1-186:18\"\u003eへ進みます。\u003c/p\u003e\n\u003cp data-sourcepos=\"188:1-188:115\"\u003eGitHub公式の現在の手順でも、Release画面で既存tagを選ぶか、新しいtagを作成できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"190:1-190:49\"\u003e今回はすでにtagをpushしているので、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"192:1-195:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eChoose a tag\n→ v1.0.0\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"197:1-197:18\"\u003eを選びます。\u003c/p\u003e\n\u003chr data-sourcepos=\"199:1-200:0\"\u003e\n\u003ch2 data-sourcepos=\"201:1-201:36\"\u003e\n\u003cspan id=\"7-release-titleとnotesを書く\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#7-release-title%E3%81%A8notes%E3%82%92%E6%9B%B8%E3%81%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e7. Release titleとnotesを書く\u003c/h2\u003e\n\u003cp data-sourcepos=\"203:1-203:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"205:1-207:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eMy Tool v1.0.0\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"209:1-209:43\"\u003eのようなRelease titleを付けます。\u003c/p\u003e\n\u003cp data-sourcepos=\"211:1-211:22\"\u003eRelease notesには、\u003c/p\u003e\n\u003cul data-sourcepos=\"213:1-218:0\"\u003e\n\u003cli data-sourcepos=\"213:1-213:17\"\u003e初回正式版\u003c/li\u003e\n\u003cli data-sourcepos=\"214:1-214:14\"\u003e主な機能\u003c/li\u003e\n\u003cli data-sourcepos=\"215:1-215:14\"\u003e動作環境\u003c/li\u003e\n\u003cli data-sourcepos=\"216:1-216:14\"\u003e利用方法\u003c/li\u003e\n\u003cli data-sourcepos=\"217:1-218:0\"\u003e注意事項\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"219:1-219:24\"\u003eなどを書きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"221:1-221:12\"\u003e単なる、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"223:1-225:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev1.0.0公開\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"227:1-227:21\"\u003eだけではなく、\u003c/p\u003e\n\u003cblockquote data-sourcepos=\"229:1-229:66\"\u003e\n\u003cp data-sourcepos=\"229:3-229:66\"\u003eこのReleaseをダウンロードすると何が使えるのか\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"231:1-231:69\"\u003eが分かる内容にしておくと後で見返しやすいです。\u003c/p\u003e\n\u003chr data-sourcepos=\"233:1-234:0\"\u003e\n\u003ch2 data-sourcepos=\"235:1-235:46\"\u003e\n\u003cspan id=\"8-zipをrelease-assetとして添付する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#8-zip%E3%82%92release-asset%E3%81%A8%E3%81%97%E3%81%A6%E6%B7%BB%E4%BB%98%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e8. ZIPをRelease Assetとして添付する\u003c/h2\u003e\n\u003cp data-sourcepos=\"237:1-237:27\"\u003e次に、Macで作った、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"239:1-241:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emy-tool-v1.0.0.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"243:1-243:43\"\u003eをRelease作成画面へ添付します。\u003c/p\u003e\n\u003cp data-sourcepos=\"245:1-245:153\"\u003eGitHub公式でも、Releaseにはcompiled programsなどのbinary filesをドラッグ＆ドロップまたはファイル選択で添付できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"249:1-249:35\"\u003eこれが\u003cstrong\u003eRelease Asset\u003c/strong\u003eです。\u003c/p\u003e\n\u003chr data-sourcepos=\"251:1-252:0\"\u003e\n\u003ch2 data-sourcepos=\"253:1-253:33\"\u003e\n\u003cspan id=\"9-source-code-zipとは別物\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9-source-code-zip%E3%81%A8%E3%81%AF%E5%88%A5%E7%89%A9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. Source code ZIPとは別物\u003c/h2\u003e\n\u003cp data-sourcepos=\"255:1-255:43\"\u003eReleaseを公開するとGitHub側でも、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"257:1-260:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eSource code (zip)\nSource code (tar.gz)\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"262:1-262:24\"\u003eが表示されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"264:1-264:111\"\u003eこれはGitHubがtag時点のリポジトリから自動生成するソースコードアーカイブです。\u003c/p\u003e\n\u003cp data-sourcepos=\"266:1-266:33\"\u003e一方、自分で添付した、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"268:1-270:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emy-tool-v1.0.0.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"272:1-272:18\"\u003eは別物です。\u003c/p\u003e\n\u003cp data-sourcepos=\"274:1-274:18\"\u003e整理すると、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"276:1-284:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eSource code (zip)\n→ GitHubが自動生成\n→ リポジトリ内容のスナップショット\n\nmy-tool-v1.0.0.zip\n→ 自分で作成\n→ 利用者・別PC向けの完成物\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"286:1-286:9\"\u003eです。\u003c/p\u003e\n\u003chr data-sourcepos=\"288:1-289:0\"\u003e\n\u003ch2 data-sourcepos=\"290:1-290:28\"\u003e\n\u003cspan id=\"10-publish-releaseする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#10-publish-release%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e10. Publish releaseする\u003c/h2\u003e\n\u003cp data-sourcepos=\"292:1-292:27\"\u003e内容を確認したら、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"294:1-296:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ePublish release\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"298:1-298:18\"\u003eを押します。\u003c/p\u003e\n\u003cp data-sourcepos=\"300:1-300:12\"\u003eこれで、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"302:1-310:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecommit\n↓\ntag\n↓\nRelease\n↓\nRelease Asset\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"312:1-312:36\"\u003eがひとつにつながります。\u003c/p\u003e\n\u003chr data-sourcepos=\"314:1-315:0\"\u003e\n\u003ch2 data-sourcepos=\"316:1-316:32\"\u003e\n\u003cspan id=\"11-windows側で受け取る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#11-windows%E5%81%B4%E3%81%A7%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e11. Windows側で受け取る\u003c/h2\u003e\n\u003cp data-sourcepos=\"318:1-318:59\"\u003eWindows側ではGitHubのReleaseページを開きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"320:1-320:15\"\u003eAssetsから、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"322:1-324:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003emy-tool-v1.0.0.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"326:1-326:51\"\u003eをクリックしてダウンロードします。\u003c/p\u003e\n\u003cp data-sourcepos=\"328:1-328:21\"\u003eこれだけです。\u003c/p\u003e\n\u003cp data-sourcepos=\"330:1-330:39\"\u003e開発を続けるわけではなく、\u003c/p\u003e\n\u003cp data-sourcepos=\"332:1-332:60\"\u003e\u003cstrong\u003e完成物を受け取りたいだけならcloneは不要\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"334:1-334:9\"\u003eです。\u003c/p\u003e\n\u003chr data-sourcepos=\"336:1-337:0\"\u003e\n\u003ch2 data-sourcepos=\"338:1-338:32\"\u003e\n\u003cspan id=\"12-windowsmacでも同じ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#12-windowsmac%E3%81%A7%E3%82%82%E5%90%8C%E3%81%98\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e12. Windows→Macでも同じ\u003c/h2\u003e\n\u003cp data-sourcepos=\"340:1-340:30\"\u003e逆方向でも同じです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"342:1-350:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eWindowsで成果物作成\n↓\nZIP\n↓\nRelease Asset\n↓\nMacでダウンロード\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"352:1-352:73\"\u003eGitHub ReleaseはOS間のファイル転送サービスというより、\u003c/p\u003e\n\u003cp data-sourcepos=\"354:1-354:49\"\u003e\u003cstrong\u003eバージョン付き成果物を置く場所\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"356:1-356:42\"\u003eとして使うのがポイントです。\u003c/p\u003e\n\u003chr data-sourcepos=\"358:1-359:0\"\u003e\n\u003ch2 data-sourcepos=\"360:1-360:35\"\u003e\n\u003cspan id=\"13-releaseを使うメリット\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#13-release%E3%82%92%E4%BD%BF%E3%81%86%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e13. Releaseを使うメリット\u003c/h2\u003e\n\u003cp data-sourcepos=\"362:1-362:12\"\u003e例えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"364:1-369:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003etool.zip\ntool-new.zip\ntool-final.zip\ntool-final2.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"371:1-371:84\"\u003eのようにファイルを管理すると、あとで分からなくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"373:1-373:16\"\u003eReleaseなら、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"375:1-381:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ev1.0.0\n  └ my-tool-v1.0.0.zip\n\nv1.0.1\n  └ my-tool-v1.0.1.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"383:1-383:18\"\u003eと残せます。\u003c/p\u003e\n\u003cp data-sourcepos=\"385:1-385:15\"\u003eそのため、\u003c/p\u003e\n\u003cul data-sourcepos=\"387:1-391:0\"\u003e\n\u003cli data-sourcepos=\"387:1-387:35\"\u003eどの版を渡したか分かる\u003c/li\u003e\n\u003cli data-sourcepos=\"388:1-388:29\"\u003e古い版を確認できる\u003c/li\u003e\n\u003cli data-sourcepos=\"389:1-389:29\"\u003etagと成果物が紐づく\u003c/li\u003e\n\u003cli data-sourcepos=\"390:1-391:0\"\u003e別PCでも同じファイルを取れる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"392:1-392:33\"\u003eという利点があります。\u003c/p\u003e\n\u003chr data-sourcepos=\"394:1-395:0\"\u003e\n\u003ch2 data-sourcepos=\"396:1-396:49\"\u003e\n\u003cspan id=\"14-最新版への直接リンクも作れる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#14-%E6%9C%80%E6%96%B0%E7%89%88%E3%81%B8%E3%81%AE%E7%9B%B4%E6%8E%A5%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%82%E4%BD%9C%E3%82%8C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e14. 最新版への直接リンクも作れる\u003c/h2\u003e\n\u003cp data-sourcepos=\"398:1-398:61\"\u003eGitHubでは、手動で添付した最新Release Assetへ、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"400:1-402:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e/releases/latest/download/asset-name.zip\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"404:1-404:63\"\u003eという形式で直接リンクすることもできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"406:1-406:54\"\u003e定型的な配布をする場合には便利です。\u003c/p\u003e\n\u003chr data-sourcepos=\"408:1-409:0\"\u003e\n\u003ch2 data-sourcepos=\"410:1-410:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"412:1-412:29\"\u003e別PCへ何か渡すとき、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"414:1-420:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e開発を続けたい\n→ clone / pull\n\n完成物だけ使いたい\n→ Release Asset\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"422:1-422:48\"\u003eと分けると整理しやすくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"424:1-424:29\"\u003eGitHub Releaseを使えば、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"426:1-434:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eMac\n↓\n完成ZIP\n↓\nv1.0.0 Release\n↓\nWindows\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"436:1-436:60\"\u003eという形で、単なるファイル転送ではなく、\u003c/p\u003e\n\u003cp data-sourcepos=\"438:1-438:70\"\u003e\u003cstrong\u003e「どのバージョンの成果物か」まで含めて残せる\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"440:1-440:24\"\u003eようになります。\u003c/p\u003e\n","body":"Macで成果物を作ったあと、\n\n\u003e Windows側へどう渡すか？\n\nという問題があります。\n\nもちろん、\n\n- USBメモリ\n- クラウドストレージ\n- メール\n- チャット\n\nなどでも渡せます。\n\nただ、バージョン付きの成果物として残したいなら、GitHub Releaseを使うとかなり整理しやすくなります。\n\n例えば、\n\n```text\nMacで成果物を作成\n↓\nZIP化\n↓\nGit tag\n↓\nGitHub Release\n↓\nZIPをRelease Assetとして添付\n↓\nWindowsでダウンロード\n```\n\nという流れです。\n\nこの記事では、\n\n- tag\n- Release\n- Release Asset\n- Source code ZIPとの違い\n- 別PCで受け取る方法\n\nまで整理します。\n\n---\n\n## 1. clone / pull とReleaseは役割が違う\n\n前提として、\n\n```text\ngit clone\ngit pull\n```\n\nとGitHub Releaseは用途が違います。\n\n### clone / pull\n\nこちらは主に、\n\n**ソースコードを開発環境へ持ってきて、続きを編集するため**\n\nに使います。\n\n### Release\n\nこちらは、\n\n**完成した成果物を、バージョン付きで配布するため**\n\nに使います。\n\n例えば、\n\n```text\nソースをWindowsでも編集したい\n→ clone / pull\n\nMacで作った完成ZIPをWindowsで使いたい\n→ Release\n```\n\nという違いです。\n\nGitHub公式でも、cloneはGit履歴を含むローカルコピーを作る操作、Releaseは利用可能なソフトウェアの版を配布するための仕組みとして整理されています。\n\n---\n\n## 2. まず成果物ZIPを作る\n\n例えばMac側で、\n\n```text\nmy-tool-v1.0.0.zip\n```\n\nという成果物を作ったとします。\n\n重要なのは、このZIPを\n\n**開発用リポジトリそのものではなく、利用者や別PCで使う配布物として作る**\n\nことです。\n\n例えば中身は、\n\n```text\nアプリ本体\nREADME\n設定ファイル\n必要な素材\n```\n\nなどです。\n\n---\n\n## 3. リリース対象commitを確認する\n\ntagを付ける前に、まず状態を確認します。\n\n```bash\ngit status\ngit branch --show-current\ngit log --oneline -5\ngit tag --list\n```\n\n確認したいのは、\n\n- 正しいbranchか\n- working treeがcleanか\n- 正式版commitが確定しているか\n- 同じtagがすでに存在していないか\n\nです。\n\n---\n\n## 4. annotated tagを作る\n\n正式版ならannotated tagを使うと分かりやすいです。\n\n```bash\ngit tag -a v1.0.0 -m \"v1.0.0\"\n```\n\n確認します。\n\n```bash\ngit show v1.0.0\n```\n\nGit公式ではannotated tagは、作成日時・tagger・メッセージなどを持つtag objectで、Release用途を想定したものとして説明されています。\n\n---\n\n## 5. tagをGitHubへpushする\n\n```bash\ngit push origin v1.0.0\n```\n\nこれでGitHub側にも、\n\n```text\nv1.0.0\n```\n\nが登録されます。\n\n---\n\n## 6. GitHub Releaseを作る\n\nGitHubのリポジトリ画面から、\n\n```text\nReleases\n↓\nDraft a new release\n```\n\nへ進みます。\n\nGitHub公式の現在の手順でも、Release画面で既存tagを選ぶか、新しいtagを作成できます。\n\n今回はすでにtagをpushしているので、\n\n```text\nChoose a tag\n→ v1.0.0\n```\n\nを選びます。\n\n---\n\n## 7. Release titleとnotesを書く\n\n例えば、\n\n```text\nMy Tool v1.0.0\n```\n\nのようなRelease titleを付けます。\n\nRelease notesには、\n\n- 初回正式版\n- 主な機能\n- 動作環境\n- 利用方法\n- 注意事項\n\nなどを書きます。\n\n単なる、\n\n```text\nv1.0.0公開\n```\n\nだけではなく、\n\n\u003e このReleaseをダウンロードすると何が使えるのか\n\nが分かる内容にしておくと後で見返しやすいです。\n\n---\n\n## 8. ZIPをRelease Assetとして添付する\n\n次に、Macで作った、\n\n```text\nmy-tool-v1.0.0.zip\n```\n\nをRelease作成画面へ添付します。\n\nGitHub公式でも、Releaseにはcompiled programsなどのbinary filesをドラッグ＆ドロップまたはファイル選択で添付できます。\n\n\n\nこれが**Release Asset**です。\n\n---\n\n## 9. Source code ZIPとは別物\n\nReleaseを公開するとGitHub側でも、\n\n```text\nSource code (zip)\nSource code (tar.gz)\n```\n\nが表示されます。\n\nこれはGitHubがtag時点のリポジトリから自動生成するソースコードアーカイブです。\n\n一方、自分で添付した、\n\n```text\nmy-tool-v1.0.0.zip\n```\n\nは別物です。\n\n整理すると、\n\n```text\nSource code (zip)\n→ GitHubが自動生成\n→ リポジトリ内容のスナップショット\n\nmy-tool-v1.0.0.zip\n→ 自分で作成\n→ 利用者・別PC向けの完成物\n```\n\nです。\n\n---\n\n## 10. Publish releaseする\n\n内容を確認したら、\n\n```text\nPublish release\n```\n\nを押します。\n\nこれで、\n\n```text\ncommit\n↓\ntag\n↓\nRelease\n↓\nRelease Asset\n```\n\nがひとつにつながります。\n\n---\n\n## 11. Windows側で受け取る\n\nWindows側ではGitHubのReleaseページを開きます。\n\nAssetsから、\n\n```text\nmy-tool-v1.0.0.zip\n```\n\nをクリックしてダウンロードします。\n\nこれだけです。\n\n開発を続けるわけではなく、\n\n**完成物を受け取りたいだけならcloneは不要**\n\nです。\n\n---\n\n## 12. Windows→Macでも同じ\n\n逆方向でも同じです。\n\n```text\nWindowsで成果物作成\n↓\nZIP\n↓\nRelease Asset\n↓\nMacでダウンロード\n```\n\nGitHub ReleaseはOS間のファイル転送サービスというより、\n\n**バージョン付き成果物を置く場所**\n\nとして使うのがポイントです。\n\n---\n\n## 13. Releaseを使うメリット\n\n例えば、\n\n```text\ntool.zip\ntool-new.zip\ntool-final.zip\ntool-final2.zip\n```\n\nのようにファイルを管理すると、あとで分からなくなります。\n\nReleaseなら、\n\n```text\nv1.0.0\n  └ my-tool-v1.0.0.zip\n\nv1.0.1\n  └ my-tool-v1.0.1.zip\n```\n\nと残せます。\n\nそのため、\n\n- どの版を渡したか分かる\n- 古い版を確認できる\n- tagと成果物が紐づく\n- 別PCでも同じファイルを取れる\n\nという利点があります。\n\n---\n\n## 14. 最新版への直接リンクも作れる\n\nGitHubでは、手動で添付した最新Release Assetへ、\n\n```text\n/releases/latest/download/asset-name.zip\n```\n\nという形式で直接リンクすることもできます。\n\n定型的な配布をする場合には便利です。\n\n---\n\n## まとめ\n\n別PCへ何か渡すとき、\n\n```text\n開発を続けたい\n→ clone / pull\n\n完成物だけ使いたい\n→ Release Asset\n```\n\nと分けると整理しやすくなります。\n\nGitHub Releaseを使えば、\n\n```text\nMac\n↓\n完成ZIP\n↓\nv1.0.0 Release\n↓\nWindows\n```\n\nという形で、単なるファイル転送ではなく、\n\n**「どのバージョンの成果物か」まで含めて残せる**\n\nようになります。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:45:09+09:00","group":null,"id":"ca2bd56b8739e04b6792","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"GitHub","versions":[]},{"name":"Git","versions":[]},{"name":"GitHubRelease","versions":[]},{"name":"macOS","versions":[]},{"name":"Windows","versions":[]}],"title":"GitHub ReleaseでZIPを配布する方法｜tag作成から別PCで受け取るまで","updated_at":"2026-09-27T11:45:09+09:00","url":"https://qiita.com/rakukotofactory/items/ca2bd56b8739e04b6792","user":{"description":"日々の小さな作業を、少し楽にするツールを作っています。Access・Excel・Office業務ファイルなど、ローカルで使える小さな業務ツールを公開しています。","facebook_id":"","followees_count":2,"followers_count":0,"github_login_name":null,"id":"rakukotofactory","items_count":24,"linkedin_id":"","location":"","name":"ラクことファクトリー","organization":"","permanent_id":4497474,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4497474/7e2a4e834fff6f763694981c60edd8b25fa968a4/large.png?1788736453","team_only":false,"twitter_screen_name":null,"website_url":"https://rakukotofactory.booth.pm/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:14\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h1\u003e\n\u003cp data-sourcepos=\"2:1-2:358\"\u003e日本語プログラミング言語Mindのバージョン９でAI仕様駆動開発（っぽいこと）を行ってみます。本記事はその第４ステップで、AI仕様駆動開発の後工程のキモ？とも言えるAI修正・ビルド・実行・再修正ワークフローの自動化をGUIアプリでやってみたという内容です。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"4:1-4:10\"\u003e\n\u003cspan id=\"使用ai\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%BF%E7%94%A8ai\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e使用AI\u003c/h1\u003e\n\u003ch2 data-sourcepos=\"5:1-5:20\"\u003e\n\u003cspan id=\"microsoft-copilot\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#microsoft-copilot\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eMicrosoft Copilot\u003c/h2\u003e\n\u003cp data-sourcepos=\"6:1-6:228\"\u003eWindows11アプリケーションのMicrosoft Copilot。昨年末から日本語プログラミング言語あおい Aoi（Re:Mind）の原型を作成してもらった生成AI\u003csup\u003e\u003ca href=\"#fn-2\" id=\"fnref-2\"\u003e1\u003c/a\u003e\u003c/sup\u003e\u003csup\u003e\u003ca href=\"#fn-3\" id=\"fnref-3\"\u003e2\u003c/a\u003e\u003c/sup\u003e\u003csup\u003e\u003ca href=\"#fn-4\" id=\"fnref-4\"\u003e3\u003c/a\u003e\u003c/sup\u003e。モードは「無料プラン」。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"7:1-7:17\"\u003e\n\u003cspan id=\"github-copilot\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#github-copilot\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eGitHub Copilot\u003c/h2\u003e\n\u003cp data-sourcepos=\"8:1-8:293\"\u003eGitHub CopilotのPro+版。あおい実装\u003csup\u003e\u003ca href=\"#fn-1\" id=\"fnref-1\"\u003e4\u003c/a\u003e\u003c/sup\u003eと検証の主担当。スーパバイザとしてMicrosoft Copilotを使いGitHub CopilotのCodeAgent推論量を最小化するプロンプトをCopilotで生成しGitHub CopilotのCodeAgentで実行という工法？を採用しています。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"11:1-11:26\"\u003e\n\u003cspan id=\"本記事のステップ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%AC%E8%A8%98%E4%BA%8B%E3%81%AE%E3%82%B9%E3%83%86%E3%83%83%E3%83%97\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e本記事のステップ\u003c/h1\u003e\n\u003cp data-sourcepos=\"12:1-13:275\"\u003e\u003ca href=\"https://qiita.com/mylifewithviolin/items/34b835716331a64345aa\" id=\"reference-5c929e35f61c2364f981\"\u003eステップ２\u003c/a\u003eにてGitHub CopilotがMindの言語仕様に準じて修正・ビルド・再修正ワークフローを廻して、コンパイルエラーがなくせるところまで自律的に動かせました。\u003cbr\u003e\n\u003ca href=\"https://qiita.com/mylifewithviolin/items/afd36f0deb93df945d59\" id=\"reference-ae7b5d28c05c9cde8643\"\u003eステップ３\u003c/a\u003eでアプリ仕様について調整しましたので、本ステップではアプリ仕様に到達するまで修正・ビルド・実行・再修正ワークフローを廻してみます。\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-15:242\"\u003eステップ２ではGUIアプリの実行と状態判定は人間が行っていましたが、本ステップではAIがGUIアプリを実行して状態を判定してソースコードの修正に入るという点がポイントです。\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-17:290\"\u003eただし、仕様実現の可否には画面の動きの総合的な判定が必要になりますので、そこは人間系が行い、AIが自律的に不具合修正するのは実行時にアプリが落ちてしまうとか実行時の致命的エラー改修にとどまります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"19:1-19:38\"\u003e\n\u003cspan id=\"microsoft-copilotとの事前準備\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#microsoft-copilot%E3%81%A8%E3%81%AE%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eMicrosoft Copilotとの事前準備\u003c/h2\u003e\n\u003cp data-sourcepos=\"20:1-20:111\"\u003e本ステップではだいたいこれでいけるだろうと考えていたのでとくにありません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"21:1-21:17\"\u003e\n\u003cspan id=\"github-copilot-1\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#github-copilot-1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eGitHub Copilot\u003c/h2\u003e\n\u003cp data-sourcepos=\"22:1-22:142\"\u003eステップ３の仕様調整はその後もいろいろ調整が続いて、現在の修正方針mdは下記のv4となっております。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:34\"\u003e\u003cstrong\u003edocs\\firework修正方針v4.md\u003c/strong\u003e\u003c/p\u003e\n\u003ch4 data-sourcepos=\"25:1-25:20\"\u003e\n\u003cspan id=\"9-完了条件\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#9-%E5%AE%8C%E4%BA%86%E6%9D%A1%E4%BB%B6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e9. 完了条件\u003c/h4\u003e\n\u003cul data-sourcepos=\"27:1-35:0\"\u003e\n\u003cli data-sourcepos=\"27:1-27:87\"\u003e\n\u003ccode\u003ecanvas_color_control.src\u003c/code\u003e がSJIS/CP932で保存され、コンパイルできる。\u003c/li\u003e\n\u003cli data-sourcepos=\"28:1-28:153\"\u003ecanvas作成、消去、円、楕円、塗り色、線色、描画、移動の単語がTcl/Tk canvasの確認済みAPIだけで構成されている。\u003c/li\u003e\n\u003cli data-sourcepos=\"29:1-29:121\"\u003e\n\u003ccode\u003eFireworkTextEffect\u003c/code\u003e を除くC#の通常花火の状態、粒子範囲、6色、30ms周期をMind版へ反映する。\u003c/li\u003e\n\u003cli data-sourcepos=\"30:1-30:127\"\u003e爆発粒子を白色固定残光で覆わず、半径1〜3のカラー粒子が不規則な方向・速度で拡散する。\u003c/li\u003e\n\u003cli data-sourcepos=\"31:1-31:137\"\u003e画面端の座標クランプによる粒子集中、整数速度の丸めによる規則的な多角形状が目視で残らない。\u003c/li\u003e\n\u003cli data-sourcepos=\"32:1-32:57\"\u003eビルド後に \u003ccode\u003efirework.inf\u003c/code\u003e が生成されない。\u003c/li\u003e\n\u003cli data-sourcepos=\"33:1-33:108\"\u003e文字花火、未確認のARGB、Bitmap画素取得、未確認の後置構文を追加していない。\u003c/li\u003e\n\u003cli data-sourcepos=\"34:1-35:0\"\u003e追加検証の結果と実行時のGUI状態を別途記録する。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"36:1-37:206\"\u003eここにむかった改修の途中でWindowsフォームが実行中（実行開始の描画初期段階で）予期せず閉じてしまうという不具合が発生してしまいます。\u003cbr\u003e\n当初はGUIアプリの実行と落ちる段階をダイアログ挿入でユーザーが介在することで判定していたのですが、ここでGitHub Copilotが重要な提案をしてきます。\u003c/p\u003e\n\u003ch4 data-sourcepos=\"39:1-39:26\"\u003e\n\u003cspan id=\"応答レスポンス\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%BF%9C%E7%AD%94%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e応答レスポンス\u003c/h4\u003e\n\u003cblockquote data-sourcepos=\"40:1-40:494\"\u003e\n\u003cp data-sourcepos=\"40:2-40:494\"\u003e状況から、単に次回予約を止めるだけでは十分でない可能性があります。診断1〜10の複数のモーダルダイアログが同じ非同期イベント処理内で実行されているので、ダイアログ自体がイベントスタックを乱している仮説が強まりました。イベント内のモーダル表示をなくし、代わりに後から読める診断記録へ切り替えられるか、Mindのファイル出力仕様を確認します。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote data-sourcepos=\"42:1-42:388\"\u003e\n\u003cp data-sourcepos=\"42:2-42:388\"\u003eファイル出力マニュアルで、開いたログファイルへ文字列を書き込み、改行を付ける標準APIが使えることを確認しました。これなら非同期イベント内でモーダルダイアログを開かずに段階を記録できます。必要なオープン／書き込み／クローズの正確な構文を確認してから置き換えます。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp data-sourcepos=\"44:1-44:492\"\u003eこれならGUIアプリでも自分で起動して、ログを見て結果を判定して不具合箇所を段階的に特定して修正できますねというやりとりになって、GitHub Copilotが起動を試みてくれたりしていたのですが（起動自体は以前からやってくれていたのですが）起動したMind由来のWindowフォームに出力した文字を見ると、ランタイムが未発見というよく見るエラーが確認されました。\u003c/p\u003e\n\u003cp data-sourcepos=\"46:1-46:396\"\u003eこれはGitHub Copilotが起動したときの状態で、わたしは環境設定PATHを実行したターミナルから起動していますので、これが起きないことは自明として認識していました。GitHub Copilotが起動するときは都度内部ターミナルのインスタンスを使うので、結局下記のようにVSCodeのtasks.jsonを修正しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"48:1-48:13\"\u003e\n\u003cspan id=\"tasksjson\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tasksjson\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003etasks.json\u003c/h2\u003e\n\u003cp data-sourcepos=\"49:1-49:433\"\u003eGitHub Copilotが最終的に修正した状態。GitHub Copilotが処理中に私がVSCodeで他のファイルを開いたりするので、runタスクのターゲットファイルはリテラル書きとなりました。call C:\\Mind9\\usemind.batをアプリの起動毎に実行してもらうというのがポイントです。またターミナルをコマンドプロンプトで起動するよう設定しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"51:1-83:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.vscode\\tasks.json\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"version\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"2.0.0\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"tasks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"label\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"build\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"shell\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"call C:\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eMind9\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eusemind.bat \u0026amp;\u0026amp; mind ${fileBasenameNoExtension} guilib\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"group\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"build\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"options\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"shell\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"executable\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"C:\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eWindows\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eSystem32\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003ecmd.exe\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"args\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"/d\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"/c\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"problemMatcher\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"label\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"run\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"shell\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"call C:\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eMind9\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eusemind.bat \u0026amp;\u0026amp; firework\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"group\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"test\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"options\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"shell\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"executable\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"C:\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eWindows\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003eSystem32\u003c/span\u003e\u003cspan class=\"se\"\u003e\\\\\u003c/span\u003e\u003cspan class=\"s2\"\u003ecmd.exe\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"args\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"/d\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"/c\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"problemMatcher\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"85:1-85:83\"\u003e\n\u003cspan id=\"お題のソースコードaiによるデバッグ診断ログ追記状態\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E9%A1%8C%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89ai%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E8%A8%BA%E6%96%AD%E3%83%AD%E3%82%B0%E8%BF%BD%E8%A8%98%E7%8A%B6%E6%85%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eお題のソースコード（AIによるデバッグ診断ログ追記状態）\u003c/h2\u003e\n\u003cp data-sourcepos=\"86:1-86:250\"\u003eGitHub Copilotがデバッグ診断ログの出力を追加した状態のソースコードです。それがなくても既にだいぶ長大なソースコードをMind9コンパイラがエラーなしで完走する状態となっています。\u003c/p\u003e\n\u003cdetails\u003e\u003csummary\u003efirework.src AIによるデバッグ診断ログ追記状態\u003c/summary\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"mind\" data-sourcepos=\"89:1-664:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eeval\\firework.src\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\"snowdrops\\\\randomtcl.src\"を　コンパイル。\n\"canvas_color_control.src\"を　コンパイル。\n\nキャンバスは　ＩＤ。\nキャンバス幅は　定数　１２００。\nキャンバス高は　定数　６００。\n粒子数は　定数　２０４。\n主粒子数は　定数　１８０。\n外周粒子数は　定数　２４。\n花火の数は　定数　４。\n\n粒子型は　型紙\n        円ＩＤは　図形ＩＤ\n        X座標は　変数\n        Y座標は　変数\n        X速度は　変数\n        Y速度は　変数\n        粒子半径は　変数\n        寿命は　変数\n                使用中は　変数\n                粒子色番号は　変数\n        粒子本体は　円ＩＤと　X座標と　Y座標と　X速度と　Y速度と　粒子半径と　寿命と　使用中と　粒子色番号\n        粒子列は　粒子数の　粒子本体\n        全体は　花火の数の　粒子列。\n\n花火型は　型紙\n                花火Xは　変数\n                花火Yは　変数\n                花火目標Yは　変数\n                花火上昇速度は　変数\n                花火主粒子数は　変数\n                花火半径は　変数\n                花火活動中は　変数\n                花火爆発済みは　変数\n                花火寿命は　変数\n        花火本体は　花火Xと　花火Yと　花火目標Yと　花火上昇速度と　花火主粒子数と　花火半径と　花火活動中と　花火爆発済みと　花火寿命\n        全体は　花火の数の　花火本体。\n\n花火粒子群は　構造体　粒子型。\n花火群は　構造体　花火型。\n\n花火番号は　変数。\n粒子番号は　変数。\n花火中心円ＩＤは　図形ＩＤ。\n残光円ＩＤは　図形ＩＤ。\n速度は　変数。\n方向は　変数。\n色番号は　変数。\nフレーム数は　変数。\n診断花火番号は　変数。\n診断段階は　変数。\n診断ログは　ファイル。\n診断ログ有効は　変数。\n\n診断を記録するとは　（文字列　→　・）\n        記録文は　文字列\n        記録文に　入れる\n        診断ログ有効が　１に　等しい　ならば\n                \"firework_diagnostic.log\"で　診断ログを　新規オープンする\n                エラー？\n                        ならば　０を　診断ログ有効に　入れ\n                        さもなければ\n                                記録文を　診断ログに　一行書き込み\n                                診断ログを　クローズする\n                                エラー？\n                                        ならば　０を　診断ログ有効に　入れ\n                                        つぎに\n                        つぎに\n        つぎに。\n\n粒子を初期化するとは\n        粒子数で　回数指定し\n                粒子番号に　回数を　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                ３を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n        繰り返す。\n\n花火を作るとは\n        乱数値は　小数変数\n        範囲指定の疑似乱数２(１００,１１００)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火X（花火番号）に　入れ\n        ６００を　花火群の　花火Y（花火番号）に　入れ\n        範囲指定の疑似乱数２(１００,３００)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火目標Y（花火番号）に　入れ\n        範囲指定の疑似乱数２(８,１３)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火上昇速度（花火番号）に　入れ\n        範囲指定の疑似乱数(１２０,１８０)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火主粒子数（花火番号）に　入れ\n        範囲指定の疑似乱数２(７０,１１０)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火半径（花火番号）に　入れ\n        １を　花火群の　花火活動中（花火番号）に　入れ\n        ０を　花火群の　花火爆発済み（花火番号）に　入れ\n        ０を　花火群の　花火寿命（花火番号）に　入れ\n        粒子を初期化する。\n\n花火を爆発させるとは\n        乱数値は　小数変数\n        角度は　小数変数\n        速度小数は　小数変数\n        X速度小数は　小数変数\n        Y速度小数は　小数変数\n        揺らぎは　小数変数\n        主粒子数現在は　変数\n        速度層は　変数\n        外周番号は　変数\n        外周距離は　変数\n        外周速度は　変数\n        外周最大半径は　変数\n        １を　花火群の　花火爆発済み（花火番号）に　入れ\n        ２４を　花火群の　花火寿命（花火番号）に　入れ\n        診断花火番号が　０に　等しい　ならば\n                花火番号を　診断花火番号に　入れ\n                \"DIAG_01_EXPLOSION_BEGIN\"を　診断を記録する\n        つぎに\n        花火群の　花火主粒子数（花火番号）を　主粒子数現在に　入れ\n        花火群の　花火主粒子数（花火番号）で　回数指定し\n                粒子番号に　回数を　入れ\n                [角度 := PI * 2.0 * 粒子番号 / 主粒子数現在]をつみ\n                範囲指定の疑似乱数(０,３)を　揺らぎに　入れ\n                [揺らぎ := 揺らぎ - 1.0]をつみ\n                [角度 := 角度 + 揺らぎ / 12.0]をつみ\n                粒子番号を　３で　割った余りを　速度層に　入れ\n                速度層が　０に　等しい　ならば\n                        範囲指定の疑似乱数(２,５)を　速度小数に　入れ\n                つぎに\n                速度層が　１に　等しい　ならば\n                        範囲指定の疑似乱数(４,８)を　速度小数に　入れ\n                つぎに\n                速度層が　２に　等しい　ならば\n                        範囲指定の疑似乱数(７,１２)を　速度小数に　入れ\n                つぎに\n                cos(角度)を　X速度小数に　入れ\n                sin(角度)を　Y速度小数に　入れ\n                [X速度小数 := X速度小数 * 速度小数]をつみ\n                [Y速度小数 := Y速度小数 * 速度小数]をつみ\n                X速度小数を　整数化し　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                Y速度小数を　整数化し　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,４)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,７)を　色番号に　入れ\n                色番号を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数２(１８,２８)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n        繰り返す\n        花火番号が　診断花火番号に　等しい　ならば\n                \"DIAG_02_MAIN_PARTICLES_DONE\"を　診断を記録する\n        つぎに\n        花火群の　花火主粒子数（花火番号）を　粒子番号に　入れ\n        外周粒子数で　回数指定し\n                外周番号に　回数を　入れ\n                花火群の　花火主粒子数（花火番号）に　外周番号を　加え　粒子番号に　入れ\n                [角度 := PI * 2.0 * 外周番号 / 外周粒子数]をつみ\n                範囲指定の疑似乱数(０,３)を　揺らぎに　入れ\n                [揺らぎ := 揺らぎ - 1.0]をつみ\n                [角度 := 角度 + 揺らぎ / 12.0]をつみ\n                花火群の　花火半径（花火番号）を　外周最大半径に　入れ\n                [外周最大半径 := 外周最大半径 / 2]をつみ\n                範囲指定の疑似乱数(２０,外周最大半径)を　外周距離に　入れ\n                範囲指定の疑似乱数２(６,１０)を　外周速度に　入れ\n                外周距離に　外周速度を　加え　速度に　入れ\n                cos(角度)を　X速度小数に　入れ\n                sin(角度)を　Y速度小数に　入れ\n                [X速度小数 := X速度小数 * 速度]をつみ\n                [Y速度小数 := Y速度小数 * 速度]をつみ\n                X速度小数を　整数化し　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                Y速度小数を　整数化し　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数２(１６,２６)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,３)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,７)を　色番号に　入れ\n                色番号を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n        繰り返す\n        花火番号が　診断花火番号に　等しい　ならば\n                \"DIAG_03_OUTER_PARTICLES_DONE\"を　診断を記録する\n        つぎに。\n花火群を初期化するとは\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火を作る\n        繰り返す。\n\n花火を更新するとは\n        Xは　変数\n        Yは　変数\n        花火群の　花火活動中（花火番号）が　０に　等しい\n                ならば　終わる　つぎに\n        花火群の　花火爆発済み（花火番号）が　０に　等しい\n                ならば\n                        花火群の　花火Y（花火番号）から　花火群の　花火上昇速度（花火番号）を　引き　花火群の　花火Y（花火番号）に　入れ\n                        花火群の　花火Y（花火番号）が　花火群の　花火目標Y（花火番号）　以下\n                                ならば\n                                        花火群の　花火目標Y（花火番号）を　花火群の　花火Y（花火番号）に　入れ\n                                        花火を爆発させる\n                                つぎに\n                つぎに\n        花火群の　花火爆発済み（花火番号）が　１に　等しい\n                ならば\n                        花火群の　花火寿命（花火番号）が　２４に　等しい\n                                ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                \"DIAG_04_PARTICLE_UPDATE_BEGIN\"を　診断を記録する\n                                        つぎに\n                                つぎに\n                        粒子数で　回数指定し\n                                粒子番号に　回数を　入れ\n                                花火粒子群の　使用中（花火番号，粒子番号）が　１に　等しい　ならば\n                                        花火粒子群の　X座標（花火番号，粒子番号）に　花火粒子群の　X速度（花火番号，粒子番号）を　加え　Xに　入れ\n                                        Xを　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　Y座標（花火番号，粒子番号）に　花火粒子群の　Y速度（花火番号，粒子番号）を　加え　Yに　入れ\n                                        Yを　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　Y速度（花火番号，粒子番号）を　一つ増加し\n                                        花火粒子群の　寿命（花火番号，粒子番号）から　１を　引き　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　１６　以下\n                                                ならば　２を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　８　以下\n                                                ならば　１を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　０　以下\n                                                ならば　０を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                つぎに\n                        繰り返す\n                        花火群の　花火寿命（花火番号）が　２４に　等しい\n                                ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                \"DIAG_05_PARTICLE_UPDATE_DONE\"を　診断を記録する\n                                        つぎに\n                                つぎに\n                        花火群の　花火寿命（花火番号）から　１を　引き　花火群の　花火寿命（花火番号）に　入れ\n                        花火群の　花火寿命（花火番号）が　０　以下\n                                ならば　０を　花火群の　花火活動中（花火番号）に　入れ\n                        つぎに\n                つぎに\n        。\n\n花火群を更新するとは\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火群の　花火活動中（花火番号）が　１に　等しい\n                        ならば　花火を更新する\n                        つぎに\n                花火番号が　診断花火番号に　等しい　ならば\n                        花火群の　花火爆発済み（花火番号）が　１に　等しい　ならば\n                                診断段階が　０に　等しい　ならば\n                                        \"STACK_AFTER_FIRST_EXPLOSION_UPDATE\"を　診断を記録する\n                                        「最初の爆発を含む花火更新直後」で　このメッセージでスタック検査し\n                                        \"STACK_FIRST_EXPLOSION_UPDATE_OK\"を　診断を記録する\n                                つぎに\n                        つぎに\n                つぎに\n                花火群の　花火活動中（花火番号）が　０に　等しい\n                        ならば　花火を作る\n                        つぎに\n        繰り返す。\n\n花火を描くとは\n        描画Xは　変数\n        描画Yは　変数\n        描画範囲内は　変数\n        粒子区分は　変数\n        花火番号が　診断花火番号に　等しい　ならば\n                診断段階が　０に　等しい　ならば\n                        \"STACK_DRAW_ENTRY\"を　診断を記録する\n                        「診断対象花火描画の入口」で　このメッセージでスタック検査し\n                        \"STACK_DRAW_ENTRY_OK\"を　診断を記録する\n                つぎに\n        つぎに\n        花火群の　花火活動中（花火番号）が　１に　等しい\n                ならば\n        花火群の　花火爆発済み（花火番号）が　０に　等しい\n                ならば\n                        キャンバスと　花火群の　花火X（花火番号）と　花火群の　花火Y（花火番号）と　６で　円のＩＤを取得し　花火中心円ＩＤに　入れ\n                        白色で　花火中心円ＩＤに　満たす色を設定し\n                        花火中心円ＩＤを　描画する\n                さもなければ\n                        粒子数で　回数指定し\n                                粒子番号に　回数を　入れ\n                                花火粒子群の　使用中（花火番号，粒子番号）が　１に　等しい　ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                診断段階が　０に　等しい　ならば\n                                                        粒子番号を　８で　割った余りを　粒子区分に　入れ\n                                                        粒子区分が　０に　等しい　ならば\n                                                                粒子番号を　文字列変換し\n                                                                \"PARTICLE_BATCH_\"と　変換文字列を　合成し　診断を記録する\n                                                                「粒子区分の描画前」で　このメッセージでスタック検査し\n                                                                \"STACK_PARTICLE_BATCH_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　X座標（花火番号，粒子番号）を　描画Xに　入れ\n                                        花火粒子群の　Y座標（花火番号，粒子番号）を　描画Yに　入れ\n                                        ０を　描画範囲内に　入れ\n                                        描画Xが　０以上　ならば\n                                                描画Yが　０以上　ならば\n                                                        描画Xが　キャンバス幅より　小さい　ならば\n                                                                描画Yが　キャンバス高より　小さい　ならば\n                                                                        １を　描画範囲内に　入れ\n                                                                つぎに\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        描画範囲内が　１に　等しい　ならば\n                                        キャンバスと　描画Xと　描画Yと　花火粒子群の　粒子半径（花火番号，粒子番号）で　円のＩＤを取得し　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　入れ\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_AFTER_FIRST_CIRCLE_ID\"を　診断を記録する\n                                                                「最初の粒子の円ID取得後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_CIRCLE_ID_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　１に　等しい　ならば\n                                                花火橙色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　２に　等しい　ならば\n                                                花火深紅色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　３に　等しい　ならば\n                                                花火金色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　４に　等しい　ならば\n                                                花火空色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　５に　等しい　ならば\n                                                花火黄緑色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　６に　等しい　ならば\n                                                花火紫紅色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_BEFORE_FIRST_PARTICLE_DRAW\"を　診断を記録する\n                                                                「最初の粒子の色設定後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_PARTICLE_COLOR_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　円ＩＤ（花火番号，粒子番号）を　描画する\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　２以上　ならば\n                                                        粒子番号が　８以下　ならば\n                                                                診断段階が　０に　等しい　ならば\n                                                                        粒子番号を　文字列変換し\n                                                                        \"STACK_AFTER_PARTICLE_\"と　変換文字列を　合成し　診断を記録する\n                                                                        「粒子描画後」で　このメッセージでスタック検査し\n                                                                        \"STACK_PARTICLE_STEP_OK\"を　診断を記録する\n                                                                つぎに\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_AFTER_FIRST_PARTICLE_DRAW\"を　診断を記録する\n                                                                「最初の粒子を描画した直後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_PARTICLE_DRAW_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        つぎに\n                                つぎに\n                        繰り返す\n                        花火番号が　診断花火番号に　等しい　ならば\n                                診断段階が　０に　等しい　ならば\n                                        \"STACK_PARTICLE_LOOP_DONE\"を　診断を記録する\n                                        「診断対象花火の粒子描画ループ末尾」で　このメッセージでスタック検査し\n                                        \"STACK_PARTICLE_LOOP_OK\"を　診断を記録する\n                                つぎに\n                        つぎに\n                つぎに\n                        つぎに\n        花火番号が　診断花火番号に　等しい　ならば\n                \"STACK_DRAW_WORD_RETURN\"を　診断を記録する\n                「花火を描く処理語の末尾」で　このメッセージでスタック検査し\n                \"STACK_DRAW_WORD_RETURN_OK\"を　診断を記録する\n        つぎに\n        。\n\n花火を再描画するとは\n        キャンバスを　キャンバスをクリアし\n        花火群を更新する\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火群の　花火寿命（花火番号）が　２３に　等しい\n                        ならば\n                                花火番号が　診断花火番号に　等しい　ならば\n                                        \"DIAG_06_TARGET_DRAW_BEGIN\"を　診断を記録する\n                                つぎに\n                        つぎに\n                花火を描く\n                花火番号が　診断花火番号に　等しい　ならば\n                        診断段階が　０に　等しい　ならば\n                                \"STACK_AFTER_TARGET_DRAW\"を　診断を記録する\n                                「診断対象花火の描画直後」で　このメッセージでスタック検査し\n                                \"STACK_TARGET_DRAW_OK\"を　診断を記録する\n                        診断段階が　８に　等しい　ならば\n                                \"STACK_AFTER_DRAW_LOOP\"を　診断を記録する\n                                「花火描画ループの直後」で　このメッセージでスタック検査し\n                                \"STACK_DRAW_LOOP_OK\"を　診断を記録する\n                        つぎに\n                        つぎに\n                つぎに\n                花火群の　花火寿命（花火番号）が　２３に　等しい\n                        ならば\n                                花火番号が　診断花火番号に　等しい　ならば\n                                        \"DIAG_07_TARGET_DRAW_DONE\"を　診断を記録する\n                                        ７を　診断段階に　入れ\n                                つぎに\n                        つぎに\n        繰り返す\n        診断段階が　７に　等しい　ならば\n                \"DIAG_08_ALL_FIREWORKS_DRAW_DONE\"を　診断を記録する\n                ８を　診断段階に　入れ\n        つぎに\n        診断段階が　８に　等しい　ならば\n                \"STACK_AFTER_DRAW_LOOP\"を　診断を記録する\n                「花火描画ループの直後」で　このメッセージでスタック検査し\n                \"STACK_DRAW_LOOP_OK\"を　診断を記録する\n        つぎに\n        。\n\n遅延後の花火処理とは　仮定義。\n\n次の花火処理を予約とは\n        ３０ミリと　遅延後の花火処理の　実行情報と　空列で　遅延実行を予約\n        診断段階が　１０に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「2回目の遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「3回目の遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに。\n\n遅延後の花火処理とは　本定義　（引数１、引数２、・・、引数個数　→　・）\n        イベント引数を捨て\n        診断段階が　０に　等しい　ならば\n                \"STACK_FIRST_EVENT_ENTRY\"を　診断を記録する\n                「初回遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_FIRST_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「2回目の遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「3回目の遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"DIAG_11_FIRST_EVENT_BEGIN\"を　診断を記録する\n                １１を　診断段階に　入れ\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"DIAG_14_SECOND_EVENT_BEGIN\"を　診断を記録する\n                １４を　診断段階に　入れ\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"DIAG_17_THIRD_EVENT_BEGIN\"を　診断を記録する\n                １７を　診断段階に　入れ\n        つぎに\n        花火を再描画する\n        診断段階が　８に　等しい　ならば\n                \"STACK_FIRST_REDRAW_RETURN\"を　診断を記録する\n                「初回再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_FIRST_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１１に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「最初の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１４に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「2回目の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１７に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「3回目の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　８に　等しい　ならば\n                \"DIAG_09_REDRAW_RETURNED\"を　診断を記録する\n                ９を　診断段階に　入れ\n        つぎに\n        診断段階が　９に　等しい　ならば\n                \"DIAG_10_BEFORE_NEXT_DELAY\"を　診断を記録する\n                １０を　診断段階に　入れ\n        つぎに\n        診断段階が　１１に　等しい　ならば\n                \"DIAG_12_FIRST_FRAME_DONE\"を　診断を記録する\n                １２を　診断段階に　入れ\n        つぎに\n        診断段階が　１２に　等しい　ならば\n                \"DIAG_13_BEFORE_SECOND_DELAY\"を　診断を記録する\n                １３を　診断段階に　入れ\n        つぎに\n        診断段階が　１４に　等しい　ならば\n                \"DIAG_15_SECOND_FRAME_DONE\"を　診断を記録する\n                １５を　診断段階に　入れ\n        つぎに\n        診断段階が　１５に　等しい　ならば\n                \"DIAG_16_BEFORE_THIRD_DELAY\"を　診断を記録する\n                １６を　診断段階に　入れ\n        つぎに\n        診断段階が　１７に　等しい　ならば\n                \"DIAG_18_THIRD_FRAME_DONE\"を　診断を記録する\n                １８を　診断段階に　入れ\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「初回遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「2回目の遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「3回目の遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        次の花火処理を予約すること\n        。\n\nメインとは\n        ０を　診断花火番号に　入れ\n        ０を　診断段階に　入れ\n        １を　診断ログ有効に　入れ\n        \"APP_READY\"を　診断を記録する\n        メインウィンドウと　キャンバス幅と　キャンバス高と　黒色で\n                キャンバスを立て　キャンバスに　入れ\n        花火群を初期化する\n        花火を再描画する\n        \"INITIALIZATION_COMPLETE\"を　診断を記録する\n        次の花火処理を予約する。\n\nメインウィンドウクローズ処理とは　本定義　（・　→　・）\n        \"NORMAL_SHUTDOWN\"を　診断を記録する\n        実行終りすること。\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003c/details\u003e\n\u003ch2 data-sourcepos=\"667:1-667:15\"\u003e\n\u003cspan id=\"診断ログ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A8%BA%E6%96%AD%E3%83%AD%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e診断ログ\u003c/h2\u003e\n\u003cp data-sourcepos=\"668:1-669:27\"\u003e下記のような診断ログで終了時の最終値を記録します。下記は現在の異常終了の状態。\u003cbr\u003e\n\u003cstrong\u003efirework_diagnostic.log\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"670:1-672:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003efirework_diagnostic.log\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ePARTICLE_BATCH_8\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"673:1-673:99\"\u003e正常終了時は下記のコードによりNORMAL_SHUTDOWNを記録するとなっています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"mind\" data-sourcepos=\"674:1-679:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003eeval\\firework.src\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eメインウィンドウクローズ処理とは　本定義　（・　→　・）\n        \"NORMAL_SHUTDOWN\"を　診断を記録する\n        実行終りすること。\n\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"680:1-680:278\"\u003eこの「メインウィンドウクローズ処理とは　本定義」とかはGitHub Copilotがいろいろ調べて設定したコードです。これでコンパイルは正常通過していることが前提なのですが、なかなかすごいものを感じます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"683:1-683:30\"\u003e\n\u003cspan id=\"現在の状態--未解決-\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%8F%BE%E5%9C%A8%E3%81%AE%E7%8A%B6%E6%85%8B--%E6%9C%AA%E8%A7%A3%E6%B1%BA-\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e現在の状態 -未解決-\u003c/h2\u003e\n\u003cp data-sourcepos=\"684:1-685:158\"\u003eこの状態でかなりの長時間作業を繰り返していましたが、ひさしぶりにGitHub CopilotがContinue to iterate?の問い合わせを出してきました。\u003cbr\u003e\nCopilot has been working on this problem for a while. It can continue to iterate, or you can send a new message to refine your prompt. Configure max requests.\u003c/p\u003e\n\u003cp data-sourcepos=\"687:1-687:157\"\u003eGitHub Copilot Pro+のクレジットはこの時点で４０％程度でしたが、いったん続行した後しばらくして中断としています。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"689:1-689:14\"\u003e\n\u003cspan id=\"おわりに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eおわりに\u003c/h1\u003e\n\u003cp data-sourcepos=\"690:1-690:389\"\u003eいかがでしたでしょうか？本件ターゲット仕様の難度が高いのでなかなか実行時不具合を解消できていませんが、GUIアプリでもAIが自律的に改修とリビルド、実行時診断ができる環境の例ができたのはよかったような気がします。同じことをしようとしている方々の参考になれば幸いです。\u003c/p\u003e\n\u003csection class=\"footnotes\"\u003e\n\u003col\u003e\n\u003cli id=\"fn-2\"\u003e\n\u003cp data-sourcepos=\"693:7-693:105\"\u003eMicrosoft Copilotで日本語トランスコンパイラ言語 Re:Mindをいじりたおす - Qiita \u003ca href=\"#fnref-2\" class=\"\"\u003e↩\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn-3\"\u003e\n\u003cp data-sourcepos=\"694:7-694:122\"\u003eCopilotが生成した日本語トランスコンパイラ言語 Re:MindのASTパーサが動作するまで - Qiita \u003ca href=\"#fnref-3\" class=\"\"\u003e↩\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn-4\"\u003e\n\u003cp data-sourcepos=\"695:6-695:126\"\u003eCopilotが生成した日本語トランスコンパイラ言語 Re:MindのC#コード生成が動作するまで - Qiita \u003ca href=\"#fnref-4\" class=\"\"\u003e↩\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli id=\"fn-1\"\u003e\n\u003cp data-sourcepos=\"692:7-692:83\"\u003eAIとつくる日本語プログラミング言語Aoi （あおい） - Qiita \u003ca href=\"#fnref-1\" class=\"\"\u003e↩\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/section\u003e\n","body":"# はじめに\n日本語プログラミング言語Mindのバージョン９でAI仕様駆動開発（っぽいこと）を行ってみます。本記事はその第４ステップで、AI仕様駆動開発の後工程のキモ？とも言えるAI修正・ビルド・実行・再修正ワークフローの自動化をGUIアプリでやってみたという内容です。\n\n# 使用AI\n## Microsoft Copilot\nWindows11アプリケーションのMicrosoft Copilot。昨年末から日本語プログラミング言語あおい Aoi（Re:Mind）の原型を作成してもらった生成AI[^2][^3][^4]。モードは「無料プラン」。\n## GitHub Copilot\nGitHub CopilotのPro+版。あおい実装[^1]と検証の主担当。スーパバイザとしてMicrosoft Copilotを使いGitHub CopilotのCodeAgent推論量を最小化するプロンプトをCopilotで生成しGitHub CopilotのCodeAgentで実行という工法？を採用しています。\n\n\n# 本記事のステップ\n[ステップ２](https://qiita.com/mylifewithviolin/items/34b835716331a64345aa)にてGitHub CopilotがMindの言語仕様に準じて修正・ビルド・再修正ワークフローを廻して、コンパイルエラーがなくせるところまで自律的に動かせました。\n[ステップ３](https://qiita.com/mylifewithviolin/items/afd36f0deb93df945d59)でアプリ仕様について調整しましたので、本ステップではアプリ仕様に到達するまで修正・ビルド・実行・再修正ワークフローを廻してみます。\n\nステップ２ではGUIアプリの実行と状態判定は人間が行っていましたが、本ステップではAIがGUIアプリを実行して状態を判定してソースコードの修正に入るという点がポイントです。\n\nただし、仕様実現の可否には画面の動きの総合的な判定が必要になりますので、そこは人間系が行い、AIが自律的に不具合修正するのは実行時にアプリが落ちてしまうとか実行時の致命的エラー改修にとどまります。\n\n## Microsoft Copilotとの事前準備\n本ステップではだいたいこれでいけるだろうと考えていたのでとくにありません。\n## GitHub Copilot\nステップ３の仕様調整はその後もいろいろ調整が続いて、現在の修正方針mdは下記のv4となっております。\n\n**docs\\firework修正方針v4.md**\n#### 9. 完了条件\n\n- `canvas_color_control.src` がSJIS/CP932で保存され、コンパイルできる。\n- canvas作成、消去、円、楕円、塗り色、線色、描画、移動の単語がTcl/Tk canvasの確認済みAPIだけで構成されている。\n- `FireworkTextEffect` を除くC#の通常花火の状態、粒子範囲、6色、30ms周期をMind版へ反映する。\n- 爆発粒子を白色固定残光で覆わず、半径1〜3のカラー粒子が不規則な方向・速度で拡散する。\n- 画面端の座標クランプによる粒子集中、整数速度の丸めによる規則的な多角形状が目視で残らない。\n- ビルド後に `firework.inf` が生成されない。\n- 文字花火、未確認のARGB、Bitmap画素取得、未確認の後置構文を追加していない。\n- 追加検証の結果と実行時のGUI状態を別途記録する。\n\nここにむかった改修の途中でWindowsフォームが実行中（実行開始の描画初期段階で）予期せず閉じてしまうという不具合が発生してしまいます。\n当初はGUIアプリの実行と落ちる段階をダイアログ挿入でユーザーが介在することで判定していたのですが、ここでGitHub Copilotが重要な提案をしてきます。\n\n#### 応答レスポンス\n\u003e状況から、単に次回予約を止めるだけでは十分でない可能性があります。診断1〜10の複数のモーダルダイアログが同じ非同期イベント処理内で実行されているので、ダイアログ自体がイベントスタックを乱している仮説が強まりました。イベント内のモーダル表示をなくし、代わりに後から読める診断記録へ切り替えられるか、Mindのファイル出力仕様を確認します。\n\n\u003eファイル出力マニュアルで、開いたログファイルへ文字列を書き込み、改行を付ける標準APIが使えることを確認しました。これなら非同期イベント内でモーダルダイアログを開かずに段階を記録できます。必要なオープン／書き込み／クローズの正確な構文を確認してから置き換えます。\n\nこれならGUIアプリでも自分で起動して、ログを見て結果を判定して不具合箇所を段階的に特定して修正できますねというやりとりになって、GitHub Copilotが起動を試みてくれたりしていたのですが（起動自体は以前からやってくれていたのですが）起動したMind由来のWindowフォームに出力した文字を見ると、ランタイムが未発見というよく見るエラーが確認されました。\n\nこれはGitHub Copilotが起動したときの状態で、わたしは環境設定PATHを実行したターミナルから起動していますので、これが起きないことは自明として認識していました。GitHub Copilotが起動するときは都度内部ターミナルのインスタンスを使うので、結局下記のようにVSCodeのtasks.jsonを修正しました。\n\n## tasks.json\nGitHub Copilotが最終的に修正した状態。GitHub Copilotが処理中に私がVSCodeで他のファイルを開いたりするので、runタスクのターゲットファイルはリテラル書きとなりました。call C:\\\\Mind9\\\\usemind.batをアプリの起動毎に実行してもらうというのがポイントです。またターミナルをコマンドプロンプトで起動するよう設定しています。\n\n```json:.vscode\\tasks.json\n{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"build\",\n      \"type\": \"shell\",\n      \"command\": \"call C:\\\\Mind9\\\\usemind.bat \u0026\u0026 mind ${fileBasenameNoExtension} guilib\",\n      \"group\": \"build\",\n      \"options\": {\n        \"shell\": {\n          \"executable\": \"C:\\\\Windows\\\\System32\\\\cmd.exe\",\n          \"args\": [\"/d\", \"/c\"]\n        }\n      },\n      \"problemMatcher\": []\n    },\n    {\n      \"label\": \"run\",\n      \"type\": \"shell\",\n      \"command\": \"call C:\\\\Mind9\\\\usemind.bat \u0026\u0026 firework\",\n      \"group\": \"test\",\n      \"options\": {\n        \"shell\": {\n          \"executable\": \"C:\\\\Windows\\\\System32\\\\cmd.exe\",\n          \"args\": [\"/d\", \"/c\"]\n        }\n      },\n      \"problemMatcher\": []\n    }\n  ]\n}\n```\n\n## お題のソースコード（AIによるデバッグ診断ログ追記状態）\nGitHub Copilotがデバッグ診断ログの出力を追加した状態のソースコードです。それがなくても既にだいぶ長大なソースコードをMind9コンパイラがエラーなしで完走する状態となっています。\n\u003cdetails\u003e\u003csummary\u003efirework.src AIによるデバッグ診断ログ追記状態\u003c/summary\u003e\n\n```mind:eval\\firework.src\n\"snowdrops\\\\randomtcl.src\"を　コンパイル。\n\"canvas_color_control.src\"を　コンパイル。\n\nキャンバスは　ＩＤ。\nキャンバス幅は　定数　１２００。\nキャンバス高は　定数　６００。\n粒子数は　定数　２０４。\n主粒子数は　定数　１８０。\n外周粒子数は　定数　２４。\n花火の数は　定数　４。\n\n粒子型は　型紙\n        円ＩＤは　図形ＩＤ\n        X座標は　変数\n        Y座標は　変数\n        X速度は　変数\n        Y速度は　変数\n        粒子半径は　変数\n        寿命は　変数\n                使用中は　変数\n                粒子色番号は　変数\n        粒子本体は　円ＩＤと　X座標と　Y座標と　X速度と　Y速度と　粒子半径と　寿命と　使用中と　粒子色番号\n        粒子列は　粒子数の　粒子本体\n        全体は　花火の数の　粒子列。\n\n花火型は　型紙\n                花火Xは　変数\n                花火Yは　変数\n                花火目標Yは　変数\n                花火上昇速度は　変数\n                花火主粒子数は　変数\n                花火半径は　変数\n                花火活動中は　変数\n                花火爆発済みは　変数\n                花火寿命は　変数\n        花火本体は　花火Xと　花火Yと　花火目標Yと　花火上昇速度と　花火主粒子数と　花火半径と　花火活動中と　花火爆発済みと　花火寿命\n        全体は　花火の数の　花火本体。\n\n花火粒子群は　構造体　粒子型。\n花火群は　構造体　花火型。\n\n花火番号は　変数。\n粒子番号は　変数。\n花火中心円ＩＤは　図形ＩＤ。\n残光円ＩＤは　図形ＩＤ。\n速度は　変数。\n方向は　変数。\n色番号は　変数。\nフレーム数は　変数。\n診断花火番号は　変数。\n診断段階は　変数。\n診断ログは　ファイル。\n診断ログ有効は　変数。\n\n診断を記録するとは　（文字列　→　・）\n        記録文は　文字列\n        記録文に　入れる\n        診断ログ有効が　１に　等しい　ならば\n                \"firework_diagnostic.log\"で　診断ログを　新規オープンする\n                エラー？\n                        ならば　０を　診断ログ有効に　入れ\n                        さもなければ\n                                記録文を　診断ログに　一行書き込み\n                                診断ログを　クローズする\n                                エラー？\n                                        ならば　０を　診断ログ有効に　入れ\n                                        つぎに\n                        つぎに\n        つぎに。\n\n粒子を初期化するとは\n        粒子数で　回数指定し\n                粒子番号に　回数を　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                ３を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                ０を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n        繰り返す。\n\n花火を作るとは\n        乱数値は　小数変数\n        範囲指定の疑似乱数２(１００,１１００)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火X（花火番号）に　入れ\n        ６００を　花火群の　花火Y（花火番号）に　入れ\n        範囲指定の疑似乱数２(１００,３００)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火目標Y（花火番号）に　入れ\n        範囲指定の疑似乱数２(８,１３)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火上昇速度（花火番号）に　入れ\n        範囲指定の疑似乱数(１２０,１８０)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火主粒子数（花火番号）に　入れ\n        範囲指定の疑似乱数２(７０,１１０)を　乱数値に　入れ\n        乱数値を　整数化し　花火群の　花火半径（花火番号）に　入れ\n        １を　花火群の　花火活動中（花火番号）に　入れ\n        ０を　花火群の　花火爆発済み（花火番号）に　入れ\n        ０を　花火群の　花火寿命（花火番号）に　入れ\n        粒子を初期化する。\n\n花火を爆発させるとは\n        乱数値は　小数変数\n        角度は　小数変数\n        速度小数は　小数変数\n        X速度小数は　小数変数\n        Y速度小数は　小数変数\n        揺らぎは　小数変数\n        主粒子数現在は　変数\n        速度層は　変数\n        外周番号は　変数\n        外周距離は　変数\n        外周速度は　変数\n        外周最大半径は　変数\n        １を　花火群の　花火爆発済み（花火番号）に　入れ\n        ２４を　花火群の　花火寿命（花火番号）に　入れ\n        診断花火番号が　０に　等しい　ならば\n                花火番号を　診断花火番号に　入れ\n                \"DIAG_01_EXPLOSION_BEGIN\"を　診断を記録する\n        つぎに\n        花火群の　花火主粒子数（花火番号）を　主粒子数現在に　入れ\n        花火群の　花火主粒子数（花火番号）で　回数指定し\n                粒子番号に　回数を　入れ\n                [角度 := PI * 2.0 * 粒子番号 / 主粒子数現在]をつみ\n                範囲指定の疑似乱数(０,３)を　揺らぎに　入れ\n                [揺らぎ := 揺らぎ - 1.0]をつみ\n                [角度 := 角度 + 揺らぎ / 12.0]をつみ\n                粒子番号を　３で　割った余りを　速度層に　入れ\n                速度層が　０に　等しい　ならば\n                        範囲指定の疑似乱数(２,５)を　速度小数に　入れ\n                つぎに\n                速度層が　１に　等しい　ならば\n                        範囲指定の疑似乱数(４,８)を　速度小数に　入れ\n                つぎに\n                速度層が　２に　等しい　ならば\n                        範囲指定の疑似乱数(７,１２)を　速度小数に　入れ\n                つぎに\n                cos(角度)を　X速度小数に　入れ\n                sin(角度)を　Y速度小数に　入れ\n                [X速度小数 := X速度小数 * 速度小数]をつみ\n                [Y速度小数 := Y速度小数 * 速度小数]をつみ\n                X速度小数を　整数化し　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                Y速度小数を　整数化し　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,４)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,７)を　色番号に　入れ\n                色番号を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数２(１８,２８)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n        繰り返す\n        花火番号が　診断花火番号に　等しい　ならば\n                \"DIAG_02_MAIN_PARTICLES_DONE\"を　診断を記録する\n        つぎに\n        花火群の　花火主粒子数（花火番号）を　粒子番号に　入れ\n        外周粒子数で　回数指定し\n                外周番号に　回数を　入れ\n                花火群の　花火主粒子数（花火番号）に　外周番号を　加え　粒子番号に　入れ\n                [角度 := PI * 2.0 * 外周番号 / 外周粒子数]をつみ\n                範囲指定の疑似乱数(０,３)を　揺らぎに　入れ\n                [揺らぎ := 揺らぎ - 1.0]をつみ\n                [角度 := 角度 + 揺らぎ / 12.0]をつみ\n                花火群の　花火半径（花火番号）を　外周最大半径に　入れ\n                [外周最大半径 := 外周最大半径 / 2]をつみ\n                範囲指定の疑似乱数(２０,外周最大半径)を　外周距離に　入れ\n                範囲指定の疑似乱数２(６,１０)を　外周速度に　入れ\n                外周距離に　外周速度を　加え　速度に　入れ\n                cos(角度)を　X速度小数に　入れ\n                sin(角度)を　Y速度小数に　入れ\n                [X速度小数 := X速度小数 * 速度]をつみ\n                [Y速度小数 := Y速度小数 * 速度]をつみ\n                X速度小数を　整数化し　花火粒子群の　X速度（花火番号，粒子番号）に　入れ\n                Y速度小数を　整数化し　花火粒子群の　Y速度（花火番号，粒子番号）に　入れ\n                花火群の　花火X（花火番号）を　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                花火群の　花火Y（花火番号）を　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数２(１６,２６)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,３)を　乱数値に　入れ\n                乱数値を　整数化し　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                範囲指定の疑似乱数(１,７)を　色番号に　入れ\n                色番号を　花火粒子群の　粒子色番号（花火番号，粒子番号）に　入れ\n                １を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n        繰り返す\n        花火番号が　診断花火番号に　等しい　ならば\n                \"DIAG_03_OUTER_PARTICLES_DONE\"を　診断を記録する\n        つぎに。\n花火群を初期化するとは\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火を作る\n        繰り返す。\n\n花火を更新するとは\n        Xは　変数\n        Yは　変数\n        花火群の　花火活動中（花火番号）が　０に　等しい\n                ならば　終わる　つぎに\n        花火群の　花火爆発済み（花火番号）が　０に　等しい\n                ならば\n                        花火群の　花火Y（花火番号）から　花火群の　花火上昇速度（花火番号）を　引き　花火群の　花火Y（花火番号）に　入れ\n                        花火群の　花火Y（花火番号）が　花火群の　花火目標Y（花火番号）　以下\n                                ならば\n                                        花火群の　花火目標Y（花火番号）を　花火群の　花火Y（花火番号）に　入れ\n                                        花火を爆発させる\n                                つぎに\n                つぎに\n        花火群の　花火爆発済み（花火番号）が　１に　等しい\n                ならば\n                        花火群の　花火寿命（花火番号）が　２４に　等しい\n                                ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                \"DIAG_04_PARTICLE_UPDATE_BEGIN\"を　診断を記録する\n                                        つぎに\n                                つぎに\n                        粒子数で　回数指定し\n                                粒子番号に　回数を　入れ\n                                花火粒子群の　使用中（花火番号，粒子番号）が　１に　等しい　ならば\n                                        花火粒子群の　X座標（花火番号，粒子番号）に　花火粒子群の　X速度（花火番号，粒子番号）を　加え　Xに　入れ\n                                        Xを　花火粒子群の　X座標（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　Y座標（花火番号，粒子番号）に　花火粒子群の　Y速度（花火番号，粒子番号）を　加え　Yに　入れ\n                                        Yを　花火粒子群の　Y座標（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　Y速度（花火番号，粒子番号）を　一つ増加し\n                                        花火粒子群の　寿命（花火番号，粒子番号）から　１を　引き　花火粒子群の　寿命（花火番号，粒子番号）に　入れ\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　１６　以下\n                                                ならば　２を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　８　以下\n                                                ならば　１を　花火粒子群の　粒子半径（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                        花火粒子群の　寿命（花火番号，粒子番号）が　０　以下\n                                                ならば　０を　花火粒子群の　使用中（花火番号，粒子番号）に　入れ\n                                        つぎに\n                                つぎに\n                        繰り返す\n                        花火群の　花火寿命（花火番号）が　２４に　等しい\n                                ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                \"DIAG_05_PARTICLE_UPDATE_DONE\"を　診断を記録する\n                                        つぎに\n                                つぎに\n                        花火群の　花火寿命（花火番号）から　１を　引き　花火群の　花火寿命（花火番号）に　入れ\n                        花火群の　花火寿命（花火番号）が　０　以下\n                                ならば　０を　花火群の　花火活動中（花火番号）に　入れ\n                        つぎに\n                つぎに\n        。\n\n花火群を更新するとは\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火群の　花火活動中（花火番号）が　１に　等しい\n                        ならば　花火を更新する\n                        つぎに\n                花火番号が　診断花火番号に　等しい　ならば\n                        花火群の　花火爆発済み（花火番号）が　１に　等しい　ならば\n                                診断段階が　０に　等しい　ならば\n                                        \"STACK_AFTER_FIRST_EXPLOSION_UPDATE\"を　診断を記録する\n                                        「最初の爆発を含む花火更新直後」で　このメッセージでスタック検査し\n                                        \"STACK_FIRST_EXPLOSION_UPDATE_OK\"を　診断を記録する\n                                つぎに\n                        つぎに\n                つぎに\n                花火群の　花火活動中（花火番号）が　０に　等しい\n                        ならば　花火を作る\n                        つぎに\n        繰り返す。\n\n花火を描くとは\n        描画Xは　変数\n        描画Yは　変数\n        描画範囲内は　変数\n        粒子区分は　変数\n        花火番号が　診断花火番号に　等しい　ならば\n                診断段階が　０に　等しい　ならば\n                        \"STACK_DRAW_ENTRY\"を　診断を記録する\n                        「診断対象花火描画の入口」で　このメッセージでスタック検査し\n                        \"STACK_DRAW_ENTRY_OK\"を　診断を記録する\n                つぎに\n        つぎに\n        花火群の　花火活動中（花火番号）が　１に　等しい\n                ならば\n        花火群の　花火爆発済み（花火番号）が　０に　等しい\n                ならば\n                        キャンバスと　花火群の　花火X（花火番号）と　花火群の　花火Y（花火番号）と　６で　円のＩＤを取得し　花火中心円ＩＤに　入れ\n                        白色で　花火中心円ＩＤに　満たす色を設定し\n                        花火中心円ＩＤを　描画する\n                さもなければ\n                        粒子数で　回数指定し\n                                粒子番号に　回数を　入れ\n                                花火粒子群の　使用中（花火番号，粒子番号）が　１に　等しい　ならば\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                診断段階が　０に　等しい　ならば\n                                                        粒子番号を　８で　割った余りを　粒子区分に　入れ\n                                                        粒子区分が　０に　等しい　ならば\n                                                                粒子番号を　文字列変換し\n                                                                \"PARTICLE_BATCH_\"と　変換文字列を　合成し　診断を記録する\n                                                                「粒子区分の描画前」で　このメッセージでスタック検査し\n                                                                \"STACK_PARTICLE_BATCH_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　X座標（花火番号，粒子番号）を　描画Xに　入れ\n                                        花火粒子群の　Y座標（花火番号，粒子番号）を　描画Yに　入れ\n                                        ０を　描画範囲内に　入れ\n                                        描画Xが　０以上　ならば\n                                                描画Yが　０以上　ならば\n                                                        描画Xが　キャンバス幅より　小さい　ならば\n                                                                描画Yが　キャンバス高より　小さい　ならば\n                                                                        １を　描画範囲内に　入れ\n                                                                つぎに\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        描画範囲内が　１に　等しい　ならば\n                                        キャンバスと　描画Xと　描画Yと　花火粒子群の　粒子半径（花火番号，粒子番号）で　円のＩＤを取得し　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　入れ\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_AFTER_FIRST_CIRCLE_ID\"を　診断を記録する\n                                                                「最初の粒子の円ID取得後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_CIRCLE_ID_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　１に　等しい　ならば\n                                                花火橙色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　２に　等しい　ならば\n                                                花火深紅色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　３に　等しい　ならば\n                                                花火金色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　４に　等しい　ならば\n                                                花火空色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　５に　等しい　ならば\n                                                花火黄緑色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火粒子群の　粒子色番号（花火番号，粒子番号）が　６に　等しい　ならば\n                                                花火紫紅色を　花火粒子群の　円ＩＤ（花火番号，粒子番号）に　満たす色を設定し\n                                        つぎに\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_BEFORE_FIRST_PARTICLE_DRAW\"を　診断を記録する\n                                                                「最初の粒子の色設定後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_PARTICLE_COLOR_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火粒子群の　円ＩＤ（花火番号，粒子番号）を　描画する\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　２以上　ならば\n                                                        粒子番号が　８以下　ならば\n                                                                診断段階が　０に　等しい　ならば\n                                                                        粒子番号を　文字列変換し\n                                                                        \"STACK_AFTER_PARTICLE_\"と　変換文字列を　合成し　診断を記録する\n                                                                        「粒子描画後」で　このメッセージでスタック検査し\n                                                                        \"STACK_PARTICLE_STEP_OK\"を　診断を記録する\n                                                                つぎに\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        花火番号が　診断花火番号に　等しい　ならば\n                                                粒子番号が　１に　等しい　ならば\n                                                        診断段階が　０に　等しい　ならば\n                                                                \"STACK_AFTER_FIRST_PARTICLE_DRAW\"を　診断を記録する\n                                                                「最初の粒子を描画した直後」で　このメッセージでスタック検査し\n                                                                \"STACK_FIRST_PARTICLE_DRAW_OK\"を　診断を記録する\n                                                        つぎに\n                                                つぎに\n                                        つぎに\n                                        つぎに\n                                つぎに\n                        繰り返す\n                        花火番号が　診断花火番号に　等しい　ならば\n                                診断段階が　０に　等しい　ならば\n                                        \"STACK_PARTICLE_LOOP_DONE\"を　診断を記録する\n                                        「診断対象花火の粒子描画ループ末尾」で　このメッセージでスタック検査し\n                                        \"STACK_PARTICLE_LOOP_OK\"を　診断を記録する\n                                つぎに\n                        つぎに\n                つぎに\n                        つぎに\n        花火番号が　診断花火番号に　等しい　ならば\n                \"STACK_DRAW_WORD_RETURN\"を　診断を記録する\n                「花火を描く処理語の末尾」で　このメッセージでスタック検査し\n                \"STACK_DRAW_WORD_RETURN_OK\"を　診断を記録する\n        つぎに\n        。\n\n花火を再描画するとは\n        キャンバスを　キャンバスをクリアし\n        花火群を更新する\n        花火の数で　回数指定し\n                花火番号に　回数を　入れ\n                花火群の　花火寿命（花火番号）が　２３に　等しい\n                        ならば\n                                花火番号が　診断花火番号に　等しい　ならば\n                                        \"DIAG_06_TARGET_DRAW_BEGIN\"を　診断を記録する\n                                つぎに\n                        つぎに\n                花火を描く\n                花火番号が　診断花火番号に　等しい　ならば\n                        診断段階が　０に　等しい　ならば\n                                \"STACK_AFTER_TARGET_DRAW\"を　診断を記録する\n                                「診断対象花火の描画直後」で　このメッセージでスタック検査し\n                                \"STACK_TARGET_DRAW_OK\"を　診断を記録する\n                        診断段階が　８に　等しい　ならば\n                                \"STACK_AFTER_DRAW_LOOP\"を　診断を記録する\n                                「花火描画ループの直後」で　このメッセージでスタック検査し\n                                \"STACK_DRAW_LOOP_OK\"を　診断を記録する\n                        つぎに\n                        つぎに\n                つぎに\n                花火群の　花火寿命（花火番号）が　２３に　等しい\n                        ならば\n                                花火番号が　診断花火番号に　等しい　ならば\n                                        \"DIAG_07_TARGET_DRAW_DONE\"を　診断を記録する\n                                        ７を　診断段階に　入れ\n                                つぎに\n                        つぎに\n        繰り返す\n        診断段階が　７に　等しい　ならば\n                \"DIAG_08_ALL_FIREWORKS_DRAW_DONE\"を　診断を記録する\n                ８を　診断段階に　入れ\n        つぎに\n        診断段階が　８に　等しい　ならば\n                \"STACK_AFTER_DRAW_LOOP\"を　診断を記録する\n                「花火描画ループの直後」で　このメッセージでスタック検査し\n                \"STACK_DRAW_LOOP_OK\"を　診断を記録する\n        つぎに\n        。\n\n遅延後の花火処理とは　仮定義。\n\n次の花火処理を予約とは\n        ３０ミリと　遅延後の花火処理の　実行情報と　空列で　遅延実行を予約\n        診断段階が　１０に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「2回目の遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_DELAY_RESERVATION_RETURNED\"を　診断を記録する\n                「3回目の遅延予約から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに。\n\n遅延後の花火処理とは　本定義　（引数１、引数２、・・、引数個数　→　・）\n        イベント引数を捨て\n        診断段階が　０に　等しい　ならば\n                \"STACK_FIRST_EVENT_ENTRY\"を　診断を記録する\n                「初回遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_FIRST_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「2回目の遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_EVENT_ARGS_DISCARDED\"を　診断を記録する\n                「3回目の遅延イベント入口で引数を捨てた直後」で　このメッセージでスタック検査し\n                \"STACK_EVENT_ENTRY_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"DIAG_11_FIRST_EVENT_BEGIN\"を　診断を記録する\n                １１を　診断段階に　入れ\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"DIAG_14_SECOND_EVENT_BEGIN\"を　診断を記録する\n                １４を　診断段階に　入れ\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"DIAG_17_THIRD_EVENT_BEGIN\"を　診断を記録する\n                １７を　診断段階に　入れ\n        つぎに\n        花火を再描画する\n        診断段階が　８に　等しい　ならば\n                \"STACK_FIRST_REDRAW_RETURN\"を　診断を記録する\n                「初回再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_FIRST_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１１に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「最初の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１４に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「2回目の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１７に　等しい　ならば\n                \"STACK_REDRAW_RETURNED\"を　診断を記録する\n                「3回目の再描画から復帰した直後」で　このメッセージでスタック検査し\n                \"STACK_REDRAW_RETURN_OK\"を　診断を記録する\n        つぎに\n        診断段階が　８に　等しい　ならば\n                \"DIAG_09_REDRAW_RETURNED\"を　診断を記録する\n                ９を　診断段階に　入れ\n        つぎに\n        診断段階が　９に　等しい　ならば\n                \"DIAG_10_BEFORE_NEXT_DELAY\"を　診断を記録する\n                １０を　診断段階に　入れ\n        つぎに\n        診断段階が　１１に　等しい　ならば\n                \"DIAG_12_FIRST_FRAME_DONE\"を　診断を記録する\n                １２を　診断段階に　入れ\n        つぎに\n        診断段階が　１２に　等しい　ならば\n                \"DIAG_13_BEFORE_SECOND_DELAY\"を　診断を記録する\n                １３を　診断段階に　入れ\n        つぎに\n        診断段階が　１４に　等しい　ならば\n                \"DIAG_15_SECOND_FRAME_DONE\"を　診断を記録する\n                １５を　診断段階に　入れ\n        つぎに\n        診断段階が　１５に　等しい　ならば\n                \"DIAG_16_BEFORE_THIRD_DELAY\"を　診断を記録する\n                １６を　診断段階に　入れ\n        つぎに\n        診断段階が　１７に　等しい　ならば\n                \"DIAG_18_THIRD_FRAME_DONE\"を　診断を記録する\n                １８を　診断段階に　入れ\n        つぎに\n        診断段階が　１０に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「初回遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１３に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「2回目の遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        診断段階が　１６に　等しい　ならば\n                \"STACK_BEFORE_DELAY_RESERVATION\"を　診断を記録する\n                「3回目の遅延予約の直前」で　このメッセージでスタック検査し\n                \"STACK_BEFORE_DELAY_RESERVATION_OK\"を　診断を記録する\n        つぎに\n        次の花火処理を予約すること\n        。\n\nメインとは\n        ０を　診断花火番号に　入れ\n        ０を　診断段階に　入れ\n        １を　診断ログ有効に　入れ\n        \"APP_READY\"を　診断を記録する\n        メインウィンドウと　キャンバス幅と　キャンバス高と　黒色で\n                キャンバスを立て　キャンバスに　入れ\n        花火群を初期化する\n        花火を再描画する\n        \"INITIALIZATION_COMPLETE\"を　診断を記録する\n        次の花火処理を予約する。\n\nメインウィンドウクローズ処理とは　本定義　（・　→　・）\n        \"NORMAL_SHUTDOWN\"を　診断を記録する\n        実行終りすること。\n\n```\n\u003c/details\u003e\n\n## 診断ログ\n下記のような診断ログで終了時の最終値を記録します。下記は現在の異常終了の状態。\n**firework_diagnostic.log**\n```text:firework_diagnostic.log\nPARTICLE_BATCH_8\n```\n正常終了時は下記のコードによりNORMAL_SHUTDOWNを記録するとなっています。\n```mind:eval\\firework.src\nメインウィンドウクローズ処理とは　本定義　（・　→　・）\n        \"NORMAL_SHUTDOWN\"を　診断を記録する\n        実行終りすること。\n\n```\nこの「メインウィンドウクローズ処理とは　本定義」とかはGitHub Copilotがいろいろ調べて設定したコードです。これでコンパイルは正常通過していることが前提なのですが、なかなかすごいものを感じます。\n\n\n## 現在の状態 -未解決-\nこの状態でかなりの長時間作業を繰り返していましたが、ひさしぶりにGitHub CopilotがContinue to iterate?の問い合わせを出してきました。\nCopilot has been working on this problem for a while. It can continue to iterate, or you can send a new message to refine your prompt. Configure max requests.\n\nGitHub Copilot Pro+のクレジットはこの時点で４０％程度でしたが、いったん続行した後しばらくして中断としています。\n\n# おわりに\nいかがでしたでしょうか？本件ターゲット仕様の難度が高いのでなかなか実行時不具合を解消できていませんが、GUIアプリでもAIが自律的に改修とリビルド、実行時診断ができる環境の例ができたのはよかったような気がします。同じことをしようとしている方々の参考になれば幸いです。\n\n[^1]: AIとつくる日本語プログラミング言語Aoi （あおい） - Qiita\n[^2]: Microsoft Copilotで日本語トランスコンパイラ言語 Re:Mindをいじりたおす - Qiita\n[^3]: Copilotが生成した日本語トランスコンパイラ言語 Re:MindのASTパーサが動作するまで - Qiita\n[^4]:Copilotが生成した日本語トランスコンパイラ言語 Re:MindのC#コード生成が動作するまで - Qiita\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:44:52+09:00","group":null,"id":"57b573a380cc7e6f4957","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"mind","versions":[]},{"name":"日本語プログラミング言語","versions":[]},{"name":"VSCode","versions":[]},{"name":"GitHubCopilot","versions":[]},{"name":"仕様駆動開発","versions":[]}],"title":"日本語プログラミング言語Mind９でAI仕様駆動開発を行ってみた（ステップ４）～GUIアプリの修正・ビルド・実行・再修正ワークフローが動く～","updated_at":"2026-09-27T11:51:55+09:00","url":"https://qiita.com/mylifewithviolin/items/57b573a380cc7e6f4957","user":{"description":"フリーランスのシステムエンジニアです。\r\n趣味はバイオリンとピアノを習うこと。弾くことではないようです。\r\nI like beautiful Japanese language, whether natural or structured. Because it has beautiful character types and attractive pronunciation sounds.","facebook_id":"","followees_count":97,"followers_count":64,"github_login_name":"mylifewithviolin","id":"mylifewithviolin","items_count":610,"linkedin_id":"","location":"","name":"","organization":"アプリ工房いずみ","permanent_id":3118036,"profile_image_url":"https://avatars.githubusercontent.com/u/123276433?v=4","team_only":false,"twitter_screen_name":null,"website_url":"http://mylifewithviolin.blog.fc2.com/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:105\"\u003e前日夜の最新トレンド記事のAIポッドキャストを毎日朝7時に更新しています。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-5:60\"\u003e通勤中などにながら聴きしよう！\u003cbr\u003e\n（Qiita投稿は通勤には間に合わないと思われますが）\u003cbr\u003e\nフィードバックとか助かりますのでください\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:18\"\u003e↓こちらから\u003c/p\u003e\n\u003ciframe width=\"100%\" height=\"315\" src=\"https://www.youtube.com/embed/cqZPEdwi-wU\" frameborder=\"0\" loading=\"lazy\" allowfullscreen\u003e\u003c/iframe\u003e\n\u003cp data-sourcepos=\"11:1-13:125\"\u003e出典\u003cbr\u003e\nJenkinsのPRレビューでテストとセキュリティスキャンも回す ― AIのAPPROVEを機械的に覆すMineWatchのCI\u003cbr\u003e\n\u003ca href=\"https://qiita.com/jqit-yukiono/items/8675266928f574677d9e?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-c55da8f7a929a555d14c\"\u003ehttps://qiita.com/jqit-yukiono/items/8675266928f574677d9e?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"15:1-16:125\"\u003e個人開発でApp Storeリリースフローを学ぶ（三部作）― 本番だけ静かに壊れた、CI/CDの2つの事故\u003cbr\u003e\n\u003ca href=\"https://qiita.com/jqit-yukiono/items/21973aaa1c605b34c174?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-13696b3e6c124b1d736d\"\u003ehttps://qiita.com/jqit-yukiono/items/21973aaa1c605b34c174?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"18:1-19:122\"\u003eAI生成そのままのスライドを使った登壇が多すぎてちょっと食傷気味なんだよォーーーッ！！！\u003cbr\u003e\n\u003ca href=\"https://qiita.com/shimagaji/items/8acd9b94dfe56fefc0dc?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-643ac6f520d912e99cef\"\u003ehttps://qiita.com/shimagaji/items/8acd9b94dfe56fefc0dc?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"21:1-22:119\"\u003e高校の文化祭でPOSシステムをCloudflare上に1から構築/運用した話 〜短期開発から本番障害、そして完売まで〜\u003cbr\u003e\n\u003ca href=\"https://qiita.com/ast-24/items/454fc975b095230565c7?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-9f5b9a7795e8bb642f4f\"\u003ehttps://qiita.com/ast-24/items/454fc975b095230565c7?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-25:122\"\u003eAI感のないAWS構成図をAIエージェントに描かせたい！\u003cbr\u003e\n\u003ca href=\"https://qiita.com/sagochiko/items/ef77b084ff2dee1f859c?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-1612bbf4c2f611c09194\"\u003ehttps://qiita.com/sagochiko/items/ef77b084ff2dee1f859c?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"27:1-28:129\"\u003eその仕事、本当に「Skills」で再現できるのか考えてみる\u003cbr\u003e\n\u003ca href=\"https://qiita.com/Takashi_Masumori/items/4fbf6144f4cab3f00123?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-a19bb6e8f7de7e5e66c5\"\u003ehttps://qiita.com/Takashi_Masumori/items/4fbf6144f4cab3f00123?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"30:1-31:121\"\u003eVRChatワールド「すんごいカメレオン」同期技術解説\u003cbr\u003e\n\u003ca href=\"https://qiita.com/Yodokoro/items/1eaaa4260efe413fee50?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-61c9ec4d4af2683a63fb\"\u003ehttps://qiita.com/Yodokoro/items/1eaaa4260efe413fee50?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"33:1-34:119\"\u003eAI時代真っ只中に、ほぼ未経験で自社開発エンジニアとして新卒入社した僕が思うこと\u003cbr\u003e\n\u003ca href=\"https://qiita.com/1nori_/items/bd8052e938a343c82555?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-e0d49d59f586bccc227e\"\u003ehttps://qiita.com/1nori_/items/bd8052e938a343c82555?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"36:1-37:123\"\u003e大切な問題ほど、なぜ語られなくなるのか？ ― 「沈黙の螺旋」と財源幻想をデバッグし、国家レジリエンスを高める : システム設計視点の行動経済学 (33)\u003cbr\u003e\n\u003ca href=\"https://qiita.com/maskot1977/items/9b6f1a03204797cf91d2?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-cc8b2d7d149eb4c59b12\"\u003ehttps://qiita.com/maskot1977/items/9b6f1a03204797cf91d2?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-40:122\"\u003eMCP サーバーと繋いだだけで情報漏洩？ 危険の4パターンと事前に確認すべき7つのことをまとめてみた\u003cbr\u003e\n\u003ca href=\"https://qiita.com/songchong/items/89175eaa7a30f71d0351?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-d2eaee0c299f264d85e8\"\u003ehttps://qiita.com/songchong/items/89175eaa7a30f71d0351?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"42:1-43:120\"\u003eLambda 関数 URL で Condition 句による IP アドレス制限が可能になったか検証する\u003cbr\u003e\n\u003ca href=\"https://qiita.com/hayao_k/items/e6518a0d494006fac712?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-5f814b2276db0787b9a1\"\u003ehttps://qiita.com/hayao_k/items/e6518a0d494006fac712?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"45:1-46:122\"\u003eKubernetes入門：Podが動くまでに中で何が起きているのか、クラスタの構成要素を整理する\u003cbr\u003e\n\u003ca href=\"https://qiita.com/yushibats/items/16b6b3c8ec5acd6bdc8b?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-d0699752924a8bf0d880\"\u003ehttps://qiita.com/yushibats/items/16b6b3c8ec5acd6bdc8b?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"48:1-49:123\"\u003eJev は LLM ジャッジに使えるか、紛らわしい誤答72件で確かめた\u003cbr\u003e\n\u003ca href=\"https://qiita.com/taka_yayoi/items/3cc901e872209835491a?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-2544e60fb109d01018f8\"\u003ehttps://qiita.com/taka_yayoi/items/3cc901e872209835491a?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"51:1-52:122\"\u003eClaude Code の拒否ルールを7つの形で測った。最新版でまだ4つ残っている\u003cbr\u003e\n\u003ca href=\"https://qiita.com/suwa_nobu/items/d21ff35b9329eecc6143?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-be631988397d59945159\"\u003ehttps://qiita.com/suwa_nobu/items/d21ff35b9329eecc6143?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"54:1-55:118\"\u003eAWS DevOps Agent と障害対応の速さを競ってみた\u003cbr\u003e\n\u003ca href=\"https://qiita.com/htGin/items/064fcb82f1efa60d9b9b?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-0231738b8c3aa0c1bfaa\"\u003ehttps://qiita.com/htGin/items/064fcb82f1efa60d9b9b?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"57:1-58:122\"\u003eClaude Opus 5.5 の「40%安い」は、キャッシュの当たり方で24%にも59%にもなった\u003cbr\u003e\n\u003ca href=\"https://qiita.com/suwa_nobu/items/870b1d6c5502aca68f55?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-049c933e06807ba8a614\"\u003ehttps://qiita.com/suwa_nobu/items/870b1d6c5502aca68f55?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-61:119\"\u003e【Hike言語】Wasm難読化の自作から初OSS、そして言語開発者の神対応で「クラッシュ防止機能」を即日実装できた話\u003cbr\u003e\n\u003ca href=\"https://qiita.com/kinop3/items/d550650596d70f1a5b10?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-a10058a4446302bf27d6\"\u003ehttps://qiita.com/kinop3/items/d550650596d70f1a5b10?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"63:1-64:122\"\u003e情シス全員がグローバル管理者になっていませんか？ ～ Microsoft Entra PIM で始める特権管理\u003cbr\u003e\n\u003ca href=\"https://qiita.com/carol0226/items/5b42b547f102fa8a17a4?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-7e4f5b3837aa2ad632cd\"\u003ehttps://qiita.com/carol0226/items/5b42b547f102fa8a17a4?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-67:128\"\u003e自分の頭で考えることって重要だよなと改めて思わされた話\u003cbr\u003e\n\u003ca href=\"https://qiita.com/ike_s_muramatsu/items/44664e57e2738614fccf?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-592509c81b8a301183c2\"\u003ehttps://qiita.com/ike_s_muramatsu/items/44664e57e2738614fccf?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"69:1-70:126\"\u003e生成AI送信前にPythonで個人情報をマスキングする\u003cbr\u003e\n\u003ca href=\"https://qiita.com/TechStudioLab/items/83349e8b4cdf07b97cd1?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\" class=\"autolink\" id=\"reference-4aed949938e33cb6f78d\"\u003ehttps://qiita.com/TechStudioLab/items/83349e8b4cdf07b97cd1?utm_campaign=popular_items\u0026amp;utm_medium=feed\u0026amp;utm_source=popular_items\u003c/a\u003e\u003c/p\u003e\n\u003chr data-sourcepos=\"72:1-72:3\"\u003e\n\u003cp data-sourcepos=\"73:1-73:159\"\u003e\u003cem\u003e毎日のQiitaトレンドをAIポッドキャストでお届けしています。通勤・退勤のお供にぜひチャンネル登録お願いします！\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"75:1-76:337\"\u003e\u003cem\u003eAIでの学習教材作りました！全20セッションでWebアプリ公開まで学べます。\u003c/em\u003e\u003cbr\u003e\n\u003cem\u003e📖 \u003ca href=\"https://qiita.com/ennagara128/items/0add23ad4f88fb54c65c\" id=\"reference-08ce64a68408ab19f6fe\"\u003eQiitaで教材の詳細を見る\u003c/a\u003e | 📗 \u003ca href=\"https://zenn.dev/ennagara128/books/ai-programming-trial\" rel=\"nofollow noopener\" target=\"_blank\"\u003eZenn Booksで購入\u003c/a\u003e | 📘 \u003ca href=\"https://note.com/ennagara128/n/n209ec4a81a02\" rel=\"nofollow noopener\" target=\"_blank\"\u003enoteで購入\u003c/a\u003e | 🧑‍🏫 \u003ca href=\"https://menta.work/plan/20251\" rel=\"nofollow noopener\" target=\"_blank\"\u003eMENTAでメンター付きプラン\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n","body":"前日夜の最新トレンド記事のAIポッドキャストを毎日朝7時に更新しています。\n\n通勤中などにながら聴きしよう！\n（Qiita投稿は通勤には間に合わないと思われますが）\nフィードバックとか助かりますのでください\n\n↓こちらから\n\n\u003ciframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/cqZPEdwi-wU\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" loading=\"lazy\" allowfullscreen\u003e\u003c/iframe\u003e\n\n出典\nJenkinsのPRレビューでテストとセキュリティスキャンも回す ― AIのAPPROVEを機械的に覆すMineWatchのCI\nhttps://qiita.com/jqit-yukiono/items/8675266928f574677d9e?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n個人開発でApp Storeリリースフローを学ぶ（三部作）― 本番だけ静かに壊れた、CI/CDの2つの事故\nhttps://qiita.com/jqit-yukiono/items/21973aaa1c605b34c174?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nAI生成そのままのスライドを使った登壇が多すぎてちょっと食傷気味なんだよォーーーッ！！！\nhttps://qiita.com/shimagaji/items/8acd9b94dfe56fefc0dc?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n高校の文化祭でPOSシステムをCloudflare上に1から構築/運用した話 〜短期開発から本番障害、そして完売まで〜\nhttps://qiita.com/ast-24/items/454fc975b095230565c7?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nAI感のないAWS構成図をAIエージェントに描かせたい！\nhttps://qiita.com/sagochiko/items/ef77b084ff2dee1f859c?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nその仕事、本当に「Skills」で再現できるのか考えてみる\nhttps://qiita.com/Takashi_Masumori/items/4fbf6144f4cab3f00123?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nVRChatワールド「すんごいカメレオン」同期技術解説\nhttps://qiita.com/Yodokoro/items/1eaaa4260efe413fee50?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nAI時代真っ只中に、ほぼ未経験で自社開発エンジニアとして新卒入社した僕が思うこと\nhttps://qiita.com/1nori_/items/bd8052e938a343c82555?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n大切な問題ほど、なぜ語られなくなるのか？ ― 「沈黙の螺旋」と財源幻想をデバッグし、国家レジリエンスを高める : システム設計視点の行動経済学 (33)\nhttps://qiita.com/maskot1977/items/9b6f1a03204797cf91d2?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nMCP サーバーと繋いだだけで情報漏洩？ 危険の4パターンと事前に確認すべき7つのことをまとめてみた\nhttps://qiita.com/songchong/items/89175eaa7a30f71d0351?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nLambda 関数 URL で Condition 句による IP アドレス制限が可能になったか検証する\nhttps://qiita.com/hayao_k/items/e6518a0d494006fac712?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nKubernetes入門：Podが動くまでに中で何が起きているのか、クラスタの構成要素を整理する\nhttps://qiita.com/yushibats/items/16b6b3c8ec5acd6bdc8b?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nJev は LLM ジャッジに使えるか、紛らわしい誤答72件で確かめた\nhttps://qiita.com/taka_yayoi/items/3cc901e872209835491a?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nClaude Code の拒否ルールを7つの形で測った。最新版でまだ4つ残っている\nhttps://qiita.com/suwa_nobu/items/d21ff35b9329eecc6143?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nAWS DevOps Agent と障害対応の速さを競ってみた\nhttps://qiita.com/htGin/items/064fcb82f1efa60d9b9b?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\nClaude Opus 5.5 の「40%安い」は、キャッシュの当たり方で24%にも59%にもなった\nhttps://qiita.com/suwa_nobu/items/870b1d6c5502aca68f55?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n【Hike言語】Wasm難読化の自作から初OSS、そして言語開発者の神対応で「クラッシュ防止機能」を即日実装できた話\nhttps://qiita.com/kinop3/items/d550650596d70f1a5b10?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n情シス全員がグローバル管理者になっていませんか？ ～ Microsoft Entra PIM で始める特権管理\nhttps://qiita.com/carol0226/items/5b42b547f102fa8a17a4?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n自分の頭で考えることって重要だよなと改めて思わされた話\nhttps://qiita.com/ike_s_muramatsu/items/44664e57e2738614fccf?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n生成AI送信前にPythonで個人情報をマスキングする\nhttps://qiita.com/TechStudioLab/items/83349e8b4cdf07b97cd1?utm_campaign=popular_items\u0026utm_medium=feed\u0026utm_source=popular_items\n\n---\n_毎日のQiitaトレンドをAIポッドキャストでお届けしています。通勤・退勤のお供にぜひチャンネル登録お願いします！_\n\n_AIでの学習教材作りました！全20セッションでWebアプリ公開まで学べます。_\n_📖 [Qiitaで教材の詳細を見る](https://qiita.com/ennagara128/items/0add23ad4f88fb54c65c) | 📗 [Zenn Booksで購入](https://zenn.dev/ennagara128/books/ai-programming-trial) | 📘 [noteで購入](https://note.com/ennagara128/n/n209ec4a81a02) | 🧑‍🏫 [MENTAでメンター付きプラン](https://menta.work/plan/20251)_\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:42:51+09:00","group":null,"id":"be4211214a6852cfe503","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"ClaudeCode","versions":[]},{"name":"Security","versions":[]},{"name":"生成AI","versions":[]},{"name":"AI","versions":[]},{"name":"AWS","versions":[]}],"title":"2026年9月27日 今日のQiitaトレンド記事をポッドキャストで聴こう！","updated_at":"2026-09-27T11:42:51+09:00","url":"https://qiita.com/ennagara128/items/be4211214a6852cfe503","user":{"description":null,"facebook_id":null,"followees_count":0,"followers_count":345,"github_login_name":null,"id":"ennagara128","items_count":543,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4156578,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/4156578/a82b923531ddc21bfa1f808acd2239c9669df885/x_large.png?1776655051","team_only":false,"twitter_screen_name":"ai_info128","website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:440\"\u003eシステムスペックのエラーログの読み方を勉強していました。最初は真っ赤なログを見ても「何が起きてるのか全然わからない…」という状態でしたが、エラーログは\u003cstrong\u003e役割の違う3つのパーツ\u003c/strong\u003eに分解できると知り、なんとなく読み方のコツが掴めてきたので、例のテストコードを使ってアウトプットとしてまとめておきます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"5:1-5:33\"\u003e\n\u003cspan id=\"題材にするエラーログ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%A1%8C%E6%9D%90%E3%81%AB%E3%81%99%E3%82%8B%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%AD%E3%82%B0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e題材にするエラーログ\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"7:1-18:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eFailure/Error: Capybara.assert_current_path(\"/signup\", ignore_query: true)\n\nCapybara::ExpectationNotMet:\n   expected \"/signup_path\" to equal \"/signup\"\n\n[Screenshot Image]: /myapp/tmp/capybara/failures_....png\n\n# /usr/local/bundle/gems/capybara-3.40.0/lib/capybara/session/matchers.rb:24:in `block in assert_current_path'\n...\n# ./spec/system/fix_bug_spec.rb:15:in `block (5 levels) in \u0026lt;top (required)\u0026gt;'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"20:1-20:49\"\u003e\n\u003cspan id=\"ログは3つのブロックでできている\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AD%E3%82%B0%E3%81%AF3%E3%81%A4%E3%81%AE%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%A7%E3%81%A7%E3%81%8D%E3%81%A6%E3%81%84%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eログは3つのブロックでできている\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"22:1-22:63\"\u003e\n\u003cspan id=\"-failureerror--expectationnotmet最重要パート\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-failureerror--expectationnotmet%E6%9C%80%E9%87%8D%E8%A6%81%E3%83%91%E3%83%BC%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e① Failure/Error 〜 ExpectationNotMet(最重要パート)\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"24:1-29:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eFailure/Error: Capybara.assert_current_path(\"/signup\", ignore_query: true)\n\nCapybara::ExpectationNotMet:\n   expected \"/signup_path\" to equal \"/signup\"\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"31:1-31:115\"\u003eここが「\u003cstrong\u003e何を確認していて、なぜ失敗したのか\u003c/strong\u003e」が書かれている一番大事な部分。\u003c/p\u003e\n\u003ctable data-sourcepos=\"33:1-38:45\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"33:1-33:19\"\u003e\n\u003cth data-sourcepos=\"33:2-33:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"33:11-33:18\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"35:1-35:50\"\u003e\n\u003ctd data-sourcepos=\"35:2-35:16\"\u003eFailure/Error\u003c/td\u003e\n\u003ctd data-sourcepos=\"35:18-35:49\"\u003e失敗した箇所のコード\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"36:1-36:48\"\u003e\n\u003ctd data-sourcepos=\"36:2-36:11\"\u003eexpected\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:13-36:47\"\u003eテストが期待していた値\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"37:1-37:60\"\u003e\n\u003ctd data-sourcepos=\"37:2-37:11\"\u003eto equal\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:13-37:59\"\u003e「〜と一致するはず」という意味\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"38:1-38:45\"\u003e\n\u003ctd data-sourcepos=\"38:2-38:17\"\u003e(実際の値)\u003c/td\u003e\n\u003ctd data-sourcepos=\"38:19-38:44\"\u003e実際に得られた値\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"40:1-40:281\"\u003e\u003ccode\u003eexpected \"A\" to equal \"B\"\u003c/code\u003e は「\u003cstrong\u003eAになるはずなのに、実際はBになっている\u003c/strong\u003e」という順番で読む、というのがポイント。A=期待値、B=実際の値。この読み方さえ覚えておけば、他のエラーでも同じ考え方が使えそう。\u003c/p\u003e\n\u003cp data-sourcepos=\"42:1-42:180\"\u003e今回の例で言うと「\u003ccode\u003e/signup\u003c/code\u003eに遷移してほしかったのに、実際は\u003ccode\u003e/signup_path\u003c/code\u003eに遷移してしまった」ということが、この3行だけで分かる。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"44:1-44:44\"\u003e\n\u003cspan id=\"-スクリーンショットのパス\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%AE%E3%83%91%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e② スクリーンショットのパス\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"46:1-48:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e[Screenshot Image]: /myapp/tmp/capybara/failures_....png\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"50:1-50:228\"\u003eテストが失敗した瞬間の画面を自動で撮影した画像の保存場所。文字情報だけで原因が分かる時は必須ではないけれど、迷ったら開いて画面を見ると理解の助けになる。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"52:1-52:61\"\u003e\n\u003cspan id=\"-スタックトレース処理の呼び出し順序\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B9%E5%87%A6%E7%90%86%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E9%A0%86%E5%BA%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e③ スタックトレース(処理の呼び出し順序)\u003c/h3\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"54:1-58:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e# /usr/local/bundle/gems/capybara-3.40.0/lib/capybara/session/matchers.rb:24:in `block in assert_current_path'\n...\n# ./spec/system/fix_bug_spec.rb:15:in `block (5 levels) in \u0026lt;top (required)\u0026gt;'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"60:1-60:281\"\u003eエラーが起きるまでにどのファイルのどの処理が呼ばれたかの記録。\u003cstrong\u003e下から上に読む\u003c/strong\u003eのがポイントで、一番下の\u003ccode\u003e./spec/system/fix_bug_spec.rb:15\u003c/code\u003eが「自分が書いたテストコードの中で、最初に呼び出された場所」。\u003c/p\u003e\n\u003cp data-sourcepos=\"62:1-62:273\"\u003egem内部の行(\u003ccode\u003ematchers.rb\u003c/code\u003eなど)は基本的に読み込む必要なし。今回の例では「\u003ccode\u003efix_bug_spec.rb\u003c/code\u003eの15行目で\u003ccode\u003eassert_current_path\u003c/code\u003eが呼ばれ、それがCapybara内部の処理を経てエラーになった」という流れがここから分かる。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"64:1-64:55\"\u003e\n\u003cspan id=\"3つを合体させて読み方の型にする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3%E3%81%A4%E3%82%92%E5%90%88%E4%BD%93%E3%81%95%E3%81%9B%E3%81%A6%E8%AA%AD%E3%81%BF%E6%96%B9%E3%81%AE%E5%9E%8B%E3%81%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3つを合体させて「読み方の型」にする\u003c/h2\u003e\n\u003col data-sourcepos=\"66:1-69:0\"\u003e\n\u003cli data-sourcepos=\"66:1-66:98\"\u003e\n\u003cstrong\u003e③\u003c/strong\u003eを見て「コードのどこで起きたか」を特定する → \u003ccode\u003efix_bug_spec.rb:15\u003c/code\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"67:1-67:140\"\u003e\n\u003cstrong\u003e①\u003c/strong\u003eを見て「何が期待と違ったか」を特定する → \u003ccode\u003e/signup\u003c/code\u003eを期待していたのに\u003ccode\u003e/signup_path\u003c/code\u003eになっていた\u003c/li\u003e\n\u003cli data-sourcepos=\"68:1-69:0\"\u003e\n\u003cstrong\u003e②\u003c/strong\u003eは必要なら画面で確認する(今回は文字情報で十分)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"70:1-70:128\"\u003eつまり、\u003cstrong\u003e③で「場所」を、①で「原因」を確認する\u003c/strong\u003e、という2段階で読むのが基本パターン。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"72:1-72:27\"\u003e\n\u003cspan id=\"原因を突き止める\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8E%9F%E5%9B%A0%E3%82%92%E7%AA%81%E3%81%8D%E6%AD%A2%E3%82%81%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e原因を突き止める\u003c/h2\u003e\n\u003cp data-sourcepos=\"74:1-74:276\"\u003e①から「アプリ側が\u003ccode\u003e/signup\u003c/code\u003eではなく\u003ccode\u003e/signup_path\u003c/code\u003eという誤ったパスに遷移させている」ことが分かる。今回のケースは、ビューファイルで\u003ccode\u003esignup_path\u003c/code\u003eをクォート付き文字列にしてしまっているパターンだった。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"erb\" data-sourcepos=\"76:1-82:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e\u0026lt;%# NG: 文字列として扱われ、/signup_path という誤ったURLになる %\u0026gt;\u003c/span\u003e\n\u003cspan class=\"cp\"\u003e\u0026lt;%=\u003c/span\u003e \u003cspan class=\"n\"\u003elink_to\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"始めよう\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"signup_path\"\u003c/span\u003e \u003cspan class=\"cp\"\u003e%\u0026gt;\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e\u0026lt;%# OK: パスヘルパーとして正しく解釈される %\u0026gt;\u003c/span\u003e\n\u003cspan class=\"cp\"\u003e\u0026lt;%=\u003c/span\u003e \u003cspan class=\"n\"\u003elink_to\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"始めよう\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003esignup_path\u003c/span\u003e \u003cspan class=\"cp\"\u003e%\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"84:1-84:239\"\u003eクォートで囲んでしまうと、Railsは\u003ccode\u003esignup_path\u003c/code\u003eをパスヘルパーではなく単なる文字列として扱ってしまい、その結果\u003ccode\u003e/signup_path\u003c/code\u003eという存在しないパスに飛んでしまう、というオチ。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"86:1-86:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"88:1-92:0\"\u003e\n\u003cli data-sourcepos=\"88:1-88:126\"\u003eエラーログは「①原因」「②スクリーンショット」「③発生場所」の3ブロックに分けて読む\u003c/li\u003e\n\u003cli data-sourcepos=\"89:1-89:79\"\u003e\n\u003ccode\u003eexpected \"A\" to equal \"B\"\u003c/code\u003eは「Aのはずが実際はBだった」と読む\u003c/li\u003e\n\u003cli data-sourcepos=\"90:1-90:80\"\u003eスタックトレースは下から上へ、自分のコードの行を探す\u003c/li\u003e\n\u003cli data-sourcepos=\"91:1-92:0\"\u003eパスヘルパーは文字列としてクォートしない\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"93:1-93:300\"\u003eエラーログを闇雲に上から読んでいた頃に比べて、「まず場所→次に原因」という順番を意識するだけでかなり読みやすくなった気がします。同じようにエラーログに苦手意識がある初学者の方の参考になれば嬉しいです。\u003c/p\u003e\n","body":"## はじめに\n\nシステムスペックのエラーログの読み方を勉強していました。最初は真っ赤なログを見ても「何が起きてるのか全然わからない…」という状態でしたが、エラーログは**役割の違う3つのパーツ**に分解できると知り、なんとなく読み方のコツが掴めてきたので、例のテストコードを使ってアウトプットとしてまとめておきます。\n\n## 題材にするエラーログ\n\n```\nFailure/Error: Capybara.assert_current_path(\"/signup\", ignore_query: true)\n\nCapybara::ExpectationNotMet:\n   expected \"/signup_path\" to equal \"/signup\"\n\n[Screenshot Image]: /myapp/tmp/capybara/failures_....png\n\n# /usr/local/bundle/gems/capybara-3.40.0/lib/capybara/session/matchers.rb:24:in `block in assert_current_path'\n...\n# ./spec/system/fix_bug_spec.rb:15:in `block (5 levels) in \u003ctop (required)\u003e'\n```\n\n## ログは3つのブロックでできている\n\n### ① Failure/Error 〜 ExpectationNotMet(最重要パート)\n\n```\nFailure/Error: Capybara.assert_current_path(\"/signup\", ignore_query: true)\n\nCapybara::ExpectationNotMet:\n   expected \"/signup_path\" to equal \"/signup\"\n```\n\nここが「**何を確認していて、なぜ失敗したのか**」が書かれている一番大事な部分。\n\n| 項目 | 内容 |\n|---|---|\n| Failure/Error | 失敗した箇所のコード |\n| expected | テストが期待していた値 |\n| to equal | 「〜と一致するはず」という意味 |\n| (実際の値) | 実際に得られた値 |\n\n`expected \"A\" to equal \"B\"` は「**Aになるはずなのに、実際はBになっている**」という順番で読む、というのがポイント。A=期待値、B=実際の値。この読み方さえ覚えておけば、他のエラーでも同じ考え方が使えそう。\n\n今回の例で言うと「`/signup`に遷移してほしかったのに、実際は`/signup_path`に遷移してしまった」ということが、この3行だけで分かる。\n\n### ② スクリーンショットのパス\n\n```\n[Screenshot Image]: /myapp/tmp/capybara/failures_....png\n```\n\nテストが失敗した瞬間の画面を自動で撮影した画像の保存場所。文字情報だけで原因が分かる時は必須ではないけれど、迷ったら開いて画面を見ると理解の助けになる。\n\n### ③ スタックトレース(処理の呼び出し順序)\n\n```\n# /usr/local/bundle/gems/capybara-3.40.0/lib/capybara/session/matchers.rb:24:in `block in assert_current_path'\n...\n# ./spec/system/fix_bug_spec.rb:15:in `block (5 levels) in \u003ctop (required)\u003e'\n```\n\nエラーが起きるまでにどのファイルのどの処理が呼ばれたかの記録。**下から上に読む**のがポイントで、一番下の`./spec/system/fix_bug_spec.rb:15`が「自分が書いたテストコードの中で、最初に呼び出された場所」。\n\ngem内部の行(`matchers.rb`など)は基本的に読み込む必要なし。今回の例では「`fix_bug_spec.rb`の15行目で`assert_current_path`が呼ばれ、それがCapybara内部の処理を経てエラーになった」という流れがここから分かる。\n\n## 3つを合体させて「読み方の型」にする\n\n1. **③**を見て「コードのどこで起きたか」を特定する → `fix_bug_spec.rb:15`\n2. **①**を見て「何が期待と違ったか」を特定する → `/signup`を期待していたのに`/signup_path`になっていた\n3. **②**は必要なら画面で確認する(今回は文字情報で十分)\n\nつまり、**③で「場所」を、①で「原因」を確認する**、という2段階で読むのが基本パターン。\n\n## 原因を突き止める\n\n①から「アプリ側が`/signup`ではなく`/signup_path`という誤ったパスに遷移させている」ことが分かる。今回のケースは、ビューファイルで`signup_path`をクォート付き文字列にしてしまっているパターンだった。\n\n```erb\n\u003c%# NG: 文字列として扱われ、/signup_path という誤ったURLになる %\u003e\n\u003c%= link_to \"始めよう\", \"signup_path\" %\u003e\n\n\u003c%# OK: パスヘルパーとして正しく解釈される %\u003e\n\u003c%= link_to \"始めよう\", signup_path %\u003e\n```\n\nクォートで囲んでしまうと、Railsは`signup_path`をパスヘルパーではなく単なる文字列として扱ってしまい、その結果`/signup_path`という存在しないパスに飛んでしまう、というオチ。\n\n## まとめ\n\n- エラーログは「①原因」「②スクリーンショット」「③発生場所」の3ブロックに分けて読む\n- `expected \"A\" to equal \"B\"`は「Aのはずが実際はBだった」と読む\n- スタックトレースは下から上へ、自分のコードの行を探す\n- パスヘルパーは文字列としてクォートしない\n\nエラーログを闇雲に上から読んでいた頃に比べて、「まず場所→次に原因」という順番を意識するだけでかなり読みやすくなった気がします。同じようにエラーログに苦手意識がある初学者の方の参考になれば嬉しいです。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:32:56+09:00","group":null,"id":"ad70af0c5c219bfba1bd","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Rails","versions":[]},{"name":"Ruby","versions":[]},{"name":"初学者","versions":[]},{"name":"RUNTEQ","versions":[]}],"title":"RSpecのエラーログ、読み方の型がなんとなく分かった話","updated_at":"2026-09-27T11:32:56+09:00","url":"https://qiita.com/taka58/items/ad70af0c5c219bfba1bd","user":{"description":null,"facebook_id":null,"followees_count":3,"followers_count":0,"github_login_name":null,"id":"taka58","items_count":5,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4493382,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4493382/profile-images/1787986462","team_only":false,"twitter_screen_name":"KDrunteq","website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:110\"\u003e個人でWebサイトやアプリを作っています（屋号は \u003ca href=\"https://sesebox.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003esesebox\u003c/a\u003e です）。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-3:324\"\u003eひとつのGitリポジトリに、アプリをたくさん入れて管理しています（こういう形を「モノレポ」と呼びます）。その中の1本を「いったん保留」にしたあと、\u003cstrong\u003e保留にしたはずのアプリが、pushのたびにVercelでデプロイされ続けていた\u003c/strong\u003e話です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"5:1-5:72\"\u003e\n\u003cspan id=\"気づいたきっかけ失敗のメールが大量に来ていた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%B0%97%E3%81%A5%E3%81%84%E3%81%9F%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E5%A4%B1%E6%95%97%E3%81%AE%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%8C%E5%A4%A7%E9%87%8F%E3%81%AB%E6%9D%A5%E3%81%A6%E3%81%84%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e気づいたきっかけ：失敗のメールが大量に来ていた\u003c/h2\u003e\n\u003cp data-sourcepos=\"7:1-7:96\"\u003e朝メールを見ると、Vercelからこんなメールがたくさん届いていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"9:1-12:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eProduction deployment failed for haiote\nPreview deployment failed for haiote\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"14:1-14:195\"\u003e\u003ccode\u003ehaiote\u003c/code\u003e は、前の日に試作して「いったん保留」にしたゲームです。もう触っていないのに、なぜかデプロイが走って、しかも失敗していました。\u003c/p\u003e\n\u003cp data-sourcepos=\"16:1-16:75\"\u003eデプロイの一覧を出してみると、こうなっていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"18:1-20:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003enpx vercel \u003cspan class=\"nb\"\u003els \u003c/span\u003ehaiote \u003cspan class=\"nt\"\u003e--scope\u003c/span\u003e \u0026lt;チーム名\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"22:1-29:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eAge   Deployment                         Status     Environment\n15m   https://haiote-xxxx.vercel.app     Canceled   Production\n22m   https://haiote-xxxx.vercel.app     Canceled   Production\n33m   https://haiote-xxxx.vercel.app     Error      Preview\n10h   https://haiote-xxxx.vercel.app     Error      Production\n...\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"31:1-31:109\"\u003e\u003cstrong\u003e保留にしたアプリなのに、数十分おきに新しいデプロイが作られていました。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"33:1-33:39\"\u003e\n\u003cspan id=\"原因git連携が残っていた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8E%9F%E5%9B%A0git%E9%80%A3%E6%90%BA%E3%81%8C%E6%AE%8B%E3%81%A3%E3%81%A6%E3%81%84%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e原因：Git連携が残っていた\u003c/h2\u003e\n\u003cp data-sourcepos=\"35:1-35:209\"\u003eVercelのプロジェクトには、GitHubのリポジトリとつなぐ「Git連携」という機能があります。つないでおくと、\u003cstrong\u003epushするたびに自動でデプロイ\u003c/strong\u003eしてくれます。\u003c/p\u003e\n\u003cp data-sourcepos=\"37:1-37:75\"\u003e問題は、自分のリポジトリがモノレポだったことです。\u003c/p\u003e\n\u003cul data-sourcepos=\"39:1-42:0\"\u003e\n\u003cli data-sourcepos=\"39:1-39:57\"\u003e\n\u003ccode\u003ehaiote\u003c/code\u003e のコードを1行も変えていなくても\u003c/li\u003e\n\u003cli data-sourcepos=\"40:1-40:70\"\u003e\u003cstrong\u003e同じリポジトリのほかのアプリをpushしただけで\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"41:1-42:0\"\u003e\n\u003ccode\u003ehaiote\u003c/code\u003e のデプロイも作られる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"43:1-43:136\"\u003eという状態でした。ほかのアプリを作業するたびに、関係ない \u003ccode\u003ehaiote\u003c/code\u003e が巻き込まれていたわけです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"45:1-45:73\"\u003e\n\u003cspan id=\"何がまずいのか無料枠の1日100デプロイを食う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%95%E3%81%8C%E3%81%BE%E3%81%9A%E3%81%84%E3%81%AE%E3%81%8B%E7%84%A1%E6%96%99%E6%9E%A0%E3%81%AE1%E6%97%A5100%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%82%92%E9%A3%9F%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e何がまずいのか：無料枠の「1日100デプロイ」を食う\u003c/h2\u003e\n\u003cp data-sourcepos=\"47:1-47:124\"\u003eVercelの無料プラン（Hobby）には、\u003cstrong\u003e1日に作れるデプロイは100件まで\u003c/strong\u003eという上限があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"49:1-49:304\"\u003eここで大事なのは、\u003cstrong\u003eキャンセルされたデプロイ（Canceled）も1件として数えられる\u003c/strong\u003eことです。「ビルドをスキップする設定（Ignored Build Step）」を入れてあっても、デプロイの記録自体は作られるので、枠はしっかり減ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"51:1-51:164\"\u003e実際、以前この上限に当たって、本当にデプロイしたいアプリが「24時間待ってください」で止まったことがありました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"53:1-56:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eError: Resource is limited - try again in 24 hours\n(more than 100, code: \"api-deployments-free-per-day\")\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"58:1-58:87\"\u003e保留中のアプリに枠を食われるのは、まったく得がありません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"60:1-60:30\"\u003e\n\u003cspan id=\"対処git連携を切る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AF%BE%E5%87%A6git%E9%80%A3%E6%90%BA%E3%82%92%E5%88%87%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e対処：Git連携を切る\u003c/h2\u003e\n\u003cp data-sourcepos=\"62:1-62:135\"\u003e保留中のアプリは、pushで自動デプロイされる必要がありません。なので \u003cstrong\u003eGit連携だけ切りました。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"64:1-64:34\"\u003eポイントは次の3つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"66:1-69:0\"\u003e\n\u003cli data-sourcepos=\"66:1-66:66\"\u003e\n\u003cstrong\u003eプロジェクトは消さない\u003c/strong\u003e（消すと戻せない）\u003c/li\u003e\n\u003cli data-sourcepos=\"67:1-67:108\"\u003e\n\u003cstrong\u003e公開中のサイトはそのまま残る\u003c/strong\u003e（いま動いている本番デプロイは消えない）\u003c/li\u003e\n\u003cli data-sourcepos=\"68:1-69:0\"\u003e\u003cstrong\u003eあとでつなぎ直せる\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"70:1-70:10\"\u003e\n\u003cspan id=\"手順\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%89%8B%E9%A0%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e手順\u003c/h3\u003e\n\u003cp data-sourcepos=\"72:1-72:225\"\u003eVercel CLIの \u003ccode\u003evercel git disconnect\u003c/code\u003e を使います。このコマンドは「\u003cstrong\u003e今いるフォルダがどのプロジェクトにつながっているか\u003c/strong\u003e」を見て、そのプロジェクトの連携を切ります。\u003c/p\u003e\n\u003cp data-sourcepos=\"74:1-74:166\"\u003eところが、\u003ccode\u003ehaiote\u003c/code\u003e のフォルダには、プロジェクトとのつながりを書いたファイル（\u003ccode\u003e.vercel/project.json\u003c/code\u003e）がありませんでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"76:1-76:118\"\u003eそこで、\u003cstrong\u003eリポジトリの外に空のフォルダを作って\u003c/strong\u003e、そこでつなげてから切りました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"78:1-90:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nb\"\u003emkdir\u003c/span\u003e /tmp/haiote-link\n\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e /tmp/haiote-link\n\n\u003cspan class=\"c\"\u003e# ① このフォルダを haiote プロジェクトにつなげる\u003c/span\u003e\nnpx vercel \u003cspan class=\"nb\"\u003elink\u003c/span\u003e \u003cspan class=\"nt\"\u003e--yes\u003c/span\u003e \u003cspan class=\"nt\"\u003e--project\u003c/span\u003e haiote \u003cspan class=\"nt\"\u003e--scope\u003c/span\u003e \u0026lt;チーム名\u0026gt;\n\n\u003cspan class=\"c\"\u003e# ② つながったか確認（projectName が haiote になっていればOK）\u003c/span\u003e\n\u003cspan class=\"nb\"\u003ecat\u003c/span\u003e .vercel/project.json\n\n\u003cspan class=\"c\"\u003e# ③ Git連携を切る\u003c/span\u003e\nnpx vercel git disconnect \u003cspan class=\"nt\"\u003e--yes\u003c/span\u003e \u003cspan class=\"nt\"\u003e--scope\u003c/span\u003e \u0026lt;チーム名\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"92:1-94:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u0026gt; Disconnected \u0026lt;ユーザー名\u0026gt;/\u0026lt;リポジトリ名\u0026gt;.\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"96:1-96:173\"\u003eこれで、pushしても \u003ccode\u003ehaiote\u003c/code\u003e のデプロイは作られなくなりました。念のためサイトを開いてみると、ちゃんと表示されていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"98:1-101:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecurl \u003cspan class=\"nt\"\u003e-s\u003c/span\u003e \u003cspan class=\"nt\"\u003e-o\u003c/span\u003e /dev/null \u003cspan class=\"nt\"\u003e-w\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"%{http_code}\u003c/span\u003e\u003cspan class=\"se\"\u003e\\n\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e https://haiote.vercel.app\n\u003cspan class=\"c\"\u003e# 200\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"103:1-103:40\"\u003e\n\u003cspan id=\"リポジトリの外でやる理由\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E5%A4%96%E3%81%A7%E3%82%84%E3%82%8B%E7%90%86%E7%94%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eリポジトリの外でやる理由\u003c/h3\u003e\n\u003cp data-sourcepos=\"105:1-105:275\"\u003e\u003ccode\u003evercel link\u003c/code\u003e を実行すると、そのフォルダに \u003ccode\u003e.vercel/\u003c/code\u003e フォルダや \u003ccode\u003e.env.local\u003c/code\u003e ファイルが作られます。リポジトリの中でやると、\u003cstrong\u003e関係ないファイルが増えて、うっかりコミットしてしまう\u003c/strong\u003eおそれがあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"107:1-107:72\"\u003e切るためだけなら、使い捨てのフォルダで十分です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"109:1-109:32\"\u003e\n\u003cspan id=\"-の確認は必ずやる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#-%E3%81%AE%E7%A2%BA%E8%AA%8D%E3%81%AF%E5%BF%85%E3%81%9A%E3%82%84%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e② の確認は必ずやる\u003c/h3\u003e\n\u003cp data-sourcepos=\"111:1-111:324\"\u003e\u003ccode\u003evercel git disconnect\u003c/code\u003e は、\u003cstrong\u003e今いるフォルダのプロジェクト\u003c/strong\u003eが対象です。間違ったフォルダで打つと、\u003cstrong\u003e別のアプリの連携を切ってしまいます。\u003c/strong\u003e 打つ前に \u003ccode\u003e.vercel/project.json\u003c/code\u003e の中身を見て、対象のプロジェクト名になっているか確かめています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"113:1-113:63\"\u003e\n\u003cspan id=\"ついでに踏んだことapiでやろうとしたら403\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%84%E3%81%A7%E3%81%AB%E8%B8%8F%E3%82%93%E3%81%A0%E3%81%93%E3%81%A8api%E3%81%A7%E3%82%84%E3%82%8D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E3%82%89403\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eついでに踏んだこと：APIでやろうとしたら403\u003c/h2\u003e\n\u003cp data-sourcepos=\"115:1-115:174\"\u003e最初は、VercelのREST APIを直接呼んで切ろうとしました。CLIがログイン情報を保存している \u003ccode\u003eauth.json\u003c/code\u003e のトークンを使ったのですが、\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"117:1-119:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eHTTP Error 403: Forbidden\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"121:1-121:130\"\u003eで弾かれました。同じパソコンでCLIは普通に動いていたので、\u003cstrong\u003eCLIに任せるほうが確実\u003c/strong\u003eでした。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"123:1-123:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"125:1-129:0\"\u003e\n\u003cli data-sourcepos=\"125:1-125:121\"\u003eモノレポでGit連携をしていると、\u003cstrong\u003eほかのアプリをpushしただけでもデプロイが作られる\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"126:1-126:107\"\u003eVercelの無料プランは\u003cstrong\u003e1日100デプロイまで\u003c/strong\u003e。\u003cstrong\u003eキャンセルされたものも数える\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"127:1-127:132\"\u003e保留・終了にしたアプリは、\u003cstrong\u003eGit連携だけ切る\u003c/strong\u003e（プロジェクトもサイトも残る・あとで戻せる）\u003c/li\u003e\n\u003cli data-sourcepos=\"128:1-129:0\"\u003e\n\u003ccode\u003evercel git disconnect\u003c/code\u003e は\u003cstrong\u003e今いるフォルダが対象\u003c/strong\u003e。リポジトリの外の空フォルダで \u003ccode\u003evercel link\u003c/code\u003e してから切ると安全\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"130:1-130:247\"\u003eアプリを「保留」にするときに、コードのことは考えても、\u003cstrong\u003eデプロイの設定のことは忘れがち\u003c/strong\u003eでした。今は「保留にしたら、Git連携が残っていないか見る」を手順に入れています。\u003c/p\u003e\n\u003cp data-sourcepos=\"132:1-132:93\"\u003e作っているものの一覧は \u003ca href=\"https://sesebox.dev\" rel=\"nofollow noopener\" target=\"_blank\"\u003esesebox.dev\u003c/a\u003e に置いています。\u003c/p\u003e\n","body":"個人でWebサイトやアプリを作っています（屋号は [sesebox](https://sesebox.dev) です）。\n\nひとつのGitリポジトリに、アプリをたくさん入れて管理しています（こういう形を「モノレポ」と呼びます）。その中の1本を「いったん保留」にしたあと、**保留にしたはずのアプリが、pushのたびにVercelでデプロイされ続けていた**話です。\n\n## 気づいたきっかけ：失敗のメールが大量に来ていた\n\n朝メールを見ると、Vercelからこんなメールがたくさん届いていました。\n\n```\nProduction deployment failed for haiote\nPreview deployment failed for haiote\n```\n\n`haiote` は、前の日に試作して「いったん保留」にしたゲームです。もう触っていないのに、なぜかデプロイが走って、しかも失敗していました。\n\nデプロイの一覧を出してみると、こうなっていました。\n\n```bash\nnpx vercel ls haiote --scope \u003cチーム名\u003e\n```\n\n```\nAge   Deployment                         Status     Environment\n15m   https://haiote-xxxx.vercel.app     Canceled   Production\n22m   https://haiote-xxxx.vercel.app     Canceled   Production\n33m   https://haiote-xxxx.vercel.app     Error      Preview\n10h   https://haiote-xxxx.vercel.app     Error      Production\n...\n```\n\n**保留にしたアプリなのに、数十分おきに新しいデプロイが作られていました。**\n\n## 原因：Git連携が残っていた\n\nVercelのプロジェクトには、GitHubのリポジトリとつなぐ「Git連携」という機能があります。つないでおくと、**pushするたびに自動でデプロイ**してくれます。\n\n問題は、自分のリポジトリがモノレポだったことです。\n\n- `haiote` のコードを1行も変えていなくても\n- **同じリポジトリのほかのアプリをpushしただけで**\n- `haiote` のデプロイも作られる\n\nという状態でした。ほかのアプリを作業するたびに、関係ない `haiote` が巻き込まれていたわけです。\n\n## 何がまずいのか：無料枠の「1日100デプロイ」を食う\n\nVercelの無料プラン（Hobby）には、**1日に作れるデプロイは100件まで**という上限があります。\n\nここで大事なのは、**キャンセルされたデプロイ（Canceled）も1件として数えられる**ことです。「ビルドをスキップする設定（Ignored Build Step）」を入れてあっても、デプロイの記録自体は作られるので、枠はしっかり減ります。\n\n実際、以前この上限に当たって、本当にデプロイしたいアプリが「24時間待ってください」で止まったことがありました。\n\n```\nError: Resource is limited - try again in 24 hours\n(more than 100, code: \"api-deployments-free-per-day\")\n```\n\n保留中のアプリに枠を食われるのは、まったく得がありません。\n\n## 対処：Git連携を切る\n\n保留中のアプリは、pushで自動デプロイされる必要がありません。なので **Git連携だけ切りました。**\n\nポイントは次の3つです。\n\n- **プロジェクトは消さない**（消すと戻せない）\n- **公開中のサイトはそのまま残る**（いま動いている本番デプロイは消えない）\n- **あとでつなぎ直せる**\n\n### 手順\n\nVercel CLIの `vercel git disconnect` を使います。このコマンドは「**今いるフォルダがどのプロジェクトにつながっているか**」を見て、そのプロジェクトの連携を切ります。\n\nところが、`haiote` のフォルダには、プロジェクトとのつながりを書いたファイル（`.vercel/project.json`）がありませんでした。\n\nそこで、**リポジトリの外に空のフォルダを作って**、そこでつなげてから切りました。\n\n```bash\nmkdir /tmp/haiote-link\ncd /tmp/haiote-link\n\n# ① このフォルダを haiote プロジェクトにつなげる\nnpx vercel link --yes --project haiote --scope \u003cチーム名\u003e\n\n# ② つながったか確認（projectName が haiote になっていればOK）\ncat .vercel/project.json\n\n# ③ Git連携を切る\nnpx vercel git disconnect --yes --scope \u003cチーム名\u003e\n```\n\n```\n\u003e Disconnected \u003cユーザー名\u003e/\u003cリポジトリ名\u003e.\n```\n\nこれで、pushしても `haiote` のデプロイは作られなくなりました。念のためサイトを開いてみると、ちゃんと表示されていました。\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://haiote.vercel.app\n# 200\n```\n\n### リポジトリの外でやる理由\n\n`vercel link` を実行すると、そのフォルダに `.vercel/` フォルダや `.env.local` ファイルが作られます。リポジトリの中でやると、**関係ないファイルが増えて、うっかりコミットしてしまう**おそれがあります。\n\n切るためだけなら、使い捨てのフォルダで十分です。\n\n### ② の確認は必ずやる\n\n`vercel git disconnect` は、**今いるフォルダのプロジェクト**が対象です。間違ったフォルダで打つと、**別のアプリの連携を切ってしまいます。** 打つ前に `.vercel/project.json` の中身を見て、対象のプロジェクト名になっているか確かめています。\n\n## ついでに踏んだこと：APIでやろうとしたら403\n\n最初は、VercelのREST APIを直接呼んで切ろうとしました。CLIがログイン情報を保存している `auth.json` のトークンを使ったのですが、\n\n```\nHTTP Error 403: Forbidden\n```\n\nで弾かれました。同じパソコンでCLIは普通に動いていたので、**CLIに任せるほうが確実**でした。\n\n## まとめ\n\n- モノレポでGit連携をしていると、**ほかのアプリをpushしただけでもデプロイが作られる**\n- Vercelの無料プランは**1日100デプロイまで**。**キャンセルされたものも数える**\n- 保留・終了にしたアプリは、**Git連携だけ切る**（プロジェクトもサイトも残る・あとで戻せる）\n- `vercel git disconnect` は**今いるフォルダが対象**。リポジトリの外の空フォルダで `vercel link` してから切ると安全\n\nアプリを「保留」にするときに、コードのことは考えても、**デプロイの設定のことは忘れがち**でした。今は「保留にしたら、Git連携が残っていないか見る」を手順に入れています。\n\n作っているものの一覧は [sesebox.dev](https://sesebox.dev) に置いています。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:31:19+09:00","group":null,"id":"3ffd3de86f8cc1dca27b","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Vercel","versions":[]},{"name":"Git","versions":[]},{"name":"個人開発","versions":[]},{"name":"初心者","versions":[]},{"name":"モノレポ","versions":[]}],"title":"保留にしたアプリが、pushのたびにVercelでデプロイされ続けていました（Git連携の切り方）","updated_at":"2026-09-27T11:31:19+09:00","url":"https://qiita.com/tarou_0818/items/3ffd3de86f8cc1dca27b","user":{"description":null,"facebook_id":null,"followees_count":1,"followers_count":2,"github_login_name":null,"id":"tarou_0818","items_count":58,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":4462092,"profile_image_url":"https://secure.gravatar.com/avatar/a88340750da8a8db4d0fa3bfb0a9a3ee","team_only":false,"twitter_screen_name":null,"website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:55\"\u003e\n\u003cspan id=\"aiコーディングのためのターミナル選び\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#ai%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E9%81%B8%E3%81%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eAIコーディングのためのターミナル選び\u003c/h1\u003e\n\u003cp data-sourcepos=\"3:1-3:178\"\u003e\u003cem\u003e2026年9月時点の情報です。この分野では、わずか数か月で前提が変わることがあります。情報の基準日を意識してお読みください。\u003c/em\u003e\u003c/p\u003e\n\u003ch2 data-sourcepos=\"5:1-5:69\"\u003e\n\u003cspan id=\"はじめになぜ今ターミナルを選び直すのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB%E3%81%AA%E3%81%9C%E4%BB%8A%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%82%92%E9%81%B8%E3%81%B3%E7%9B%B4%E3%81%99%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに：なぜ今「ターミナル」を選び直すのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"7:1-7:248\"\u003e私はここ数年、macOS 上で \u003cstrong\u003eiTerm2 + VS Code\u003c/strong\u003e という構成を使って開発してきました。しかし、Claude Code と Codex を毎日使うようになると、ターミナルに求める役割が明らかに変わりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:312\"\u003e従来のターミナルは、「自分がコマンドを入力する場所」でした。一方、現在は「エージェントが長時間作業し、必要に応じて人間に許可や判断を求める場所」になりつつあります。その結果、次のような新しい悩みが生まれます。\u003c/p\u003e\n\u003cul data-sourcepos=\"11:1-15:0\"\u003e\n\u003cli data-sourcepos=\"11:1-11:114\"\u003eClaude Code を3つ並べたら、どのタブが自分の入力を待っているのか分からなくなった\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-12:117\"\u003e夜にエージェントへ大きなタスクを任せてノート PC を閉じたら、作業が止まっていた\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:68\"\u003eShift+Enter で改行したつもりが送信されてしまった\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-15:0\"\u003e通知が来ない（あるいは来すぎる）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"16:1-16:238\"\u003eそこで、近年注目されている \u003cstrong\u003etmux、cmux、Orca、OpenCode、Warp、herdr、Zed、Conductor、Traycer\u003c/strong\u003e を調べ、「Claude Code と Codex を主に使う iTerm2 + VS Code ユーザー」の視点から整理しました。\u003c/p\u003e\n\u003chr data-sourcepos=\"18:1-19:0\"\u003e\n\u003ch2 data-sourcepos=\"20:1-20:7\"\u003e\n\u003cspan id=\"tldr\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tldr\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTLDR\u003c/h2\u003e\n\u003col data-sourcepos=\"22:1-26:0\"\u003e\n\u003cli data-sourcepos=\"22:1-22:371\"\u003e\n\u003cstrong\u003e「どのターミナルに乗り換えるか」という問いだけでは、もはや十分ではありません。\u003c/strong\u003e 上に挙げた9つのツールは、それぞれ異なる「層」に属しています。たとえば、tmux と Orca は同じ基準で比較できません。考えるべきなのは、「自分の運用にどの層まで必要か」です。\u003c/li\u003e\n\u003cli data-sourcepos=\"23:1-23:353\"\u003e\n\u003cstrong\u003eiTerm2 ユーザーは、すぐに別のターミナルへ乗り換える必要はありません。\u003c/strong\u003e 2026年9月8日にリリースされた iTerm2 3.7 には、Claude Code の公式統合機能が加わりました。また、Claude Code の Agent Teams（分割ペイン表示）に公式対応するのは、\u003cstrong\u003etmux と iTerm2 のみ\u003c/strong\u003eです。\u003c/li\u003e\n\u003cli data-sourcepos=\"24:1-24:261\"\u003e\n\u003cstrong\u003eVS Code の統合ターミナルは、公式ドキュメントに照らすと制約の多い実行環境です。\u003c/strong\u003e VS Code はエージェントの主な「実行場所」ではなく、「レビューする場所」として使うのが合理的です。\u003c/li\u003e\n\u003cli data-sourcepos=\"25:1-26:0\"\u003e\n\u003cstrong\u003e並列で動かすエージェントが常時4つを超えるなら、新たな層を1つ追加しましょう。\u003c/strong\u003e 永続化と SSH を重視するなら herdr、macOS ネイティブの見やすさを求めるなら cmux、worktree の作成から PR まで一貫して管理したいなら Conductor または Orca が候補になります。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr data-sourcepos=\"27:1-28:0\"\u003e\n\u003ch2 data-sourcepos=\"29:1-29:67\"\u003e\n\u003cspan id=\"1-ターミナル選びは5層スタックの選択です\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E9%81%B8%E3%81%B3%E3%81%AF5%E5%B1%A4%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E9%81%B8%E6%8A%9E%E3%81%A7%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 「ターミナル選び」は5層スタックの選択です\u003c/h2\u003e\n\u003cp data-sourcepos=\"31:1-31:99\"\u003eまず、調査対象の各ツールがどの層に位置づけられるのかを整理します。\u003c/p\u003e\n\u003ctable data-sourcepos=\"33:1-39:126\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"33:1-33:34\"\u003e\n\u003cth data-sourcepos=\"33:2-33:6\"\u003e層\u003c/th\u003e\n\u003cth data-sourcepos=\"33:8-33:15\"\u003e役割\u003c/th\u003e\n\u003cth data-sourcepos=\"33:17-33:33\"\u003e主なツール\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"35:1-35:34\"\u003e\n\u003ctd data-sourcepos=\"35:2-35:5\"\u003eL5\u003c/td\u003e\n\u003ctd data-sourcepos=\"35:7-35:23\"\u003e計画・仕様\u003c/td\u003e\n\u003ctd data-sourcepos=\"35:25-35:33\"\u003eTraycer\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"36:1-36:123\"\u003e\n\u003ctd data-sourcepos=\"36:2-36:5\"\u003eL4\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:7-36:47\"\u003eオーケストレーション（ADE）\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:49-36:122\"\u003eOrca / Conductor / Codex app / Claude Code Desktop / Zed（ACP 経由）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"37:1-37:70\"\u003e\n\u003ctd data-sourcepos=\"37:2-37:5\"\u003eL3\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:7-37:32\"\u003eエージェント本体\u003c/td\u003e\n\u003ctd data-sourcepos=\"37:34-37:69\"\u003eClaude Code / Codex CLI / OpenCode\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"38:1-38:54\"\u003e\n\u003ctd data-sourcepos=\"38:2-38:5\"\u003eL2\u003c/td\u003e\n\u003ctd data-sourcepos=\"38:7-38:29\"\u003e多重化・永続化\u003c/td\u003e\n\u003ctd data-sourcepos=\"38:31-38:53\"\u003etmux / herdr / Zellij\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"39:1-39:126\"\u003e\n\u003ctd data-sourcepos=\"39:2-39:5\"\u003eL1\u003c/td\u003e\n\u003ctd data-sourcepos=\"39:7-39:41\"\u003eターミナルエミュレータ\u003c/td\u003e\n\u003ctd data-sourcepos=\"39:43-39:125\"\u003eiTerm2 / Ghostty / cmux / Warp / VS Code 統合ターミナル / Zed 内蔵端末\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cul data-sourcepos=\"41:1-50:0\"\u003e\n\u003cli data-sourcepos=\"41:1-41:165\"\u003e\n\u003cstrong\u003etmux（L2）\u003c/strong\u003e：古くから使われているマルチプレクサ。Claude Code Agent Teams の分割ペインを公式に支えるバックエンドです。\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-42:134\"\u003e\n\u003cstrong\u003eherdr（L2）\u003c/strong\u003e：エージェントの状態（working / blocked / idle / done）を把握できる常駐ランタイムです。\u003c/li\u003e\n\u003cli data-sourcepos=\"43:1-43:132\"\u003e\n\u003cstrong\u003ecmux（L1+L2）\u003c/strong\u003e：libghostty で描画する macOS ネイティブ端末。サイドバーと通知リングが特徴です。\u003c/li\u003e\n\u003cli data-sourcepos=\"44:1-44:141\"\u003e\n\u003cstrong\u003eWarp（L1+L4）\u003c/strong\u003e：「Agentic Development Environment」を掲げる端末で、2026年4月にオープンソース化されました。\u003c/li\u003e\n\u003cli data-sourcepos=\"45:1-45:150\"\u003e\n\u003cstrong\u003eZed（L1/L4）\u003c/strong\u003e：Rust 製エディタ。ACP を介し、Claude Code や Codex をエディタ内の並列スレッドとして動かせます。\u003c/li\u003e\n\u003cli data-sourcepos=\"46:1-46:107\"\u003e\n\u003cstrong\u003eOrca（L4）\u003c/strong\u003e：OSS の ADE。タスクごとに worktree、端末、ブラウザを用意します。\u003c/li\u003e\n\u003cli data-sourcepos=\"47:1-47:134\"\u003e\n\u003cstrong\u003eConductor（L4）\u003c/strong\u003e：macOS 向けアプリ。worktree、端末、diff、PR を一つのワークスペースにまとめます。\u003c/li\u003e\n\u003cli data-sourcepos=\"48:1-48:121\"\u003e\n\u003cstrong\u003eTraycer（L5）\u003c/strong\u003e：仕様（spec / ticket）を先に固め、任意のエージェントへ実装を渡します。\u003c/li\u003e\n\u003cli data-sourcepos=\"49:1-50:0\"\u003e\n\u003cstrong\u003eOpenCode（L3）\u003c/strong\u003e：プロバイダに依存しない OSS エージェント。Claude Code や Codex と同じ層に位置します。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"51:1-51:511\"\u003eたとえば、「cmux と herdr はどちらがよいか」と聞かれたら、「cmux は人間が状況を把握する場所を、herdr はエージェントが稼働する環境を最適化するツール」と整理するのが正確です。cmux はターミナルエミュレータ自体なので、iTerm2 から乗り換えるか、別のアプリとして併用します。一方、herdr は iTerm2 の中で動かせます。この違いを理解すると、選択肢を絞りやすくなります。\u003c/p\u003e\n\u003chr data-sourcepos=\"53:1-54:0\"\u003e\n\u003ch2 data-sourcepos=\"55:1-55:79\"\u003e\n\u003cspan id=\"2-claude-code-と-codex-はターミナルに何を求めているのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-claude-code-%E3%81%A8-codex-%E3%81%AF%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%81%AB%E4%BD%95%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. Claude Code と Codex は、ターミナルに何を求めているのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"57:1-57:300\"\u003eツールを比較する前に、エージェント側がターミナルに求める要件を、公式ドキュメントに基づいて確認します。これらを押さえておけば、「なぜこの端末では通知が届かないのか」といった疑問を理解しやすくなります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"59:1-59:41\"\u003e\n\u003cspan id=\"21-shiftenter-で改行できるか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#21-shiftenter-%E3%81%A7%E6%94%B9%E8%A1%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.1 Shift+Enter で改行できるか\u003c/h3\u003e\n\u003cp data-sourcepos=\"61:1-61:308\"\u003eClaude Code では、Enter キーでメッセージを送信します。改行を入れる方法は、使用する端末によって異なります。公式ドキュメント（\u003ca href=\"https://code.claude.com/docs/en/terminal-config\" rel=\"nofollow noopener\" target=\"_blank\"\u003eConfigure your terminal\u003c/a\u003e）によると、対応状況は次のとおりです。\u003c/p\u003e\n\u003cul data-sourcepos=\"63:1-66:0\"\u003e\n\u003cli data-sourcepos=\"63:1-63:116\"\u003e\n\u003cstrong\u003e設定なしで使える端末\u003c/strong\u003e：Ghostty、Kitty、iTerm2、WezTerm、Warp、Apple Terminal、Windows Terminal\u003c/li\u003e\n\u003cli data-sourcepos=\"64:1-64:121\"\u003e\n\u003cstrong\u003e初回設定が必要な端末\u003c/strong\u003e：VS Code、Cursor、Alacritty、Zed。\u003ccode\u003e/terminal-setup\u003c/code\u003e を一度実行します。\u003c/li\u003e\n\u003cli data-sourcepos=\"65:1-66:0\"\u003e\n\u003cstrong\u003eShift+Enter に対応していない端末\u003c/strong\u003e：gnome-terminal、JetBrains 系 IDE。Ctrl+J、または「\u003ccode\u003e\\\u003c/code\u003e の後に Enter」を使います。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"67:1-67:142\"\u003e端末に依存しない代替手段として、Ctrl+J、または「\u003ccode\u003e\\\u003c/code\u003e の後に Enter」という操作も覚えておくと安心です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"69:1-69:53\"\u003e\n\u003cspan id=\"22-デスクトップ通知が標準で届くか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#22-%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E9%80%9A%E7%9F%A5%E3%81%8C%E6%A8%99%E6%BA%96%E3%81%A7%E5%B1%8A%E3%81%8F%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.2 デスクトップ通知が標準で届くか\u003c/h3\u003e\n\u003cp data-sourcepos=\"71:1-71:507\"\u003e意外に知られていませんが、Claude Code が標準でデスクトップ通知を出すのは、\u003cstrong\u003eGhostty、Kitty、iTerm2 の3種類のみ\u003c/strong\u003eです。それ以外の端末では、\u003ccode\u003epreferredNotifChannel\u003c/code\u003e を \u003ccode\u003e\"terminal_bell\"\u003c/code\u003e に設定するか、Notification hook を用意する必要があります。公式ドキュメントでは、Warp と VS Code の統合ターミナルが、通知を受け取るために hook または bell の設定を要する端末として明記されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"73:1-73:323\"\u003eiTerm2 でも、Settings → Profiles → Terminal の「Notification Center Alerts」を有効にし、さらに「Filter Alerts」から「Send escape sequence-generated alerts」を有効にする必要があります。これらの設定を見落とすと、iTerm2 を使っていても通知は表示されません。\u003c/p\u003e\n\u003cp data-sourcepos=\"75:1-75:95\"\u003e任意の端末で通知音を鳴らしたい場合は、hook を使う方法が確実です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"77:1-91:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"err\"\u003e//\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e~/.claude/settings.json\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Notification\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"matcher\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"permission_prompt|idle_prompt\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"afplay /System/Library/Sounds/Glass.aiff\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"93:1-93:148\"\u003e\u003ccode\u003epermission_prompt\u003c/code\u003e は許可を待っているとき、\u003ccode\u003eidle_prompt\u003c/code\u003e は一定時間、入力待ちの状態が続いたときに発火します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"95:1-95:68\"\u003e\n\u003cspan id=\"23-tmux-の中で動かすには3行の設定が必要です\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#23-tmux-%E3%81%AE%E4%B8%AD%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99%E3%81%AB%E3%81%AF3%E8%A1%8C%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%A7%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.3 tmux の中で動かすには、3行の設定が必要です\u003c/h3\u003e\n\u003cp data-sourcepos=\"97:1-97:270\"\u003etmux の中で Claude Code を動かすと、標準設定では Shift+Enter が送信として扱われ、デスクトップ通知や進捗バーも外側の端末まで届きません。公式ドキュメントが示す \u003ccode\u003e~/.tmux.conf\u003c/code\u003e の設定は、次の3行です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"tmux\" data-sourcepos=\"99:1-103:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eset -g allow-passthrough on          # 通知と進捗を外側の端末へ通す\nset -s extended-keys on              # Shift+Enter を Enter と区別する\nset -as terminal-features 'xterm*:extkeys'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"105:1-105:113\"\u003e\u003ccode\u003etmux source-file ~/.tmux.conf\u003c/code\u003e を実行すれば、稼働中のサーバーにも設定を反映できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"107:1-107:66\"\u003e\n\u003cspan id=\"24-agent-teams分割ペインは-tmux-か-iterm2-だけ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#24-agent-teams%E5%88%86%E5%89%B2%E3%83%9A%E3%82%A4%E3%83%B3%E3%81%AF-tmux-%E3%81%8B-iterm2-%E3%81%A0%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.4 Agent Teams（分割ペイン）は tmux か iTerm2 だけ\u003c/h3\u003e\n\u003cp data-sourcepos=\"109:1-109:747\"\u003eClaude Code の Agent Teams は、1つのセッションがリーダーとなり、複数の Claude Code インスタンスにタスクを分担させる実験的な機能です（\u003ccode\u003eCLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1\u003c/code\u003e で有効化します）。表示モードは2種類あります。in-process モードはどの端末でも利用できますが、各チームメイトを独立したペインに表示する split-pane モードには \u003cstrong\u003etmux または iTerm2\u003c/strong\u003e が必要です。公式ドキュメントの制限事項には、split-pane モードを \u003cstrong\u003eVS Code の統合ターミナル、Windows Terminal、Ghostty では利用できない\u003c/strong\u003eことが明記されています（\u003ca href=\"https://code.claude.com/docs/en/agent-teams\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAgent teams\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"111:1-111:274\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmintcdn.com%2Fclaude-code%2FnsvRFSDNfpSU5nT7%2Fimages%2Fsubagents-vs-agent-teams-light.png%3Ffit%3Dmax%26auto%3Dformat%26n%3DnsvRFSDNfpSU5nT7%26q%3D85%26s%3D2f8db9b4f3705dd3ab931fbe2d96e42a?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=58312324a1601b77bc26ada91c4b45fb\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmintcdn.com%2Fclaude-code%2FnsvRFSDNfpSU5nT7%2Fimages%2Fsubagents-vs-agent-teams-light.png%3Ffit%3Dmax%26auto%3Dformat%26n%3DnsvRFSDNfpSU5nT7%26q%3D85%26s%3D2f8db9b4f3705dd3ab931fbe2d96e42a?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=58312324a1601b77bc26ada91c4b45fb\" alt=\"Claude Code のサブエージェントと Agent Teams の違い（公式ドキュメントより）\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fmintcdn.com%2Fclaude-code%2FnsvRFSDNfpSU5nT7%2Fimages%2Fsubagents-vs-agent-teams-light.png%3Ffit%3Dmax%26auto%3Dformat%26n%3DnsvRFSDNfpSU5nT7%26q%3D85%26s%3D2f8db9b4f3705dd3ab931fbe2d96e42a?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=daf967b068466305117bce73367c06a7 1x\" data-canonical-src=\"https://mintcdn.com/claude-code/nsvRFSDNfpSU5nT7/images/subagents-vs-agent-teams-light.png?fit=max\u0026amp;auto=format\u0026amp;n=nsvRFSDNfpSU5nT7\u0026amp;q=85\u0026amp;s=2f8db9b4f3705dd3ab931fbe2d96e42a\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"113:1-113:256\"\u003e\u003cem\u003eサブエージェントは結果をリーダーに返すのに対し、Agent Teams ではタスクリストを共有し、チームメイト同士が直接やり取りします（出典: \u003ca href=\"https://code.claude.com/docs/en/agent-teams\" rel=\"nofollow noopener\" target=\"_blank\"\u003eClaude Code Docs\u003c/a\u003e）\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"115:1-115:72\"\u003e\u003ccode\u003eteammateMode\u003c/code\u003e の設定は \u003ccode\u003e~/.claude/settings.json\u003c/code\u003e に書きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"117:1-119:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"teammateMode\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"auto\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"121:1-121:552\"\u003e\u003ccode\u003eauto\u003c/code\u003e を指定すると、tmux 内で実行している場合、または iTerm2 に it2 CLI が導入されている場合にだけ分割ペインになります。それ以外の環境では、in-process モードに切り替わります。iTerm2 で分割ペインを使うには、it2 CLI を導入し、Settings → General → Magic で Python API を有効にします。公式ドキュメントが \u003ccode\u003etmux -CC\u003c/code\u003e（iTerm2 のネイティブ tmux 統合）を推奨していることも、iTerm2 ユーザーにとっては大きな利点です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"123:1-123:46\"\u003e\n\u003cspan id=\"25-vs-code-統合ターミナルの弱点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#25-vs-code-%E7%B5%B1%E5%90%88%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%81%AE%E5%BC%B1%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.5 VS Code 統合ターミナルの弱点\u003c/h3\u003e\n\u003cp data-sourcepos=\"125:1-125:537\"\u003eVS Code の統合ターミナルで Claude Code を使うと、\u003ccode\u003e/terminal-setup\u003c/code\u003e によって \u003ccode\u003eterminal.integrated.gpuAcceleration\u003c/code\u003e が \u003ccode\u003e\"off\"\u003c/code\u003e に書き換えられます。これは文字化けを防ぐためです。また、公式ドキュメントでは、大容量のテキストを貼り付けると一部の文字が失われることがあるため、ファイルを経由するワークフローが推奨されています。通知は標準では表示されず、Agent Teams の分割ペインにも対応していません。\u003c/p\u003e\n\u003cp data-sourcepos=\"127:1-127:451\"\u003eつまり、VS Code の統合ターミナルで Claude Code は動くものの、GPU 描画、通知、分割ペインには制約があります。VS Code を使い続けたい場合は、統合ターミナルで CLI を常用するよりも、\u003cstrong\u003eClaude Code の VS Code 拡張\u003c/strong\u003e（ネイティブパネル。\u003ca href=\"https://code.claude.com/docs/en/vs-code\" rel=\"nofollow noopener\" target=\"_blank\"\u003e公式ドキュメント\u003c/a\u003e）と外部ターミナルを使い分ける構成が適しています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"129:1-129:26\"\u003e\n\u003cspan id=\"26-codex-側の要件\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#26-codex-%E5%81%B4%E3%81%AE%E8%A6%81%E4%BB%B6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2.6 Codex 側の要件\u003c/h3\u003e\n\u003cp data-sourcepos=\"131:1-131:261\"\u003eCodex CLI では、通知やサンドボックスに関する設定を \u003ccode\u003e~/.codex/config.toml\u003c/code\u003e に記述します（\u003ca href=\"https://developers.openai.com/codex/config-advanced\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAdvanced Configuration\u003c/a\u003e）。TUI の通知は、\u003ccode\u003e[tui]\u003c/code\u003e セクションで指定します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"toml\" data-sourcepos=\"133:1-138:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# ~/.codex/config.toml\u003c/span\u003e\n\u003cspan class=\"k\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003etui\u003c/span\u003e\u003cspan class=\"k\"\u003e]\u003c/span\u003e\n\u003cspan class=\"n\"\u003enotifications\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\"agent-turn-complete\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\"approval-requested\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003cspan class=\"n\"\u003enotification_method\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\"osc9\"\u003c/span\u003e   \u003cspan class=\"c\"\u003e# auto / osc9 / bel\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"140:1-140:602\"\u003eOSC 9 は、デスクトップ通知に使われる制御シーケンスです。この場合も、端末が OSC 9 を OS の通知に変換できるかどうかが重要です。従来の \u003ccode\u003enotify = [...]\u003c/code\u003e は非推奨となり、Stop などの hooks へ移行するよう案内されています。サンドボックスには、macOS では Seatbelt、Linux では bubblewrap + seccomp が使われます。\u003ccode\u003esandbox_mode\u003c/code\u003e は \u003ccode\u003eread-only\u003c/code\u003e / \u003ccode\u003eworkspace-write\u003c/code\u003e / \u003ccode\u003edanger-full-access\u003c/code\u003e から選択します（\u003ca href=\"https://developers.openai.com/codex/agent-approvals-security\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAgent approvals \u0026amp; security\u003c/a\u003e）。\u003c/p\u003e\n\u003chr data-sourcepos=\"142:1-143:0\"\u003e\n\u003ch2 data-sourcepos=\"144:1-144:39\"\u003e\n\u003cspan id=\"3-各ツールを見ていきます\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E5%90%84%E3%83%84%E3%83%BC%E3%83%AB%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%84%E3%81%8D%E3%81%BE%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 各ツールを見ていきます\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"146:1-146:77\"\u003e\n\u003cspan id=\"31-iterm2-37--現在の環境を維持したまま強化できる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#31-iterm2-37--%E7%8F%BE%E5%9C%A8%E3%81%AE%E7%92%B0%E5%A2%83%E3%82%92%E7%B6%AD%E6%8C%81%E3%81%97%E3%81%9F%E3%81%BE%E3%81%BE%E5%BC%B7%E5%8C%96%E3%81%A7%E3%81%8D%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.1 iTerm2 3.7 —— 現在の環境を維持したまま強化できる\u003c/h3\u003e\n\u003cp data-sourcepos=\"148:1-148:218\"\u003eiTerm2 3.7 は2026年9月8日にリリースされました。主な新機能は、iOS 連携アプリの iTerm2 Buddy、Claude Code 統合、タブグループです（\u003ca href=\"https://iterm2.com/news.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eiTerm2 News\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"150:1-150:131\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Overview.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=fcd6a35f07ec422436b1f666510d6013\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Overview.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=fcd6a35f07ec422436b1f666510d6013\" alt=\"iTerm2 の Claude Code Integration セットアップ画面\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Overview.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=3025d17892993747234d7e102607f5ec 1x\" data-canonical-src=\"https://iterm2.com/images/claude-code/ClaudeCode-Setup-Overview.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"152:1-152:213\"\u003e\u003cem\u003e「iTerm2 \u0026gt; Install Claude Code Integration」で、変更内容を確認しながらセットアップできます（出典: \u003ca href=\"https://iterm2.com/claude-code-integration.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eiTerm2 公式ドキュメント\u003c/a\u003e）\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"154:1-154:385\"\u003e公式ドキュメントによると、この統合機能を導入することで、各 Claude セッションの状態（working / waiting / idle）が iTerm2 の Session Status ツールに表示されます。さらに、Claude との会話、diff ビューア、コードレビュー用セッションをワンクリックで切り替えられる Workgroup も用意されます。\u003c/p\u003e\n\u003cp data-sourcepos=\"156:1-156:156\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-SessionStatus.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b932550d698abeba472db79306d586e7\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-SessionStatus.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b932550d698abeba472db79306d586e7\" alt=\"iTerm2 の Session Status。各セッションの状態がドットで表示される\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-SessionStatus.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=f0b0aefe46e6a4e6f9c84eb9e617f573 1x\" data-canonical-src=\"https://iterm2.com/images/claude-code/ClaudeCode-SessionStatus.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"158:1-158:121\"\u003e\u003cem\u003eSession Status。注意が必要なセッションほど上に並びます（出典: iTerm2 公式ドキュメント）\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"160:1-160:252\"\u003e仕組みはシンプルです。\u003ccode\u003e~/.claude/settings.json\u003c/code\u003e に状態報告用の hook を追加し、iTerm2 の Python API を通じてツールベルトに反映します。アンインストールすれば元の状態に戻せる点も安心です。\u003c/p\u003e\n\u003cp data-sourcepos=\"162:1-162:116\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Workgroup.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2ec039a43543c6e3a6425d9c726c8d4f\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Workgroup.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2ec039a43543c6e3a6425d9c726c8d4f\" alt=\"Claude Code Workgroup のセットアップ\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fiterm2.com%2Fimages%2Fclaude-code%2FClaudeCode-Setup-Workgroup.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=e43c75bdb28a354d50f730a77bccf3bf 1x\" data-canonical-src=\"https://iterm2.com/images/claude-code/ClaudeCode-Setup-Workgroup.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"164:1-164:130\"\u003e\u003cem\u003eChat / Diff / Code Review の3ペインを1クリックで切り替える Workgroup（出典: iTerm2 公式ドキュメント）\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"166:1-166:488\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: Claude に「認証モジュールをリファクタリングして」と依頼した後、Workgroup の「Diff」を押すと、作業ツリーの差分が横に表示されます。「Code Review」を押せば、別セッションの Claude がレビューを開始します。レビューで得られた指摘は、Clippings を通じて元の Claude に返せます。これまで VS Code に切り替えて行っていた作業を、iTerm2 内で完結できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"168:1-168:592\"\u003e注意点は2つあります。1つ目は、macOS でしか利用できないことです。2つ目は、Agent Teams 用の iTerm2 バックエンド（it2 CLI）について、2026年2月の時点で「iTerm2 を検出できず、何も通知しないまま in-process モードに切り替わる」という不具合が報告されていたことです（\u003ca href=\"https://github.com/anthropics/claude-code/issues/24292\" rel=\"nofollow noopener\" target=\"_blank\"\u003e#24292\u003c/a\u003e、\u003ca href=\"https://github.com/anthropics/claude-code/issues/24301\" rel=\"nofollow noopener\" target=\"_blank\"\u003e#24301\u003c/a\u003e）。安定性を優先するなら、iTerm2 + \u003ccode\u003etmux -CC\u003c/code\u003e の組み合わせが手堅い選択です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"170:1-170:59\"\u003e\n\u003cspan id=\"32-tmux--実績のある公式バックエンド\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#32-tmux--%E5%AE%9F%E7%B8%BE%E3%81%AE%E3%81%82%E3%82%8B%E5%85%AC%E5%BC%8F%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.2 tmux —— 実績のある公式バックエンド\u003c/h3\u003e\n\u003cp data-sourcepos=\"172:1-172:329\"\u003etmux はサーバー・クライアント型の構成を採用しているため、端末を閉じてもセッションが維持されます。SSH 接続先の Linux サーバーでも安定して動作します。Agent Teams の分割ペインも、tmux の \u003ccode\u003esplit-window\u003c/code\u003e や \u003ccode\u003esend-keys\u003c/code\u003e を使って実装されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"174:1-174:561\"\u003e弱点は、エージェントの状態を意味のある情報として扱えないことです。たとえば、許可待ちで停止しているペインと、単に何も実行していないペインを区別できません。また、初期の Agent Teams では、4つ以上のエージェントを同時に起動するとレイアウトが崩れる、\u003ccode\u003esend-keys\u003c/code\u003e で送る文字が化けてチームメイトの起動に失敗する、といった問題も報告されています（\u003ca href=\"https://github.com/anthropics/claude-code/issues/23615\" rel=\"nofollow noopener\" target=\"_blank\"\u003e#23615\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"176:1-176:361\"\u003eそれでも tmux が重要であり続けるのは、Agent Teams の公式バックエンドであり、さまざまな OSS が下層として利用しているからです。herdr のように tmux そのものを置き換えるツールもありますが、tmux で得たセッション管理の知識は、他のツールに移行しても活かせます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"178:1-178:78\"\u003e\n\u003cspan id=\"33-cmux--mac-でどれが待っているかを一目で把握\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#33-cmux--mac-%E3%81%A7%E3%81%A9%E3%82%8C%E3%81%8C%E5%BE%85%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%81%8B%E3%82%92%E4%B8%80%E7%9B%AE%E3%81%A7%E6%8A%8A%E6%8F%A1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.3 cmux —— Mac で「どれが待っているか」を一目で把握\u003c/h3\u003e\n\u003cp data-sourcepos=\"180:1-180:41\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fcmux.com%2Fopengraph-image?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b0bc7a8be26b371383c9a74c244997a2\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fcmux.com%2Fopengraph-image?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=b0bc7a8be26b371383c9a74c244997a2\" alt=\"cmux\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fcmux.com%2Fopengraph-image?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=62e008b99e5a258ea442a51209d968ff 1x\" data-canonical-src=\"https://cmux.com/opengraph-image\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"182:1-182:69\"\u003e\u003cem\u003e出典: \u003ca href=\"https://cmux.com/agents/claude-code\" rel=\"nofollow noopener\" target=\"_blank\"\u003ecmux 公式サイト\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"184:1-184:636\"\u003ecmux は、Swift/AppKit で開発された macOS ネイティブのターミナルです。描画エンジンに libghostty を使用し、テーマやフォントなど、既存の Ghostty 設定をそのまま読み込めます。開発の動機は非常に具体的です。Claude Code の通知は毎回「Claude is waiting for your input」と表示され、どの作業に関する通知なのかが分かりません。さらに、タブが増えるとタイトルも読めなくなります。cmux は、こうした問題を解決するために開発されました（\u003ca href=\"https://github.com/manaflow-ai/cmux\" rel=\"nofollow noopener\" target=\"_blank\"\u003eGitHub README\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"186:1-186:30\"\u003e主な機能を挙げます。\u003c/p\u003e\n\u003cul data-sourcepos=\"188:1-192:0\"\u003e\n\u003cli data-sourcepos=\"188:1-188:206\"\u003e\n\u003cstrong\u003e縦置きのサイドバー\u003c/strong\u003e: ワークスペースごとに、git ブランチ、関連する PR の番号と状態、作業ディレクトリ、LISTEN 中のポート、最新の通知内容を表示\u003c/li\u003e\n\u003cli data-sourcepos=\"189:1-189:213\"\u003e\n\u003cstrong\u003e通知リング\u003c/strong\u003e: OSC 9/99/777 を検出し、エージェントが待機状態になるとペインを青い枠で強調。サイドバーのタブも点灯し、Cmd+Shift+U で最新の未読通知へ移動\u003c/li\u003e\n\u003cli data-sourcepos=\"190:1-190:192\"\u003e\n\u003cstrong\u003e内蔵ブラウザ\u003c/strong\u003e: agent-browser 由来のスクリプト API を持ち、アクセシビリティツリーの取得、要素のクリック、フォーム入力、JS 評価ができる\u003c/li\u003e\n\u003cli data-sourcepos=\"191:1-192:0\"\u003e\n\u003cstrong\u003eCLI と Unix socket\u003c/strong\u003e: ワークスペースの作成、ペインの分割、キー入力の送信、URL のオープンまでスクリプトで操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"193:1-193:382\"\u003eさらに、Claude Code の teammate モードをワンコマンドで起動できます。チームメイトは cmux のネイティブな分割ペインとして作成されるため、tmux は必要ありません。ベータ版の iOS コンパニオンアプリも提供されています。ライセンスは GPL で、無料のオープンソースソフトウェアです。\u003c/p\u003e\n\u003cp data-sourcepos=\"195:1-195:465\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: 「API 実装」「テスト追加」「ドキュメント更新」の3つのワークスペースで Claude Code を動かし、その間に自分は別の作業を進めます。どれかが許可待ちになると、対象のペインだけが青く強調され、サイドバーには「Bash: npm test を実行しますか？」といった最新の内容が表示されます。そのワークスペースだけを確認すれば済みます。\u003c/p\u003e\n\u003cp data-sourcepos=\"197:1-197:347\"\u003ecmux 自身は「primitive であって solution ではない」と位置づけられており、worktree の管理や PR のワークフローはユーザー自身が構築する前提です。macOS でしか利用できず、プロジェクト自体もまだ若く、更新のペースが速いことは理解しておく必要があります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"199:1-199:75\"\u003e\n\u003cspan id=\"34-herdr--端末を閉じても稼働し続けるランタイム\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#34-herdr--%E7%AB%AF%E6%9C%AB%E3%82%92%E9%96%89%E3%81%98%E3%81%A6%E3%82%82%E7%A8%BC%E5%83%8D%E3%81%97%E7%B6%9A%E3%81%91%E3%82%8B%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.4 herdr —— 端末を閉じても稼働し続けるランタイム\u003c/h3\u003e\n\u003cp data-sourcepos=\"201:1-201:49\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fherdr.dev%2Fassets%2Fog-card-v9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=41c816575093b793d1b1f1a7522076d8\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fherdr.dev%2Fassets%2Fog-card-v9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=41c816575093b793d1b1f1a7522076d8\" alt=\"herdr\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fherdr.dev%2Fassets%2Fog-card-v9.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=71d8f2915df7da76208af13afde4a827 1x\" data-canonical-src=\"https://herdr.dev/assets/og-card-v9.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"203:1-203:53\"\u003e\u003cem\u003e出典: \u003ca href=\"https://herdr.dev/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eherdr 公式サイト\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"205:1-205:476\"\u003eherdr は Rust 製の単一バイナリで、iTerm2 や Ghostty など、既存の端末内で動作します。最大の特徴は、常駐サーバーがセッションを保持する点です。ウィンドウを閉じたり、ノート PC の蓋を閉じたり、ネットワークが切断されたりしても、エージェントは稼働し続けます。さらに、マシンを再起動した後はレイアウトを復元し、セッションを再開できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"207:1-207:427\"\u003eもう1つの特徴は、エージェントの状態検知です。herdr は各ペインの内容を読み取り、エージェントを working / blocked / idle / done に分類します。いずれかのエージェントが停止し、応答を待っている場合は、その状態を通知します。Claude Code、Codex、OpenCode、Cursor、Grok など、22種類のエージェント CLI を自動検出できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"209:1-209:458\"\u003eエージェント自身が herdr を操作できる点も重要です。CLI と socket API は同じ機能を提供しており、エージェントはペインの分割、別のエージェントの起動、プロンプトの送信を行えます。さらに、相手が実際に blocked 状態になるまで待機できます。単にキー入力を送って結果を期待するのではなく、相手の状態を確認しながら待つ設計です。\u003c/p\u003e\n\u003cp data-sourcepos=\"211:1-211:242\"\u003eさらに、手元のノート PC、自宅のデスクトップ、リモートサーバーを SSH で追加すると、それぞれのワークスペースとエージェントを、ローカル環境のものと並べて表示できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"213:1-213:308\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: 金曜日の夕方に、Claude Code へ「すべてのテストが通るまで再試行して」と依頼し、herdr のペインで動かしたままノート PC を閉じます。月曜日に開くとペインは「done」と表示され、途中の出力もそのまま残っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"215:1-215:429\"\u003e注意点として、herdr は v0.9 系で、まだ 1.0 に達していません（Apache-2.0、GitHub スター約4万）。ターミナルエミュレータではないため、描画品質は外側の端末に依存します。また、herdr のペイン内で tmux を入れ子にすると、状態検知が機能しなくなります。tmux と併用するのではなく、tmux の代替として使うのが適切です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"217:1-217:59\"\u003e\n\u003cspan id=\"35-warp--端末ごと開発環境にする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#35-warp--%E7%AB%AF%E6%9C%AB%E3%81%94%E3%81%A8%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.5 Warp —— 端末ごと「開発環境」にする\u003c/h3\u003e\n\u003cp data-sourcepos=\"219:1-219:76\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.warp.dev%2Fimg%2Fproducts%2Fhomepage-v2-terminal.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9cf408b202feda30dc07c6a3f6280021\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.warp.dev%2Fimg%2Fproducts%2Fhomepage-v2-terminal.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=9cf408b202feda30dc07c6a3f6280021\" alt=\"Warp Terminal\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.warp.dev%2Fimg%2Fproducts%2Fhomepage-v2-terminal.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=3c434ac0aa9cc194f52596d558e79338 1x\" data-canonical-src=\"https://www.warp.dev/img/products/homepage-v2-terminal.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"221:1-221:55\"\u003e\u003cem\u003e出典: \u003ca href=\"https://www.warp.dev/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eWarp 公式サイト\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"223:1-223:370\"\u003eWarp は Rust 製のクロスプラットフォーム対応ターミナルで、2026年4月にクライアントがオープンソース化されました。UI フレームワークは MIT、それ以外は AGPLv3 でライセンスされており、OpenAI がリポジトリの founding sponsor になっています（\u003ca href=\"https://github.com/warpdotdev/warp\" rel=\"nofollow noopener\" target=\"_blank\"\u003eGitHub\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"225:1-225:487\"\u003eWarp は「Agentic Development Environment」を名乗っています。ブロック単位の出力表示、複数のモデルに対応する内蔵エージェント、Claude Code や Codex などの外部 CLI エージェントの取り込みに対応しています。さらに Factory MCP を通じて、triage → spec → implement → review → verify の工程を複数のエージェントで進めるクラウドサービス「Warp Factories」に作業を依頼できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"227:1-227:586\"\u003e興味深いのは、Warp 自身が2026年9月の記事（\u003ca href=\"https://www.warp.dev/articles/best-terminal-for-ai-coding-agents\" rel=\"nofollow noopener\" target=\"_blank\"\u003eBest Terminal for AI Coding Agents in 2026\u003c/a\u003e）で、「ブランドではなく層で選ぶ」と整理していることです。Ghostty や iTerm2 はエミュレータ、herdr はランタイム、Warp は ADE と、それぞれ異なる層に位置づけています。そのうえで、多くの開発者は2つの層を組み合わせて使うことになると述べています。本記事の5層モデルは、この整理を拡張したものです。\u003c/p\u003e\n\u003cp data-sourcepos=\"229:1-229:535\"\u003e弱点は、ここで取り上げるツールの中でも動作が比較的重く、利用にアカウントが必要なことです。また、前述のとおり、Claude Code のデスクトップ通知は標準設定では届かないため、hook の設定が必要です。個人が Claude Code と Codex をローカルで使うだけなら、機能が過剰と感じられるかもしれません。組織で作業をクラウドに移し、その実行状況を計測・管理したい場合に、特に力を発揮します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"231:1-231:70\"\u003e\n\u003cspan id=\"36-zed--エディタ側からエージェントを並列に\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#36-zed--%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E5%81%B4%E3%81%8B%E3%82%89%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E3%82%92%E4%B8%A6%E5%88%97%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.6 Zed —— エディタ側からエージェントを並列に\u003c/h3\u003e\n\u003cp data-sourcepos=\"233:1-233:78\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fzed.dev%2Fimg%2Fpost%2Fclaude-code-zed%2Fthumbnail.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=6e1f78dcf5bde76e59a53708caf375c5\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fzed.dev%2Fimg%2Fpost%2Fclaude-code-zed%2Fthumbnail.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=6e1f78dcf5bde76e59a53708caf375c5\" alt=\"Claude Code in Zed\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fzed.dev%2Fimg%2Fpost%2Fclaude-code-zed%2Fthumbnail.webp?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=afc612567d5d00462b2d6939ca2862d2 1x\" data-canonical-src=\"https://zed.dev/img/post/claude-code-zed/thumbnail.webp\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"235:1-235:73\"\u003e\u003cem\u003e出典: \u003ca href=\"https://zed.dev/blog/claude-code-via-acp\" rel=\"nofollow noopener\" target=\"_blank\"\u003eZed 公式ブログ\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"237:1-237:506\"\u003eZed は Rust 製の高速なエディタです。連携の中心となるのは、\u003cstrong\u003eACP（Agent Client Protocol）\u003c/strong\u003e です。ACP は JSON-RPC ベースのオープン標準で、エディタがエージェントを別プロセスとして起動し、ファイル操作や権限要求をエディタ側の UI で扱えるようにします（\u003ca href=\"https://zed.dev/acp\" rel=\"nofollow noopener\" target=\"_blank\"\u003ezed.dev/acp\u003c/a\u003e）。Zed のドキュメントでは、Gemini CLI、Claude Code、Codex CLI、OpenCode との連携が紹介されています。\u003c/p\u003e\n\u003cp data-sourcepos=\"239:1-239:449\"\u003e2026年4月29日にリリースされた Zed 1.0 の主要機能は、エージェントの並列実行です。1つのウィンドウ内で複数のエージェントを同時に動かし、コードベースの異なる部分を分担させられます。各エージェントによる編集はエディタ上に diff として反映され、その場でレビューできます。これは、ターミナル単体では得にくい強みです。\u003c/p\u003e\n\u003cp data-sourcepos=\"241:1-241:578\"\u003eただし、制約もあります。Zed の外部エージェント連携は ACP アダプタを経由するため、Claude Code の一部のスラッシュコマンドは、Zed のエージェントウィンドウ内で動作しないと指摘されています。Zed の公式ドキュメント（\u003ca href=\"https://zed.dev/docs/ai/external-agents\" rel=\"nofollow noopener\" target=\"_blank\"\u003eExternal Agents\u003c/a\u003e）によると、Claude Agent の hooks と Agent Teams は現時点で対応していません。Claude Code の hooks や Agent Teams を積極的に活用している場合は、導入を見送るのが妥当です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"243:1-243:77\"\u003e\n\u003cspan id=\"37-orca--oss-でworktree端末ブラウザを一体化\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#37-orca--oss-%E3%81%A7worktree%E7%AB%AF%E6%9C%AB%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%82%92%E4%B8%80%E4%BD%93%E5%8C%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.7 Orca —— OSS で「worktree＋端末＋ブラウザ」を一体化\u003c/h3\u003e\n\u003cp data-sourcepos=\"245:1-245:164\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.onorca.dev%2Fwhats-new%2Fposters%2Forca-split-screen.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=7d12f281152cb8f7e44b95b576a878eb\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.onorca.dev%2Fwhats-new%2Fposters%2Forca-split-screen.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=7d12f281152cb8f7e44b95b576a878eb\" alt=\"Orca のメイン画面。worktree のサイドバー、エージェント端末、diff ビュー\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.onorca.dev%2Fwhats-new%2Fposters%2Forca-split-screen.jpg?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=5e1ebdd84bc66030f0fe82f79944758d 1x\" data-canonical-src=\"https://www.onorca.dev/whats-new/posters/orca-split-screen.jpg\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"247:1-247:70\"\u003e\u003cem\u003e出典: \u003ca href=\"https://www.onorca.dev/docs\" rel=\"nofollow noopener\" target=\"_blank\"\u003eOrca 公式ドキュメント\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"249:1-249:340\"\u003eOrca は、複数の AI コーディングエージェントを並列で動かすためのデスクトップアプリです。タスクごとに git worktree、エージェント用ターミナル、ブラウザタブを用意します。自らを IDE ではなく、\u003cstrong\u003eADE（Agent Development Environment）\u003c/strong\u003e と位置づけています。\u003c/p\u003e\n\u003cp data-sourcepos=\"251:1-251:675\"\u003e対応エージェントは幅広く、Claude Code、Codex、Gemini、Cursor CLI、GitHub Copilot、OpenCode、Amp、Goose、Cline、Kiro、Qwen Code などが事前設定済みで、任意の CLI エージェントも追加できます。レビュー面も充実していて、diff の特定行に Markdown コメントを付けてまとめてエージェントに返せるほか、CI の確認、コンフリクト解消、PR 作成もアプリ内で完結します。Design Mode では、worktree ごとの Chromium ウィンドウで UI 要素をクリックすると、その HTML・CSS・切り抜きスクリーンショットがエージェントに送られます。\u003c/p\u003e\n\u003cp data-sourcepos=\"253:1-253:349\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: 「このボタンの余白がおかしい」とチャットで詳細に説明する代わりに、Design Mode でそのボタンをクリックします。すると、対象要素の情報がそのまま Claude Code に渡されます。フロントエンドにおける、言葉だけでは伝えにくい修正に効果的です。\u003c/p\u003e\n\u003cp data-sourcepos=\"255:1-255:417\"\u003eMIT ライセンスのもとで無料で利用でき、macOS、Windows、Linux に対応しています。SSH 接続先のマシン上でエージェントを動かすことも可能です。一方、Chromium と Monaco エディタを内包する比較的重いアプリです。機能追加のペースも非常に速く、UI や API が変更されやすいことは、導入前に理解しておきましょう。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"257:1-257:59\"\u003e\n\u003cspan id=\"38-conductor--issue-から-pr-までを最短で\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#38-conductor--issue-%E3%81%8B%E3%82%89-pr-%E3%81%BE%E3%81%A7%E3%82%92%E6%9C%80%E7%9F%AD%E3%81%A7\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.8 Conductor —— Issue から PR までを最短で\u003c/h3\u003e\n\u003cp data-sourcepos=\"259:1-259:137\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.conductor.build%2Fdocs-assets%2Fimages%2Fgit-worktrees-claude-code-workspace.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0b2dc10a8d15f899836cfc0ce9c87be3\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.conductor.build%2Fdocs-assets%2Fimages%2Fgit-worktrees-claude-code-workspace.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=0b2dc10a8d15f899836cfc0ce9c87be3\" alt=\"Conductor のワークスペース作成画面\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fwww.conductor.build%2Fdocs-assets%2Fimages%2Fgit-worktrees-claude-code-workspace.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=46de74467b1e3648c9642f46882a6950 1x\" data-canonical-src=\"https://www.conductor.build/docs-assets/images/git-worktrees-claude-code-workspace.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"261:1-261:137\"\u003e\u003cem\u003e出典: \u003ca href=\"https://www.conductor.build/docs/guides/parallel-agents/run-multiple-claude-code-sessions\" rel=\"nofollow noopener\" target=\"_blank\"\u003eConductor 公式ドキュメント\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"263:1-263:435\"\u003eConductor は macOS 向けアプリで、Claude Code、Codex、Cursor、OpenCode を並列で動かせます。タスクごとに、独立したワークスペース、ブランチ、ファイル、ターミナル、diff、レビューの経路が用意されます。作業完了後は、diff の確認、PR の作成、マージ、ワークスペースのアーカイブまで、一連の操作をアプリ内で進められます。\u003c/p\u003e\n\u003cp data-sourcepos=\"265:1-265:379\"\u003e実務で効くのは細かい配慮です。\u003ccode\u003e.env.local\u003c/code\u003e のような gitignore 済みファイルは「Files to copy」で新しいワークスペースに複製でき、依存のインストールは setup スクリプトに書け、複数の dev サーバーを同時に動かすときは \u003ccode\u003eCONDUCTOR_PORT\u003c/code\u003e でワークスペースごとにポート帯を分けられます。\u003c/p\u003e\n\u003cp data-sourcepos=\"267:1-267:360\"\u003e料金は Free が $0 でローカルのワークスペース、Pro が月 $50 で Conductor Cloud（クラウドワークスペース）や Multiplayer、API が付きます（\u003ca href=\"https://www.conductor.build/pricing\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePricing\u003c/a\u003e）。クラウドワークスペースは Vercel のサンドボックス上で動き、アプリを閉じても作業が続きます。\u003c/p\u003e\n\u003cp data-sourcepos=\"269:1-269:361\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: GitHub の Issue を3つ選び、それぞれのワークスペースを作成します。3つの Claude Code が異なる worktree で並列に作業し、完了したものから diff を確認して PR を作成します。\u003ccode\u003egit worktree add\u003c/code\u003e の実行や \u003ccode\u003e.env\u003c/code\u003e のコピーなど、従来は手作業で行っていた準備を省けます。\u003c/p\u003e\n\u003cp data-sourcepos=\"271:1-271:347\"\u003e注意点として、公式ドキュメント自身が「ワークスペースの分離は開発上の分離であって、セキュリティ境界ではない」と明記しています。並列セッションは同じアカウントの利用枠を消費しますし、クラウドではチャット内容が Conductor 側に保存されます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"273:1-273:83\"\u003e\n\u003cspan id=\"39-traycer--仕様を先に固めるためのワークスペース\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#39-traycer--%E4%BB%95%E6%A7%98%E3%82%92%E5%85%88%E3%81%AB%E5%9B%BA%E3%82%81%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.9 Traycer —— 「仕様を先に固める」ためのワークスペース\u003c/h3\u003e\n\u003cp data-sourcepos=\"275:1-275:126\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fframerusercontent.com%2Fimages%2FmKDnrG6VDOneveSftitfSiYZY.png%3Fwidth%3D1536%26height%3D1024?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=74a75a30cc7a21638b700d45defae8b3\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fframerusercontent.com%2Fimages%2FmKDnrG6VDOneveSftitfSiYZY.png%3Fwidth%3D1536%26height%3D1024?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=74a75a30cc7a21638b700d45defae8b3\" alt=\"Traycer のワークスペース\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fframerusercontent.com%2Fimages%2FmKDnrG6VDOneveSftitfSiYZY.png%3Fwidth%3D1536%26height%3D1024?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=d7ac2d1d9348e30567401e41d1e22451 1x\" data-canonical-src=\"https://framerusercontent.com/images/mKDnrG6VDOneveSftitfSiYZY.png?width=1536\u0026amp;height=1024\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"277:1-277:56\"\u003e\u003cem\u003e出典: \u003ca href=\"https://traycer.ai/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eTraycer 公式サイト\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"279:1-279:462\"\u003eTraycer は、他のツールとは異なる層に位置します。「どのように実行するか」ではなく、「何を作るか」を事前に定めるためのワークスペースです。作業を Task 単位でまとめ、その中にエージェント（Chat または Terminal インターフェース）、ファイル、git diff、spec、ticket、review などの \u003cstrong\u003eartifact\u003c/strong\u003e を配置します（\u003ca href=\"https://docs.traycer.ai/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eTraycer Docs\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"281:1-281:320\"\u003e自分が契約しているサブスクリプションを持ち込む BYOA 方式で、Claude Code、Codex、OpenCode、Cursor を1つのワークスペース上で並べて使用できます。エージェント同士で質問を送り合い、レビューを依頼したり、作業を引き継いだりできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"283:1-283:449\"\u003e\u003cstrong\u003e具体例\u003c/strong\u003e: 「決済プロバイダを Stripe から Adyen に切り替える」という大きな変更を、まず Traycer で PRD、技術計画、チケット群に分解します。チケットごとに Claude Code へ handoff し、エージェントは仕様を読みますが書き換えません。実装後は仕様と照らして検証します。「途中で何を作っていたか分からなくなる」問題への対策です。\u003c/p\u003e\n\u003cp data-sourcepos=\"285:1-285:384\"\u003e料金は、BYOA が $0、クラウド同期を利用できる Sync が月額 $10、クレジット付きの Lite が $20、Pro が $40、Ultra が $100 です（2026年9月時点、\u003ca href=\"https://traycer.ai/pricing\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePricing\u003c/a\u003e）。小さなタスクに対しては準備の負担が大きいため、比較的規模の大きな機能をチームで分担する場合に適しています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"287:1-287:65\"\u003e\n\u003cspan id=\"310-opencode--第3のエージェントとして\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#310-opencode--%E7%AC%AC3%E3%81%AE%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E3%81%A8%E3%81%97%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.10 OpenCode —— 「第3のエージェント」として\u003c/h3\u003e\n\u003cp data-sourcepos=\"289:1-289:49\"\u003e\u003ca href=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fopencode.ai%2Fsocial-share.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2dcc99afca0c52862a937e812a8ed558\" target=\"_blank\" rel=\"nofollow noopener\"\u003e\u003cimg src=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fopencode.ai%2Fsocial-share.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;s=2dcc99afca0c52862a937e812a8ed558\" alt=\"OpenCode\" srcset=\"https://qiita-user-contents.imgix.net/https%3A%2F%2Fopencode.ai%2Fsocial-share.png?ixlib=rb-4.1.1\u0026amp;auto=format\u0026amp;gif-q=60\u0026amp;q=75\u0026amp;w=1400\u0026amp;fit=max\u0026amp;s=0454f5b6d4239006660dca3332ae7aa9 1x\" data-canonical-src=\"https://opencode.ai/social-share.png\" loading=\"lazy\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"291:1-291:58\"\u003e\u003cem\u003e出典: \u003ca href=\"https://opencode.ai/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eOpenCode 公式サイト\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"293:1-293:474\"\u003eOpenCode はターミナルではなく、Claude Code や Codex と同じ層に属する \u003cstrong\u003eエージェント\u003c/strong\u003eです。MIT ライセンスのオープンソースソフトウェアで、ターミナル、IDE、デスクトップアプリから利用できます。Claude、GPT、Gemini など、さまざまなプロバイダーのモデルに接続できます。GitHub スターは 208K を超えており、この分野で最大級のコミュニティを持ちます。\u003c/p\u003e\n\u003cp data-sourcepos=\"295:1-295:510\"\u003eターミナル選びの文脈で重要なのは client/server 構成です。\u003ccode\u003eopencode serve\u003c/code\u003e でヘッドレスの HTTP サーバーを立て、TUI はそのクライアントとして動くため、複数のクライアントから同じサーバーに接続したり、プログラムから操作したりできます（\u003ca href=\"https://opencode.ai/docs/server\" rel=\"nofollow noopener\" target=\"_blank\"\u003eServer\u003c/a\u003e）。サーバーをリモートに置いて、手元の端末から \u003ccode\u003eopencode attach\u003c/code\u003e で繋ぐ、という使い方が自然にできます。\u003c/p\u003e\n\u003cp data-sourcepos=\"297:1-297:283\"\u003eClaude Code と Codex を主に使う人にとっての位置づけは、「ローカルモデルや別プロバイダを使う保険」です。cmux、herdr、Orca、Conductor はいずれも OpenCode に対応しているので、どの層を選んでも一緒に動かせます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"299:1-299:48\"\u003e\n\u003cspan id=\"311-あわせて知っておきたいもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#311-%E3%81%82%E3%82%8F%E3%81%9B%E3%81%A6%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3.11 あわせて知っておきたいもの\u003c/h3\u003e\n\u003cul data-sourcepos=\"301:1-304:0\"\u003e\n\u003cli data-sourcepos=\"301:1-301:309\"\u003e\n\u003cstrong\u003eGhostty\u003c/strong\u003e: Shift+Enter もデスクトップ通知も設定不要で動く、最速級の端末です。ただし AI 機能は意図的に持たず、Agent Teams の分割ペインは非対応です（中で tmux を使えば回避できます）。cmux と Orca の描画エンジンでもあります。\u003c/li\u003e\n\u003cli data-sourcepos=\"302:1-302:302\"\u003e\n\u003cstrong\u003eClaude Code Desktop\u003c/strong\u003e: 2026年4月の刷新で、1つのウィンドウで複数の Claude セッションを並べ、サイドバーで管理し、ペインをドラッグで並べ替えられるようになりました。ターミナルを使わずに並列運用したい人の選択肢です。\u003c/li\u003e\n\u003cli data-sourcepos=\"303:1-304:0\"\u003e\n\u003cstrong\u003eCodex アプリ\u003c/strong\u003e: スレッドごとに worktree を持ち、ローカル環境とのハンドオフやスケジュール実行に対応しています。2026年7月に ChatGPT デスクトップアプリへ統合されたと報じられており、CLI と IDE 拡張も引き続き提供されています。Codex を中心に使う人にとっては、有力な選択肢です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"305:1-306:0\"\u003e\n\u003ch2 data-sourcepos=\"307:1-307:27\"\u003e\n\u003cspan id=\"4-ツール別の比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%83%84%E3%83%BC%E3%83%AB%E5%88%A5%E3%81%AE%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. ツール別の比較\u003c/h2\u003e\n\u003cp data-sourcepos=\"309:1-309:99\"\u003e各ツールの対応環境、実行基盤、連携・レビュー機能を順に整理します。\u003c/p\u003e\n\u003cul data-sourcepos=\"311:1-355:0\"\u003e\n\u003cli data-sourcepos=\"311:1-314:195\"\u003e\n\u003cstrong\u003eiTerm2 3.7\u003c/strong\u003e\n\u003cul data-sourcepos=\"312:3-314:195\"\u003e\n\u003cli data-sourcepos=\"312:3-312:52\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS。無料（GPL）。\u003c/li\u003e\n\u003cli data-sourcepos=\"313:3-313:197\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：Session Status による状態表示に優れています。永続化には \u003ccode\u003etmux -CC\u003c/code\u003e の併用が必要で、worktree の分離にも別途対応が必要です。\u003c/li\u003e\n\u003cli data-sourcepos=\"314:3-314:195\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインには it2 または \u003ccode\u003etmux -CC\u003c/code\u003e で対応します。Python API と it2 から操作でき、Workgroup でレビューできます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"315:1-318:192\"\u003e\n\u003cstrong\u003etmux\u003c/strong\u003e\n\u003cul data-sourcepos=\"316:3-318:192\"\u003e\n\u003cli data-sourcepos=\"316:3-316:60\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux。無料（ISC）。\u003c/li\u003e\n\u003cli data-sourcepos=\"317:3-317:207\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：エージェントの状態表示はありません。SSH 先でもセッションを永続化でき、\u003ccode\u003eclaude --worktree\u003c/code\u003e を併用すれば worktree を分離できます。\u003c/li\u003e\n\u003cli data-sourcepos=\"318:3-318:192\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインに公式対応しています。\u003ccode\u003esend-keys\u003c/code\u003e と \u003ccode\u003ecapture-pane\u003c/code\u003e で操作できますが、レビュー機能はありません。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"319:1-322:209\"\u003e\n\u003cstrong\u003ecmux\u003c/strong\u003e\n\u003cul data-sourcepos=\"320:3-322:209\"\u003e\n\u003cli data-sourcepos=\"320:3-320:52\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS。無料（GPL）。\u003c/li\u003e\n\u003cli data-sourcepos=\"321:3-321:165\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：通知リングとサイドバーで状態を把握できます。永続化や worktree の分離には別途対応が必要です。\u003c/li\u003e\n\u003cli data-sourcepos=\"322:3-322:209\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams をネイティブな分割ペインで表示できます。CLI、socket、ブラウザから操作でき、レビュー機能は条件付きで利用できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"323:1-326:173\"\u003e\n\u003cstrong\u003eherdr\u003c/strong\u003e\n\u003cul data-sourcepos=\"324:3-326:173\"\u003e\n\u003cli data-sourcepos=\"324:3-324:95\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux、Windows（ベータ版）。無料（Apache-2.0）。\u003c/li\u003e\n\u003cli data-sourcepos=\"325:3-325:219\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：4段階の状態表示に対応しています。常駐サーバーで実行を維持でき、複数マシンを横断できます。worktree の分離には別途対応が必要です。\u003c/li\u003e\n\u003cli data-sourcepos=\"326:3-326:173\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams はプラグインで対応します。\u003ccode\u003ewait-until-blocked\u003c/code\u003e から操作できますが、レビュー機能はありません。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"327:1-330:162\"\u003e\n\u003cstrong\u003eWarp\u003c/strong\u003e\n\u003cul data-sourcepos=\"328:3-330:162\"\u003e\n\u003cli data-sourcepos=\"328:3-328:92\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux、Windows。無料プランあり（MIT + AGPL）。\u003c/li\u003e\n\u003cli data-sourcepos=\"329:3-329:182\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：状態表示に対応しています。クラウド機能の Factory で実行を維持でき、worktree の分離には別途対応が必要です。\u003c/li\u003e\n\u003cli data-sourcepos=\"330:3-330:162\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインには対応していません。MCP から操作でき、レビュー機能も利用できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"331:1-334:189\"\u003e\n\u003cstrong\u003eZed\u003c/strong\u003e\n\u003cul data-sourcepos=\"332:3-334:189\"\u003e\n\u003cli data-sourcepos=\"332:3-332:70\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux、Windows。無料（OSS）。\u003c/li\u003e\n\u003cli data-sourcepos=\"333:3-333:206\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：スレッド単位で状態を確認できます。実行の永続化やリモート実行には対応しておらず、worktree の分離には別途対応が必要です。\u003c/li\u003e\n\u003cli data-sourcepos=\"334:3-334:189\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインには対応していません。ACP から条件付きで操作でき、エディタ上の diff でレビューできます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"335:1-338:183\"\u003e\n\u003cstrong\u003eOrca\u003c/strong\u003e\n\u003cul data-sourcepos=\"336:3-338:183\"\u003e\n\u003cli data-sourcepos=\"336:3-336:70\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Windows、Linux。無料（MIT）。\u003c/li\u003e\n\u003cli data-sourcepos=\"337:3-337:141\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：Inbox で状態を把握できます。SSH と自己ホストに対応し、worktree を分離できます。\u003c/li\u003e\n\u003cli data-sourcepos=\"338:3-338:183\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインには対応していません。Orca CLI から操作でき、行コメントを付けて PR へつなげられます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"339:1-342:188\"\u003e\n\u003cstrong\u003eConductor\u003c/strong\u003e\n\u003cul data-sourcepos=\"340:3-342:188\"\u003e\n\u003cli data-sourcepos=\"340:3-340:97\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS。Free プランと Pro プラン（50ドル）があります。\u003c/li\u003e\n\u003cli data-sourcepos=\"341:3-341:139\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：状態表示に対応しています。Pro では Cloud を利用でき、worktree も分離できます。\u003c/li\u003e\n\u003cli data-sourcepos=\"342:3-342:188\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams は環境変数を設定すれば利用できます。Pro の API から操作でき、diff の確認から PR 作成まで進められます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"343:1-346:151\"\u003e\n\u003cstrong\u003eTraycer\u003c/strong\u003e\n\u003cul data-sourcepos=\"344:3-346:151\"\u003e\n\u003cli data-sourcepos=\"344:3-344:97\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：デスクトップ。BYOA 方式で、無料プランがあります。\u003c/li\u003e\n\u003cli data-sourcepos=\"345:3-345:99\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：状態表示、Sync、worktree の分離に対応しています。\u003c/li\u003e\n\u003cli data-sourcepos=\"346:3-346:151\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams と操作用 API には対応していません。artifact を使ったレビューに優れています。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"347:1-350:157\"\u003e\n\u003cstrong\u003eOpenCode\u003c/strong\u003e\n\u003cul data-sourcepos=\"348:3-350:157\"\u003e\n\u003cli data-sourcepos=\"348:3-348:66\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux、WSL。無料（MIT）。\u003c/li\u003e\n\u003cli data-sourcepos=\"349:3-349:174\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：\u003ccode\u003eserve\u003c/code\u003e と \u003ccode\u003eattach\u003c/code\u003e によって実行を分離できます。状態表示と worktree 分離は、この比較では評価対象外です。\u003c/li\u003e\n\u003cli data-sourcepos=\"350:3-350:157\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：HTTP API と ACP から操作できます。Agent Teams とレビュー機能は、この比較では評価対象外です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"351:1-355:0\"\u003e\n\u003cstrong\u003eVS Code 統合端末\u003c/strong\u003e\n\u003cul data-sourcepos=\"352:3-355:0\"\u003e\n\u003cli data-sourcepos=\"352:3-352:61\"\u003e\n\u003cstrong\u003e環境・価格\u003c/strong\u003e：macOS、Linux、Windows。無料。\u003c/li\u003e\n\u003cli data-sourcepos=\"353:3-353:140\"\u003e\n\u003cstrong\u003e状態・実行基盤\u003c/strong\u003e：状態表示、実行の永続化、リモート実行、worktree の分離には対応していません。\u003c/li\u003e\n\u003cli data-sourcepos=\"354:3-355:0\"\u003e\n\u003cstrong\u003e連携・レビュー\u003c/strong\u003e：Agent Teams の分割ペインには対応していません。外部からの操作は限定的ですが、拡張機能の diff でレビューできます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"356:1-357:0\"\u003e\n\u003ch2 data-sourcepos=\"358:1-358:42\"\u003e\n\u003cspan id=\"5-ユースケース別のおすすめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E3%83%A6%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%82%B9%E5%88%A5%E3%81%AE%E3%81%8A%E3%81%99%E3%81%99%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. ユースケース別のおすすめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"360:1-404:0\"\u003e\n\u003cli data-sourcepos=\"360:1-363:112\"\u003e\n\u003cstrong\u003e1〜2つのエージェントを使う個人開発\u003c/strong\u003e\n\u003cul data-sourcepos=\"361:3-363:112\"\u003e\n\u003cli data-sourcepos=\"361:3-361:57\"\u003e第一候補：iTerm2 3.7 ＋ Claude Code Integration\u003c/li\u003e\n\u003cli data-sourcepos=\"362:3-362:20\"\u003e代替：Ghostty\u003c/li\u003e\n\u003cli data-sourcepos=\"363:3-363:112\"\u003e理由：別の端末へ乗り換えずに、状態表示、Diff/Review、iOS 通知を利用できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"364:1-367:72\"\u003e\n\u003cstrong\u003eAgent Teams を分割ペインで見たい\u003c/strong\u003e\n\u003cul data-sourcepos=\"365:3-367:72\"\u003e\n\u003cli data-sourcepos=\"365:3-365:64\"\u003e第一候補：iTerm2 ＋ \u003ccode\u003etmux -CC\u003c/code\u003e（＋ tmux.conf 3行）\u003c/li\u003e\n\u003cli data-sourcepos=\"366:3-366:17\"\u003e代替：cmux\u003c/li\u003e\n\u003cli data-sourcepos=\"367:3-367:72\"\u003e理由：公式に対応しているのは tmux と iTerm2 です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"368:1-371:127\"\u003e\n\u003cstrong\u003e3〜6つのエージェントを並列で動かし、待機中のものをすぐ見つけたい（Mac）\u003c/strong\u003e\n\u003cul data-sourcepos=\"369:3-371:127\"\u003e\n\u003cli data-sourcepos=\"369:3-369:23\"\u003e第一候補：cmux\u003c/li\u003e\n\u003cli data-sourcepos=\"370:3-370:18\"\u003e代替：herdr\u003c/li\u003e\n\u003cli data-sourcepos=\"371:3-371:127\"\u003e理由：通知リングとサイドバーにより、確認が必要なエージェントをすぐ見つけられます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"372:1-375:112\"\u003e\n\u003cstrong\u003e長時間ジョブ、SSH 先、複数マシンで使いたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"373:3-375:112\"\u003e\n\u003cli data-sourcepos=\"373:3-373:24\"\u003e第一候補：herdr\u003c/li\u003e\n\u003cli data-sourcepos=\"374:3-374:36\"\u003e代替：tmux ＋ 自前の hook\u003c/li\u003e\n\u003cli data-sourcepos=\"375:3-375:112\"\u003e理由：PC の蓋を閉じても処理が止まらず、複数マシンを横断して管理できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"376:1-379:100\"\u003e\n\u003cstrong\u003eIssue から worktree、PR までを GUI で効率化したい（Mac）\u003c/strong\u003e\n\u003cul data-sourcepos=\"377:3-379:100\"\u003e\n\u003cli data-sourcepos=\"377:3-377:66\"\u003e第一候補：Conductor（Free プランから利用可能）\u003c/li\u003e\n\u003cli data-sourcepos=\"378:3-378:17\"\u003e代替：Orca\u003c/li\u003e\n\u003cli data-sourcepos=\"379:3-379:100\"\u003e理由：setup / run スクリプト、ポート分離、PR フローがそろっています。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"380:1-383:105\"\u003e\n\u003cstrong\u003eOSS・クロスプラットフォームの ADE で、ブラウザを使って UI を検証したい\u003c/strong\u003e\n\u003cul data-sourcepos=\"381:3-383:105\"\u003e\n\u003cli data-sourcepos=\"381:3-381:23\"\u003e第一候補：Orca\u003c/li\u003e\n\u003cli data-sourcepos=\"382:3-382:22\"\u003e代替：Conductor\u003c/li\u003e\n\u003cli data-sourcepos=\"383:3-383:105\"\u003e理由：worktree、端末、ブラウザの三つがそろい、Design Mode も利用できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"384:1-387:73\"\u003e\n\u003cstrong\u003e仕様駆動で大きな機能をチーム開発したい\u003c/strong\u003e\n\u003cul data-sourcepos=\"385:3-387:73\"\u003e\n\u003cli data-sourcepos=\"385:3-385:26\"\u003e第一候補：Traycer\u003c/li\u003e\n\u003cli data-sourcepos=\"386:3-386:60\"\u003e代替：Claude Code の plan mode ＋ 手書きの spec\u003c/li\u003e\n\u003cli data-sourcepos=\"387:3-387:73\"\u003e理由：spec、ticket、review を artifact として残せます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"388:1-390:107\"\u003e\n\u003cstrong\u003eVS Code を使い続けたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"389:3-390:107\"\u003e\n\u003cli data-sourcepos=\"389:3-389:74\"\u003e第一候補：VS Code 拡張（Claude Code / Codex）＋ 外部端末\u003c/li\u003e\n\u003cli data-sourcepos=\"390:3-390:107\"\u003e理由：統合ターミナルには、GPU 描画、通知、分割ペインの制約があります。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"391:1-393:123\"\u003e\n\u003cstrong\u003eエディタを刷新し、並列エージェントを編集画面で扱いたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"392:3-393:123\"\u003e\n\u003cli data-sourcepos=\"392:3-392:31\"\u003e第一候補：Zed（ACP）\u003c/li\u003e\n\u003cli data-sourcepos=\"393:3-393:123\"\u003e理由：一つのウィンドウで並列スレッドを管理でき、diff もエディタ上で確認できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"394:1-396:114\"\u003e\n\u003cstrong\u003eモデル非依存の構成やローカルモデルを使いたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"395:3-396:114\"\u003e\n\u003cli data-sourcepos=\"395:3-395:44\"\u003e第一候補：OpenCode ＋ herdr / cmux\u003c/li\u003e\n\u003cli data-sourcepos=\"396:3-396:114\"\u003e理由：75以上のプロバイダに対応し、\u003ccode\u003eserve\u003c/code\u003e と \u003ccode\u003eattach\u003c/code\u003e で実行環境を分離できます。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"397:1-400:98\"\u003e\n\u003cstrong\u003eCodex を中心に使いたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"398:3-400:98\"\u003e\n\u003cli data-sourcepos=\"398:3-398:69\"\u003e第一候補：Codex アプリ（ChatGPT デスクトップ内）\u003c/li\u003e\n\u003cli data-sourcepos=\"399:3-399:22\"\u003e代替：Conductor\u003c/li\u003e\n\u003cli data-sourcepos=\"400:3-400:98\"\u003e理由：スレッドごとに worktree を持ち、自動実行にも対応しています。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"401:1-404:0\"\u003e\n\u003cstrong\u003eWindows または Linux で使いたい\u003c/strong\u003e\n\u003cul data-sourcepos=\"402:3-404:0\"\u003e\n\u003cli data-sourcepos=\"402:3-402:44\"\u003e第一候補：Orca / herdr / Warp / Zed\u003c/li\u003e\n\u003cli data-sourcepos=\"403:3-404:0\"\u003e理由：cmux、Conductor、iTerm2 は macOS 専用です。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr data-sourcepos=\"405:1-406:0\"\u003e\n\u003ch2 data-sourcepos=\"407:1-407:36\"\u003e\n\u003cspan id=\"6-もう一歩踏み込んだ話\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#6-%E3%82%82%E3%81%86%E4%B8%80%E6%AD%A9%E8%B8%8F%E3%81%BF%E8%BE%BC%E3%82%93%E3%81%A0%E8%A9%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6. もう一歩踏み込んだ話\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"409:1-409:56\"\u003e\n\u003cspan id=\"61-気づくまでの時間を自分で測る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#61-%E6%B0%97%E3%81%A5%E3%81%8F%E3%81%BE%E3%81%A7%E3%81%AE%E6%99%82%E9%96%93%E3%82%92%E8%87%AA%E5%88%86%E3%81%A7%E6%B8%AC%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6.1 「気づくまでの時間」を自分で測る\u003c/h3\u003e\n\u003cp data-sourcepos=\"411:1-411:403\"\u003eターミナル間の入力遅延の差は数ミリ秒程度です。しかし、AI コーディングでより大きな損失になるのは、エージェントが許可待ちのまま停止している時間です。この時間は製品カタログからは分からず、実際の運用の中で計測する必要があります。Claude Code の hooks を使えば、簡単に記録できます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"413:1-426:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"err\"\u003e//\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e~/.claude/settings.json（概念例）\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Notification\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"matcher\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"permission_prompt|idle_prompt\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"~/bin/tta-log notify\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"UserPromptSubmit\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"~/bin/tta-log submit\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sh\" data-sourcepos=\"428:1-434:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e#!/bin/sh\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# ~/bin/tta-log : 通知と復帰の時刻を端末名つきで記録する\u003c/span\u003e\n\u003cspan class=\"nb\"\u003emkdir\u003c/span\u003e \u003cspan class=\"nt\"\u003e-p\u003c/span\u003e ~/.tta\n\u003cspan class=\"nb\"\u003eprintf\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{\"ts\":%s,\"event\":\"%s\",\"term\":\"%s\",\"tmux\":%s}\\n'\u003c/span\u003e \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n  \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e$(\u003c/span\u003e\u003cspan class=\"nb\"\u003edate\u003c/span\u003e +%s\u003cspan class=\"si\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eTERM_PROGRAM\u003c/span\u003e\u003cspan class=\"k\"\u003e:-\u003c/span\u003e\u003cspan class=\"nv\"\u003eunknown\u003c/span\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eTMUX\u003c/span\u003e:+true\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e ~/.tta/events.jsonl\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"436:1-436:388\"\u003e\u003ccode\u003enotify\u003c/code\u003e から次の \u003ccode\u003esubmit\u003c/code\u003e までの時間が、「気づくまでの時間（Time-to-Attend）」です。ターミナルを1〜2週間ずつ切り替えて中央値を比較すれば、単に「cmux は良さそう」と評価するのではなく、「自分の環境では、iTerm2 の中央値が4分、cmux は50秒だった」と数値で判断できるようになります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"438:1-438:51\"\u003e\n\u003cspan id=\"62-ステータス信号を1本にまとめる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#62-%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E4%BF%A1%E5%8F%B7%E3%82%921%E6%9C%AC%E3%81%AB%E3%81%BE%E3%81%A8%E3%82%81%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6.2 ステータス信号を1本にまとめる\u003c/h3\u003e\n\u003cp data-sourcepos=\"440:1-440:543\"\u003eエージェントの状態をターミナルへ伝える経路は、現在のところ統一されていません。具体的には、通知用の OSC 9/99/777、iTerm2 の Session Status 用シーケンス、\u003ccode\u003ecmux notify\u003c/code\u003e、herdr の hook などがあります。ツールを切り替えるたびに hook を書き直すのは非効率です。そこで、状態通知の処理を1本の \u003ccode\u003eagent-status\u003c/code\u003e スクリプトに集約し、実行環境を検出して、対応するすべての経路に状態を送る構成がおすすめです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"442:1-448:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eClaude Code hook / Codex 通知 ──▶ agent-status \u0026lt;state\u0026gt; \u0026lt;detail\u0026gt;\n        ├─ 常に: OSC 9 でデスクトップ通知（tmux 内なら allow-passthrough 前提）\n        ├─ iTerm2 なら: Session Status に状態を書く\n        ├─ cmux があれば: cmux notify\n        └─ herdr があれば: herdr に状態を報告\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"450:1-450:264\"\u003eこうしておくと、端末は「交換可能な部品」になります。iTerm2 の公式統合は Claude Code だけが対象なので、Codex の状態も同じ Session Status に出したい人には、この自作スクリプトに価値があります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"452:1-452:65\"\u003e\n\u003cspan id=\"63-worktree-はセキュリティ境界ではありません\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#63-worktree-%E3%81%AF%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E5%A2%83%E7%95%8C%E3%81%A7%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6.3 worktree はセキュリティ境界ではありません\u003c/h3\u003e\n\u003cp data-sourcepos=\"454:1-454:397\"\u003eConductor の公式ドキュメントに明記されているとおり、worktree の分離は作業間の衝突を避けるためのものであり、エージェントを安全に閉じ込める仕組みではありません。必要な隔離レベルは、エージェントの並列数ではなく、どの程度まで権限の制限を緩和するかに応じて決めるのが安全です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"456:1-462:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e段5  クラウドサンドボックス / VM   Conductor Cloud, Orca の VM, Warp Factories\n段4  OS サンドボックス            Codex の Seatbelt / bubblewrap, Claude Code のサンドボックス\n段3  git worktree                  claude --worktree, Conductor, Orca, Codex アプリ, Traycer\n段2  tmux / herdr セッション       プロセスの寿命だけ分離\n段1  ペイン / タブ                  何も分離しない\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"464:1-464:197\"\u003e\u003ccode\u003e--dangerously-skip-permissions\u003c/code\u003e や \u003ccode\u003edanger-full-access\u003c/code\u003e のような権限緩和モードで実行するなら、少なくとも段4以上の隔離を用意するのが望ましいでしょう。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"466:1-466:74\"\u003e\n\u003cspan id=\"64-エージェントが端末を操作できることの裏面\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#64-%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E3%81%8C%E7%AB%AF%E6%9C%AB%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8%E3%81%AE%E8%A3%8F%E9%9D%A2\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e6.4 「エージェントが端末を操作できる」ことの裏面\u003c/h3\u003e\n\u003cp data-sourcepos=\"468:1-468:471\"\u003ecmux、herdr、iTerm2（Python API / it2）、Orca CLI、tmux の \u003ccode\u003esend-keys\u003c/code\u003e には、いずれも \u003cstrong\u003eエージェント自身が別のペインへキー入力を送り、画面の内容を読み取れる\u003c/strong\u003e仕組みがあります。これは生産性を高める一方で、プロンプトインジェクションの影響を受けたエージェントが、隣のペインに表示された許可プロンプトへ「y」を送るための経路にもなり得ます。\u003c/p\u003e\n\u003cp data-sourcepos=\"470:1-470:517\"\u003eClaude Code は、チームメイト間のメッセージを「ユーザーによる承認ではない」ものとして扱う設計です。しかし、ターミナル API を経由したキー入力は、その制御の対象外です。信頼できないリポジトリを扱うエージェントは、別のワークスペースやマシンに分けましょう。また、権限緩和モードとこれらの API を同時に使わないなど、事前に運用ルールを定めておく必要があります。\u003c/p\u003e\n\u003chr data-sourcepos=\"472:1-473:0\"\u003e\n\u003ch2 data-sourcepos=\"474:1-474:15\"\u003e\n\u003cspan id=\"おわりに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eおわりに\u003c/h2\u003e\n\u003cp data-sourcepos=\"476:1-476:337\"\u003e「AI コーディングのためのターミナル選び」を調べ始めたとき、私は、どれか1つの製品へ乗り換える問題だと考えていました。しかし、実際には、それぞれのツールが担う層を理解し、自分の運用に必要なものだけを組み合わせることが答えでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"478:1-478:435\"\u003eiTerm2 + VS Code を使っているなら、まずは iTerm2 3.7 の Claude Code 統合と、tmux 用の3行の設定から始めてください。それで不足を感じるようになったら herdr または cmux を追加し、PR 単位の並列作業が日常になったら Conductor または Orca を導入します。この順番なら、現在の環境を保ちながら、必要に応じて段階的に強化できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"480:1-480:329\"\u003e最後に、この記事で取り上げたツールの多くは、まだバージョン 1.0 に達していないか、リリースから数か月しか経っていません。Agent Teams も実験的な機能です。導入前に、バージョン、料金、対応状況を必ず公式サイトで再確認してください。\u003c/p\u003e\n\u003chr data-sourcepos=\"482:1-483:0\"\u003e\n\u003ch3 data-sourcepos=\"484:1-484:37\"\u003e\n\u003cspan id=\"参考リンク一次情報\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF%E4%B8%80%E6%AC%A1%E6%83%85%E5%A0%B1\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考リンク（一次情報）\u003c/h3\u003e\n\u003cul data-sourcepos=\"486:1-491:173\"\u003e\n\u003cli data-sourcepos=\"486:1-486:206\"\u003eClaude Code Docs: \u003ca href=\"https://code.claude.com/docs/en/terminal-config\" rel=\"nofollow noopener\" target=\"_blank\"\u003eターミナル設定\u003c/a\u003e / \u003ca href=\"https://code.claude.com/docs/en/agent-teams\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAgent Teams\u003c/a\u003e / \u003ca href=\"https://code.claude.com/docs/en/vs-code\" rel=\"nofollow noopener\" target=\"_blank\"\u003eVS Code\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"487:1-487:196\"\u003eiTerm2: \u003ca href=\"https://iterm2.com/claude-code-integration.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eClaude Code Integration\u003c/a\u003e / \u003ca href=\"https://iterm2.com/documentation-session-status.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eSession Status\u003c/a\u003e / \u003ca href=\"https://iterm2.com/news.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eNews\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"488:1-488:140\"\u003e\n\u003ca href=\"https://cmux.com/agents/claude-code\" rel=\"nofollow noopener\" target=\"_blank\"\u003ecmux\u003c/a\u003e / \u003ca href=\"https://herdr.dev/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eherdr\u003c/a\u003e / \u003ca href=\"https://www.warp.dev/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eWarp\u003c/a\u003e / \u003ca href=\"https://zed.dev/acp\" rel=\"nofollow noopener\" target=\"_blank\"\u003eZed ACP\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"489:1-489:154\"\u003e\n\u003ca href=\"https://www.onorca.dev/docs\" rel=\"nofollow noopener\" target=\"_blank\"\u003eOrca Docs\u003c/a\u003e / \u003ca href=\"https://www.conductor.build/docs\" rel=\"nofollow noopener\" target=\"_blank\"\u003eConductor Docs\u003c/a\u003e / \u003ca href=\"https://www.conductor.build/pricing\" rel=\"nofollow noopener\" target=\"_blank\"\u003eConductor Pricing\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"490:1-490:120\"\u003e\n\u003ca href=\"https://traycer.ai/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eTraycer\u003c/a\u003e / \u003ca href=\"https://opencode.ai/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eOpenCode\u003c/a\u003e / \u003ca href=\"https://opencode.ai/docs/server\" rel=\"nofollow noopener\" target=\"_blank\"\u003eOpenCode Server\u003c/a\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"491:1-491:173\"\u003e\n\u003ca href=\"https://developers.openai.com/codex/config-advanced\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCodex Docs（設定）\u003c/a\u003e / \u003ca href=\"https://developers.openai.com/codex/agent-approvals-security\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCodex サンドボックス\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n","body":"# AIコーディングのためのターミナル選び\n\n*2026年9月時点の情報です。この分野では、わずか数か月で前提が変わることがあります。情報の基準日を意識してお読みください。*\n\n## はじめに：なぜ今「ターミナル」を選び直すのか\n\n私はここ数年、macOS 上で **iTerm2 + VS Code** という構成を使って開発してきました。しかし、Claude Code と Codex を毎日使うようになると、ターミナルに求める役割が明らかに変わりました。\n\n従来のターミナルは、「自分がコマンドを入力する場所」でした。一方、現在は「エージェントが長時間作業し、必要に応じて人間に許可や判断を求める場所」になりつつあります。その結果、次のような新しい悩みが生まれます。\n\n- Claude Code を3つ並べたら、どのタブが自分の入力を待っているのか分からなくなった\n- 夜にエージェントへ大きなタスクを任せてノート PC を閉じたら、作業が止まっていた\n- Shift+Enter で改行したつもりが送信されてしまった\n- 通知が来ない（あるいは来すぎる）\n\nそこで、近年注目されている **tmux、cmux、Orca、OpenCode、Warp、herdr、Zed、Conductor、Traycer** を調べ、「Claude Code と Codex を主に使う iTerm2 + VS Code ユーザー」の視点から整理しました。\n\n---\n\n## TLDR\n\n1. **「どのターミナルに乗り換えるか」という問いだけでは、もはや十分ではありません。** 上に挙げた9つのツールは、それぞれ異なる「層」に属しています。たとえば、tmux と Orca は同じ基準で比較できません。考えるべきなのは、「自分の運用にどの層まで必要か」です。\n2. **iTerm2 ユーザーは、すぐに別のターミナルへ乗り換える必要はありません。** 2026年9月8日にリリースされた iTerm2 3.7 には、Claude Code の公式統合機能が加わりました。また、Claude Code の Agent Teams（分割ペイン表示）に公式対応するのは、**tmux と iTerm2 のみ**です。\n3. **VS Code の統合ターミナルは、公式ドキュメントに照らすと制約の多い実行環境です。** VS Code はエージェントの主な「実行場所」ではなく、「レビューする場所」として使うのが合理的です。\n4. **並列で動かすエージェントが常時4つを超えるなら、新たな層を1つ追加しましょう。** 永続化と SSH を重視するなら herdr、macOS ネイティブの見やすさを求めるなら cmux、worktree の作成から PR まで一貫して管理したいなら Conductor または Orca が候補になります。\n\n---\n\n## 1. 「ターミナル選び」は5層スタックの選択です\n\nまず、調査対象の各ツールがどの層に位置づけられるのかを整理します。\n\n| 層 | 役割 | 主なツール |\n|---|---|---|\n| L5 | 計画・仕様 | Traycer |\n| L4 | オーケストレーション（ADE） | Orca / Conductor / Codex app / Claude Code Desktop / Zed（ACP 経由） |\n| L3 | エージェント本体 | Claude Code / Codex CLI / OpenCode |\n| L2 | 多重化・永続化 | tmux / herdr / Zellij |\n| L1 | ターミナルエミュレータ | iTerm2 / Ghostty / cmux / Warp / VS Code 統合ターミナル / Zed 内蔵端末 |\n\n- **tmux（L2）**：古くから使われているマルチプレクサ。Claude Code Agent Teams の分割ペインを公式に支えるバックエンドです。\n- **herdr（L2）**：エージェントの状態（working / blocked / idle / done）を把握できる常駐ランタイムです。\n- **cmux（L1+L2）**：libghostty で描画する macOS ネイティブ端末。サイドバーと通知リングが特徴です。\n- **Warp（L1+L4）**：「Agentic Development Environment」を掲げる端末で、2026年4月にオープンソース化されました。\n- **Zed（L1/L4）**：Rust 製エディタ。ACP を介し、Claude Code や Codex をエディタ内の並列スレッドとして動かせます。\n- **Orca（L4）**：OSS の ADE。タスクごとに worktree、端末、ブラウザを用意します。\n- **Conductor（L4）**：macOS 向けアプリ。worktree、端末、diff、PR を一つのワークスペースにまとめます。\n- **Traycer（L5）**：仕様（spec / ticket）を先に固め、任意のエージェントへ実装を渡します。\n- **OpenCode（L3）**：プロバイダに依存しない OSS エージェント。Claude Code や Codex と同じ層に位置します。\n\nたとえば、「cmux と herdr はどちらがよいか」と聞かれたら、「cmux は人間が状況を把握する場所を、herdr はエージェントが稼働する環境を最適化するツール」と整理するのが正確です。cmux はターミナルエミュレータ自体なので、iTerm2 から乗り換えるか、別のアプリとして併用します。一方、herdr は iTerm2 の中で動かせます。この違いを理解すると、選択肢を絞りやすくなります。\n\n---\n\n## 2. Claude Code と Codex は、ターミナルに何を求めているのか\n\nツールを比較する前に、エージェント側がターミナルに求める要件を、公式ドキュメントに基づいて確認します。これらを押さえておけば、「なぜこの端末では通知が届かないのか」といった疑問を理解しやすくなります。\n\n### 2.1 Shift+Enter で改行できるか\n\nClaude Code では、Enter キーでメッセージを送信します。改行を入れる方法は、使用する端末によって異なります。公式ドキュメント（[Configure your terminal](https://code.claude.com/docs/en/terminal-config)）によると、対応状況は次のとおりです。\n\n- **設定なしで使える端末**：Ghostty、Kitty、iTerm2、WezTerm、Warp、Apple Terminal、Windows Terminal\n- **初回設定が必要な端末**：VS Code、Cursor、Alacritty、Zed。`/terminal-setup` を一度実行します。\n- **Shift+Enter に対応していない端末**：gnome-terminal、JetBrains 系 IDE。Ctrl+J、または「`\\` の後に Enter」を使います。\n\n端末に依存しない代替手段として、Ctrl+J、または「`\\` の後に Enter」という操作も覚えておくと安心です。\n\n### 2.2 デスクトップ通知が標準で届くか\n\n意外に知られていませんが、Claude Code が標準でデスクトップ通知を出すのは、**Ghostty、Kitty、iTerm2 の3種類のみ**です。それ以外の端末では、`preferredNotifChannel` を `\"terminal_bell\"` に設定するか、Notification hook を用意する必要があります。公式ドキュメントでは、Warp と VS Code の統合ターミナルが、通知を受け取るために hook または bell の設定を要する端末として明記されています。\n\niTerm2 でも、Settings → Profiles → Terminal の「Notification Center Alerts」を有効にし、さらに「Filter Alerts」から「Send escape sequence-generated alerts」を有効にする必要があります。これらの設定を見落とすと、iTerm2 を使っていても通知は表示されません。\n\n任意の端末で通知音を鳴らしたい場合は、hook を使う方法が確実です。\n\n```json\n// ~/.claude/settings.json\n{\n  \"hooks\": {\n    \"Notification\": [\n      {\n        \"matcher\": \"permission_prompt|idle_prompt\",\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"afplay /System/Library/Sounds/Glass.aiff\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\n`permission_prompt` は許可を待っているとき、`idle_prompt` は一定時間、入力待ちの状態が続いたときに発火します。\n\n### 2.3 tmux の中で動かすには、3行の設定が必要です\n\ntmux の中で Claude Code を動かすと、標準設定では Shift+Enter が送信として扱われ、デスクトップ通知や進捗バーも外側の端末まで届きません。公式ドキュメントが示す `~/.tmux.conf` の設定は、次の3行です。\n\n```tmux\nset -g allow-passthrough on          # 通知と進捗を外側の端末へ通す\nset -s extended-keys on              # Shift+Enter を Enter と区別する\nset -as terminal-features 'xterm*:extkeys'\n```\n\n`tmux source-file ~/.tmux.conf` を実行すれば、稼働中のサーバーにも設定を反映できます。\n\n### 2.4 Agent Teams（分割ペイン）は tmux か iTerm2 だけ\n\nClaude Code の Agent Teams は、1つのセッションがリーダーとなり、複数の Claude Code インスタンスにタスクを分担させる実験的な機能です（`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` で有効化します）。表示モードは2種類あります。in-process モードはどの端末でも利用できますが、各チームメイトを独立したペインに表示する split-pane モードには **tmux または iTerm2** が必要です。公式ドキュメントの制限事項には、split-pane モードを **VS Code の統合ターミナル、Windows Terminal、Ghostty では利用できない**ことが明記されています（[Agent teams](https://code.claude.com/docs/en/agent-teams)）。\n\n![Claude Code のサブエージェントと Agent Teams の違い（公式ドキュメントより）](https://mintcdn.com/claude-code/nsvRFSDNfpSU5nT7/images/subagents-vs-agent-teams-light.png?fit=max\u0026auto=format\u0026n=nsvRFSDNfpSU5nT7\u0026q=85\u0026s=2f8db9b4f3705dd3ab931fbe2d96e42a)\n\n*サブエージェントは結果をリーダーに返すのに対し、Agent Teams ではタスクリストを共有し、チームメイト同士が直接やり取りします（出典: [Claude Code Docs](https://code.claude.com/docs/en/agent-teams)）*\n\n`teammateMode` の設定は `~/.claude/settings.json` に書きます。\n\n```json\n{ \"teammateMode\": \"auto\" }\n```\n\n`auto` を指定すると、tmux 内で実行している場合、または iTerm2 に it2 CLI が導入されている場合にだけ分割ペインになります。それ以外の環境では、in-process モードに切り替わります。iTerm2 で分割ペインを使うには、it2 CLI を導入し、Settings → General → Magic で Python API を有効にします。公式ドキュメントが `tmux -CC`（iTerm2 のネイティブ tmux 統合）を推奨していることも、iTerm2 ユーザーにとっては大きな利点です。\n\n### 2.5 VS Code 統合ターミナルの弱点\n\nVS Code の統合ターミナルで Claude Code を使うと、`/terminal-setup` によって `terminal.integrated.gpuAcceleration` が `\"off\"` に書き換えられます。これは文字化けを防ぐためです。また、公式ドキュメントでは、大容量のテキストを貼り付けると一部の文字が失われることがあるため、ファイルを経由するワークフローが推奨されています。通知は標準では表示されず、Agent Teams の分割ペインにも対応していません。\n\nつまり、VS Code の統合ターミナルで Claude Code は動くものの、GPU 描画、通知、分割ペインには制約があります。VS Code を使い続けたい場合は、統合ターミナルで CLI を常用するよりも、**Claude Code の VS Code 拡張**（ネイティブパネル。[公式ドキュメント](https://code.claude.com/docs/en/vs-code)）と外部ターミナルを使い分ける構成が適しています。\n\n### 2.6 Codex 側の要件\n\nCodex CLI では、通知やサンドボックスに関する設定を `~/.codex/config.toml` に記述します（[Advanced Configuration](https://developers.openai.com/codex/config-advanced)）。TUI の通知は、`[tui]` セクションで指定します。\n\n```toml\n# ~/.codex/config.toml\n[tui]\nnotifications = [\"agent-turn-complete\", \"approval-requested\"]\nnotification_method = \"osc9\"   # auto / osc9 / bel\n```\n\nOSC 9 は、デスクトップ通知に使われる制御シーケンスです。この場合も、端末が OSC 9 を OS の通知に変換できるかどうかが重要です。従来の `notify = [...]` は非推奨となり、Stop などの hooks へ移行するよう案内されています。サンドボックスには、macOS では Seatbelt、Linux では bubblewrap + seccomp が使われます。`sandbox_mode` は `read-only` / `workspace-write` / `danger-full-access` から選択します（[Agent approvals \u0026 security](https://developers.openai.com/codex/agent-approvals-security)）。\n\n---\n\n## 3. 各ツールを見ていきます\n\n### 3.1 iTerm2 3.7 —— 現在の環境を維持したまま強化できる\n\niTerm2 3.7 は2026年9月8日にリリースされました。主な新機能は、iOS 連携アプリの iTerm2 Buddy、Claude Code 統合、タブグループです（[iTerm2 News](https://iterm2.com/news.html)）。\n\n![iTerm2 の Claude Code Integration セットアップ画面](https://iterm2.com/images/claude-code/ClaudeCode-Setup-Overview.png)\n\n*「iTerm2 \u003e Install Claude Code Integration」で、変更内容を確認しながらセットアップできます（出典: [iTerm2 公式ドキュメント](https://iterm2.com/claude-code-integration.html)）*\n\n公式ドキュメントによると、この統合機能を導入することで、各 Claude セッションの状態（working / waiting / idle）が iTerm2 の Session Status ツールに表示されます。さらに、Claude との会話、diff ビューア、コードレビュー用セッションをワンクリックで切り替えられる Workgroup も用意されます。\n\n![iTerm2 の Session Status。各セッションの状態がドットで表示される](https://iterm2.com/images/claude-code/ClaudeCode-SessionStatus.png)\n\n*Session Status。注意が必要なセッションほど上に並びます（出典: iTerm2 公式ドキュメント）*\n\n仕組みはシンプルです。`~/.claude/settings.json` に状態報告用の hook を追加し、iTerm2 の Python API を通じてツールベルトに反映します。アンインストールすれば元の状態に戻せる点も安心です。\n\n![Claude Code Workgroup のセットアップ](https://iterm2.com/images/claude-code/ClaudeCode-Setup-Workgroup.png)\n\n*Chat / Diff / Code Review の3ペインを1クリックで切り替える Workgroup（出典: iTerm2 公式ドキュメント）*\n\n**具体例**: Claude に「認証モジュールをリファクタリングして」と依頼した後、Workgroup の「Diff」を押すと、作業ツリーの差分が横に表示されます。「Code Review」を押せば、別セッションの Claude がレビューを開始します。レビューで得られた指摘は、Clippings を通じて元の Claude に返せます。これまで VS Code に切り替えて行っていた作業を、iTerm2 内で完結できます。\n\n注意点は2つあります。1つ目は、macOS でしか利用できないことです。2つ目は、Agent Teams 用の iTerm2 バックエンド（it2 CLI）について、2026年2月の時点で「iTerm2 を検出できず、何も通知しないまま in-process モードに切り替わる」という不具合が報告されていたことです（[#24292](https://github.com/anthropics/claude-code/issues/24292)、[#24301](https://github.com/anthropics/claude-code/issues/24301)）。安定性を優先するなら、iTerm2 + `tmux -CC` の組み合わせが手堅い選択です。\n\n### 3.2 tmux —— 実績のある公式バックエンド\n\ntmux はサーバー・クライアント型の構成を採用しているため、端末を閉じてもセッションが維持されます。SSH 接続先の Linux サーバーでも安定して動作します。Agent Teams の分割ペインも、tmux の `split-window` や `send-keys` を使って実装されています。\n\n弱点は、エージェントの状態を意味のある情報として扱えないことです。たとえば、許可待ちで停止しているペインと、単に何も実行していないペインを区別できません。また、初期の Agent Teams では、4つ以上のエージェントを同時に起動するとレイアウトが崩れる、`send-keys` で送る文字が化けてチームメイトの起動に失敗する、といった問題も報告されています（[#23615](https://github.com/anthropics/claude-code/issues/23615)）。\n\nそれでも tmux が重要であり続けるのは、Agent Teams の公式バックエンドであり、さまざまな OSS が下層として利用しているからです。herdr のように tmux そのものを置き換えるツールもありますが、tmux で得たセッション管理の知識は、他のツールに移行しても活かせます。\n\n### 3.3 cmux —— Mac で「どれが待っているか」を一目で把握\n\n![cmux](https://cmux.com/opengraph-image)\n\n*出典: [cmux 公式サイト](https://cmux.com/agents/claude-code)*\n\ncmux は、Swift/AppKit で開発された macOS ネイティブのターミナルです。描画エンジンに libghostty を使用し、テーマやフォントなど、既存の Ghostty 設定をそのまま読み込めます。開発の動機は非常に具体的です。Claude Code の通知は毎回「Claude is waiting for your input」と表示され、どの作業に関する通知なのかが分かりません。さらに、タブが増えるとタイトルも読めなくなります。cmux は、こうした問題を解決するために開発されました（[GitHub README](https://github.com/manaflow-ai/cmux)）。\n\n主な機能を挙げます。\n\n- **縦置きのサイドバー**: ワークスペースごとに、git ブランチ、関連する PR の番号と状態、作業ディレクトリ、LISTEN 中のポート、最新の通知内容を表示\n- **通知リング**: OSC 9/99/777 を検出し、エージェントが待機状態になるとペインを青い枠で強調。サイドバーのタブも点灯し、Cmd+Shift+U で最新の未読通知へ移動\n- **内蔵ブラウザ**: agent-browser 由来のスクリプト API を持ち、アクセシビリティツリーの取得、要素のクリック、フォーム入力、JS 評価ができる\n- **CLI と Unix socket**: ワークスペースの作成、ペインの分割、キー入力の送信、URL のオープンまでスクリプトで操作\n\nさらに、Claude Code の teammate モードをワンコマンドで起動できます。チームメイトは cmux のネイティブな分割ペインとして作成されるため、tmux は必要ありません。ベータ版の iOS コンパニオンアプリも提供されています。ライセンスは GPL で、無料のオープンソースソフトウェアです。\n\n**具体例**: 「API 実装」「テスト追加」「ドキュメント更新」の3つのワークスペースで Claude Code を動かし、その間に自分は別の作業を進めます。どれかが許可待ちになると、対象のペインだけが青く強調され、サイドバーには「Bash: npm test を実行しますか？」といった最新の内容が表示されます。そのワークスペースだけを確認すれば済みます。\n\ncmux 自身は「primitive であって solution ではない」と位置づけられており、worktree の管理や PR のワークフローはユーザー自身が構築する前提です。macOS でしか利用できず、プロジェクト自体もまだ若く、更新のペースが速いことは理解しておく必要があります。\n\n### 3.4 herdr —— 端末を閉じても稼働し続けるランタイム\n\n![herdr](https://herdr.dev/assets/og-card-v9.png)\n\n*出典: [herdr 公式サイト](https://herdr.dev/)*\n\nherdr は Rust 製の単一バイナリで、iTerm2 や Ghostty など、既存の端末内で動作します。最大の特徴は、常駐サーバーがセッションを保持する点です。ウィンドウを閉じたり、ノート PC の蓋を閉じたり、ネットワークが切断されたりしても、エージェントは稼働し続けます。さらに、マシンを再起動した後はレイアウトを復元し、セッションを再開できます。\n\nもう1つの特徴は、エージェントの状態検知です。herdr は各ペインの内容を読み取り、エージェントを working / blocked / idle / done に分類します。いずれかのエージェントが停止し、応答を待っている場合は、その状態を通知します。Claude Code、Codex、OpenCode、Cursor、Grok など、22種類のエージェント CLI を自動検出できます。\n\nエージェント自身が herdr を操作できる点も重要です。CLI と socket API は同じ機能を提供しており、エージェントはペインの分割、別のエージェントの起動、プロンプトの送信を行えます。さらに、相手が実際に blocked 状態になるまで待機できます。単にキー入力を送って結果を期待するのではなく、相手の状態を確認しながら待つ設計です。\n\nさらに、手元のノート PC、自宅のデスクトップ、リモートサーバーを SSH で追加すると、それぞれのワークスペースとエージェントを、ローカル環境のものと並べて表示できます。\n\n**具体例**: 金曜日の夕方に、Claude Code へ「すべてのテストが通るまで再試行して」と依頼し、herdr のペインで動かしたままノート PC を閉じます。月曜日に開くとペインは「done」と表示され、途中の出力もそのまま残っています。\n\n注意点として、herdr は v0.9 系で、まだ 1.0 に達していません（Apache-2.0、GitHub スター約4万）。ターミナルエミュレータではないため、描画品質は外側の端末に依存します。また、herdr のペイン内で tmux を入れ子にすると、状態検知が機能しなくなります。tmux と併用するのではなく、tmux の代替として使うのが適切です。\n\n### 3.5 Warp —— 端末ごと「開発環境」にする\n\n![Warp Terminal](https://www.warp.dev/img/products/homepage-v2-terminal.png)\n\n*出典: [Warp 公式サイト](https://www.warp.dev/)*\n\nWarp は Rust 製のクロスプラットフォーム対応ターミナルで、2026年4月にクライアントがオープンソース化されました。UI フレームワークは MIT、それ以外は AGPLv3 でライセンスされており、OpenAI がリポジトリの founding sponsor になっています（[GitHub](https://github.com/warpdotdev/warp)）。\n\nWarp は「Agentic Development Environment」を名乗っています。ブロック単位の出力表示、複数のモデルに対応する内蔵エージェント、Claude Code や Codex などの外部 CLI エージェントの取り込みに対応しています。さらに Factory MCP を通じて、triage → spec → implement → review → verify の工程を複数のエージェントで進めるクラウドサービス「Warp Factories」に作業を依頼できます。\n\n興味深いのは、Warp 自身が2026年9月の記事（[Best Terminal for AI Coding Agents in 2026](https://www.warp.dev/articles/best-terminal-for-ai-coding-agents)）で、「ブランドではなく層で選ぶ」と整理していることです。Ghostty や iTerm2 はエミュレータ、herdr はランタイム、Warp は ADE と、それぞれ異なる層に位置づけています。そのうえで、多くの開発者は2つの層を組み合わせて使うことになると述べています。本記事の5層モデルは、この整理を拡張したものです。\n\n弱点は、ここで取り上げるツールの中でも動作が比較的重く、利用にアカウントが必要なことです。また、前述のとおり、Claude Code のデスクトップ通知は標準設定では届かないため、hook の設定が必要です。個人が Claude Code と Codex をローカルで使うだけなら、機能が過剰と感じられるかもしれません。組織で作業をクラウドに移し、その実行状況を計測・管理したい場合に、特に力を発揮します。\n\n### 3.6 Zed —— エディタ側からエージェントを並列に\n\n![Claude Code in Zed](https://zed.dev/img/post/claude-code-zed/thumbnail.webp)\n\n*出典: [Zed 公式ブログ](https://zed.dev/blog/claude-code-via-acp)*\n\nZed は Rust 製の高速なエディタです。連携の中心となるのは、**ACP（Agent Client Protocol）** です。ACP は JSON-RPC ベースのオープン標準で、エディタがエージェントを別プロセスとして起動し、ファイル操作や権限要求をエディタ側の UI で扱えるようにします（[zed.dev/acp](https://zed.dev/acp)）。Zed のドキュメントでは、Gemini CLI、Claude Code、Codex CLI、OpenCode との連携が紹介されています。\n\n2026年4月29日にリリースされた Zed 1.0 の主要機能は、エージェントの並列実行です。1つのウィンドウ内で複数のエージェントを同時に動かし、コードベースの異なる部分を分担させられます。各エージェントによる編集はエディタ上に diff として反映され、その場でレビューできます。これは、ターミナル単体では得にくい強みです。\n\nただし、制約もあります。Zed の外部エージェント連携は ACP アダプタを経由するため、Claude Code の一部のスラッシュコマンドは、Zed のエージェントウィンドウ内で動作しないと指摘されています。Zed の公式ドキュメント（[External Agents](https://zed.dev/docs/ai/external-agents)）によると、Claude Agent の hooks と Agent Teams は現時点で対応していません。Claude Code の hooks や Agent Teams を積極的に活用している場合は、導入を見送るのが妥当です。\n\n### 3.7 Orca —— OSS で「worktree＋端末＋ブラウザ」を一体化\n\n![Orca のメイン画面。worktree のサイドバー、エージェント端末、diff ビュー](https://www.onorca.dev/whats-new/posters/orca-split-screen.jpg)\n\n*出典: [Orca 公式ドキュメント](https://www.onorca.dev/docs)*\n\nOrca は、複数の AI コーディングエージェントを並列で動かすためのデスクトップアプリです。タスクごとに git worktree、エージェント用ターミナル、ブラウザタブを用意します。自らを IDE ではなく、**ADE（Agent Development Environment）** と位置づけています。\n\n対応エージェントは幅広く、Claude Code、Codex、Gemini、Cursor CLI、GitHub Copilot、OpenCode、Amp、Goose、Cline、Kiro、Qwen Code などが事前設定済みで、任意の CLI エージェントも追加できます。レビュー面も充実していて、diff の特定行に Markdown コメントを付けてまとめてエージェントに返せるほか、CI の確認、コンフリクト解消、PR 作成もアプリ内で完結します。Design Mode では、worktree ごとの Chromium ウィンドウで UI 要素をクリックすると、その HTML・CSS・切り抜きスクリーンショットがエージェントに送られます。\n\n**具体例**: 「このボタンの余白がおかしい」とチャットで詳細に説明する代わりに、Design Mode でそのボタンをクリックします。すると、対象要素の情報がそのまま Claude Code に渡されます。フロントエンドにおける、言葉だけでは伝えにくい修正に効果的です。\n\nMIT ライセンスのもとで無料で利用でき、macOS、Windows、Linux に対応しています。SSH 接続先のマシン上でエージェントを動かすことも可能です。一方、Chromium と Monaco エディタを内包する比較的重いアプリです。機能追加のペースも非常に速く、UI や API が変更されやすいことは、導入前に理解しておきましょう。\n\n### 3.8 Conductor —— Issue から PR までを最短で\n\n![Conductor のワークスペース作成画面](https://www.conductor.build/docs-assets/images/git-worktrees-claude-code-workspace.png)\n\n*出典: [Conductor 公式ドキュメント](https://www.conductor.build/docs/guides/parallel-agents/run-multiple-claude-code-sessions)*\n\nConductor は macOS 向けアプリで、Claude Code、Codex、Cursor、OpenCode を並列で動かせます。タスクごとに、独立したワークスペース、ブランチ、ファイル、ターミナル、diff、レビューの経路が用意されます。作業完了後は、diff の確認、PR の作成、マージ、ワークスペースのアーカイブまで、一連の操作をアプリ内で進められます。\n\n実務で効くのは細かい配慮です。`.env.local` のような gitignore 済みファイルは「Files to copy」で新しいワークスペースに複製でき、依存のインストールは setup スクリプトに書け、複数の dev サーバーを同時に動かすときは `CONDUCTOR_PORT` でワークスペースごとにポート帯を分けられます。\n\n料金は Free が $0 でローカルのワークスペース、Pro が月 $50 で Conductor Cloud（クラウドワークスペース）や Multiplayer、API が付きます（[Pricing](https://www.conductor.build/pricing)）。クラウドワークスペースは Vercel のサンドボックス上で動き、アプリを閉じても作業が続きます。\n\n**具体例**: GitHub の Issue を3つ選び、それぞれのワークスペースを作成します。3つの Claude Code が異なる worktree で並列に作業し、完了したものから diff を確認して PR を作成します。`git worktree add` の実行や `.env` のコピーなど、従来は手作業で行っていた準備を省けます。\n\n注意点として、公式ドキュメント自身が「ワークスペースの分離は開発上の分離であって、セキュリティ境界ではない」と明記しています。並列セッションは同じアカウントの利用枠を消費しますし、クラウドではチャット内容が Conductor 側に保存されます。\n\n### 3.9 Traycer —— 「仕様を先に固める」ためのワークスペース\n\n![Traycer のワークスペース](https://framerusercontent.com/images/mKDnrG6VDOneveSftitfSiYZY.png?width=1536\u0026height=1024)\n\n*出典: [Traycer 公式サイト](https://traycer.ai/)*\n\nTraycer は、他のツールとは異なる層に位置します。「どのように実行するか」ではなく、「何を作るか」を事前に定めるためのワークスペースです。作業を Task 単位でまとめ、その中にエージェント（Chat または Terminal インターフェース）、ファイル、git diff、spec、ticket、review などの **artifact** を配置します（[Traycer Docs](https://docs.traycer.ai/)）。\n\n自分が契約しているサブスクリプションを持ち込む BYOA 方式で、Claude Code、Codex、OpenCode、Cursor を1つのワークスペース上で並べて使用できます。エージェント同士で質問を送り合い、レビューを依頼したり、作業を引き継いだりできます。\n\n**具体例**: 「決済プロバイダを Stripe から Adyen に切り替える」という大きな変更を、まず Traycer で PRD、技術計画、チケット群に分解します。チケットごとに Claude Code へ handoff し、エージェントは仕様を読みますが書き換えません。実装後は仕様と照らして検証します。「途中で何を作っていたか分からなくなる」問題への対策です。\n\n料金は、BYOA が $0、クラウド同期を利用できる Sync が月額 $10、クレジット付きの Lite が $20、Pro が $40、Ultra が $100 です（2026年9月時点、[Pricing](https://traycer.ai/pricing)）。小さなタスクに対しては準備の負担が大きいため、比較的規模の大きな機能をチームで分担する場合に適しています。\n\n### 3.10 OpenCode —— 「第3のエージェント」として\n\n![OpenCode](https://opencode.ai/social-share.png)\n\n*出典: [OpenCode 公式サイト](https://opencode.ai/)*\n\nOpenCode はターミナルではなく、Claude Code や Codex と同じ層に属する **エージェント**です。MIT ライセンスのオープンソースソフトウェアで、ターミナル、IDE、デスクトップアプリから利用できます。Claude、GPT、Gemini など、さまざまなプロバイダーのモデルに接続できます。GitHub スターは 208K を超えており、この分野で最大級のコミュニティを持ちます。\n\nターミナル選びの文脈で重要なのは client/server 構成です。`opencode serve` でヘッドレスの HTTP サーバーを立て、TUI はそのクライアントとして動くため、複数のクライアントから同じサーバーに接続したり、プログラムから操作したりできます（[Server](https://opencode.ai/docs/server)）。サーバーをリモートに置いて、手元の端末から `opencode attach` で繋ぐ、という使い方が自然にできます。\n\nClaude Code と Codex を主に使う人にとっての位置づけは、「ローカルモデルや別プロバイダを使う保険」です。cmux、herdr、Orca、Conductor はいずれも OpenCode に対応しているので、どの層を選んでも一緒に動かせます。\n\n### 3.11 あわせて知っておきたいもの\n\n- **Ghostty**: Shift+Enter もデスクトップ通知も設定不要で動く、最速級の端末です。ただし AI 機能は意図的に持たず、Agent Teams の分割ペインは非対応です（中で tmux を使えば回避できます）。cmux と Orca の描画エンジンでもあります。\n- **Claude Code Desktop**: 2026年4月の刷新で、1つのウィンドウで複数の Claude セッションを並べ、サイドバーで管理し、ペインをドラッグで並べ替えられるようになりました。ターミナルを使わずに並列運用したい人の選択肢です。\n- **Codex アプリ**: スレッドごとに worktree を持ち、ローカル環境とのハンドオフやスケジュール実行に対応しています。2026年7月に ChatGPT デスクトップアプリへ統合されたと報じられており、CLI と IDE 拡張も引き続き提供されています。Codex を中心に使う人にとっては、有力な選択肢です。\n\n---\n\n## 4. ツール別の比較\n\n各ツールの対応環境、実行基盤、連携・レビュー機能を順に整理します。\n\n- **iTerm2 3.7**\n  - **環境・価格**：macOS。無料（GPL）。\n  - **状態・実行基盤**：Session Status による状態表示に優れています。永続化には `tmux -CC` の併用が必要で、worktree の分離にも別途対応が必要です。\n  - **連携・レビュー**：Agent Teams の分割ペインには it2 または `tmux -CC` で対応します。Python API と it2 から操作でき、Workgroup でレビューできます。\n- **tmux**\n  - **環境・価格**：macOS、Linux。無料（ISC）。\n  - **状態・実行基盤**：エージェントの状態表示はありません。SSH 先でもセッションを永続化でき、`claude --worktree` を併用すれば worktree を分離できます。\n  - **連携・レビュー**：Agent Teams の分割ペインに公式対応しています。`send-keys` と `capture-pane` で操作できますが、レビュー機能はありません。\n- **cmux**\n  - **環境・価格**：macOS。無料（GPL）。\n  - **状態・実行基盤**：通知リングとサイドバーで状態を把握できます。永続化や worktree の分離には別途対応が必要です。\n  - **連携・レビュー**：Agent Teams をネイティブな分割ペインで表示できます。CLI、socket、ブラウザから操作でき、レビュー機能は条件付きで利用できます。\n- **herdr**\n  - **環境・価格**：macOS、Linux、Windows（ベータ版）。無料（Apache-2.0）。\n  - **状態・実行基盤**：4段階の状態表示に対応しています。常駐サーバーで実行を維持でき、複数マシンを横断できます。worktree の分離には別途対応が必要です。\n  - **連携・レビュー**：Agent Teams はプラグインで対応します。`wait-until-blocked` から操作できますが、レビュー機能はありません。\n- **Warp**\n  - **環境・価格**：macOS、Linux、Windows。無料プランあり（MIT + AGPL）。\n  - **状態・実行基盤**：状態表示に対応しています。クラウド機能の Factory で実行を維持でき、worktree の分離には別途対応が必要です。\n  - **連携・レビュー**：Agent Teams の分割ペインには対応していません。MCP から操作でき、レビュー機能も利用できます。\n- **Zed**\n  - **環境・価格**：macOS、Linux、Windows。無料（OSS）。\n  - **状態・実行基盤**：スレッド単位で状態を確認できます。実行の永続化やリモート実行には対応しておらず、worktree の分離には別途対応が必要です。\n  - **連携・レビュー**：Agent Teams の分割ペインには対応していません。ACP から条件付きで操作でき、エディタ上の diff でレビューできます。\n- **Orca**\n  - **環境・価格**：macOS、Windows、Linux。無料（MIT）。\n  - **状態・実行基盤**：Inbox で状態を把握できます。SSH と自己ホストに対応し、worktree を分離できます。\n  - **連携・レビュー**：Agent Teams の分割ペインには対応していません。Orca CLI から操作でき、行コメントを付けて PR へつなげられます。\n- **Conductor**\n  - **環境・価格**：macOS。Free プランと Pro プラン（50ドル）があります。\n  - **状態・実行基盤**：状態表示に対応しています。Pro では Cloud を利用でき、worktree も分離できます。\n  - **連携・レビュー**：Agent Teams は環境変数を設定すれば利用できます。Pro の API から操作でき、diff の確認から PR 作成まで進められます。\n- **Traycer**\n  - **環境・価格**：デスクトップ。BYOA 方式で、無料プランがあります。\n  - **状態・実行基盤**：状態表示、Sync、worktree の分離に対応しています。\n  - **連携・レビュー**：Agent Teams と操作用 API には対応していません。artifact を使ったレビューに優れています。\n- **OpenCode**\n  - **環境・価格**：macOS、Linux、WSL。無料（MIT）。\n  - **状態・実行基盤**：`serve` と `attach` によって実行を分離できます。状態表示と worktree 分離は、この比較では評価対象外です。\n  - **連携・レビュー**：HTTP API と ACP から操作できます。Agent Teams とレビュー機能は、この比較では評価対象外です。\n- **VS Code 統合端末**\n  - **環境・価格**：macOS、Linux、Windows。無料。\n  - **状態・実行基盤**：状態表示、実行の永続化、リモート実行、worktree の分離には対応していません。\n  - **連携・レビュー**：Agent Teams の分割ペインには対応していません。外部からの操作は限定的ですが、拡張機能の diff でレビューできます。\n\n---\n\n## 5. ユースケース別のおすすめ\n\n- **1〜2つのエージェントを使う個人開発**\n  - 第一候補：iTerm2 3.7 ＋ Claude Code Integration\n  - 代替：Ghostty\n  - 理由：別の端末へ乗り換えずに、状態表示、Diff/Review、iOS 通知を利用できます。\n- **Agent Teams を分割ペインで見たい**\n  - 第一候補：iTerm2 ＋ `tmux -CC`（＋ tmux.conf 3行）\n  - 代替：cmux\n  - 理由：公式に対応しているのは tmux と iTerm2 です。\n- **3〜6つのエージェントを並列で動かし、待機中のものをすぐ見つけたい（Mac）**\n  - 第一候補：cmux\n  - 代替：herdr\n  - 理由：通知リングとサイドバーにより、確認が必要なエージェントをすぐ見つけられます。\n- **長時間ジョブ、SSH 先、複数マシンで使いたい**\n  - 第一候補：herdr\n  - 代替：tmux ＋ 自前の hook\n  - 理由：PC の蓋を閉じても処理が止まらず、複数マシンを横断して管理できます。\n- **Issue から worktree、PR までを GUI で効率化したい（Mac）**\n  - 第一候補：Conductor（Free プランから利用可能）\n  - 代替：Orca\n  - 理由：setup / run スクリプト、ポート分離、PR フローがそろっています。\n- **OSS・クロスプラットフォームの ADE で、ブラウザを使って UI を検証したい**\n  - 第一候補：Orca\n  - 代替：Conductor\n  - 理由：worktree、端末、ブラウザの三つがそろい、Design Mode も利用できます。\n- **仕様駆動で大きな機能をチーム開発したい**\n  - 第一候補：Traycer\n  - 代替：Claude Code の plan mode ＋ 手書きの spec\n  - 理由：spec、ticket、review を artifact として残せます。\n- **VS Code を使い続けたい**\n  - 第一候補：VS Code 拡張（Claude Code / Codex）＋ 外部端末\n  - 理由：統合ターミナルには、GPU 描画、通知、分割ペインの制約があります。\n- **エディタを刷新し、並列エージェントを編集画面で扱いたい**\n  - 第一候補：Zed（ACP）\n  - 理由：一つのウィンドウで並列スレッドを管理でき、diff もエディタ上で確認できます。\n- **モデル非依存の構成やローカルモデルを使いたい**\n  - 第一候補：OpenCode ＋ herdr / cmux\n  - 理由：75以上のプロバイダに対応し、`serve` と `attach` で実行環境を分離できます。\n- **Codex を中心に使いたい**\n  - 第一候補：Codex アプリ（ChatGPT デスクトップ内）\n  - 代替：Conductor\n  - 理由：スレッドごとに worktree を持ち、自動実行にも対応しています。\n- **Windows または Linux で使いたい**\n  - 第一候補：Orca / herdr / Warp / Zed\n  - 理由：cmux、Conductor、iTerm2 は macOS 専用です。\n\n---\n\n## 6. もう一歩踏み込んだ話\n\n### 6.1 「気づくまでの時間」を自分で測る\n\nターミナル間の入力遅延の差は数ミリ秒程度です。しかし、AI コーディングでより大きな損失になるのは、エージェントが許可待ちのまま停止している時間です。この時間は製品カタログからは分からず、実際の運用の中で計測する必要があります。Claude Code の hooks を使えば、簡単に記録できます。\n\n```json\n// ~/.claude/settings.json（概念例）\n{\n  \"hooks\": {\n    \"Notification\": [\n      { \"matcher\": \"permission_prompt|idle_prompt\",\n        \"hooks\": [ { \"type\": \"command\", \"command\": \"~/bin/tta-log notify\" } ] }\n    ],\n    \"UserPromptSubmit\": [\n      { \"hooks\": [ { \"type\": \"command\", \"command\": \"~/bin/tta-log submit\" } ] }\n    ]\n  }\n}\n```\n\n```sh\n#!/bin/sh\n# ~/bin/tta-log : 通知と復帰の時刻を端末名つきで記録する\nmkdir -p ~/.tta\nprintf '{\"ts\":%s,\"event\":\"%s\",\"term\":\"%s\",\"tmux\":%s}\\n' \\\n  \"$(date +%s)\" \"$1\" \"${TERM_PROGRAM:-unknown}\" \"${TMUX:+true}\" \u003e\u003e ~/.tta/events.jsonl\n```\n\n`notify` から次の `submit` までの時間が、「気づくまでの時間（Time-to-Attend）」です。ターミナルを1〜2週間ずつ切り替えて中央値を比較すれば、単に「cmux は良さそう」と評価するのではなく、「自分の環境では、iTerm2 の中央値が4分、cmux は50秒だった」と数値で判断できるようになります。\n\n### 6.2 ステータス信号を1本にまとめる\n\nエージェントの状態をターミナルへ伝える経路は、現在のところ統一されていません。具体的には、通知用の OSC 9/99/777、iTerm2 の Session Status 用シーケンス、`cmux notify`、herdr の hook などがあります。ツールを切り替えるたびに hook を書き直すのは非効率です。そこで、状態通知の処理を1本の `agent-status` スクリプトに集約し、実行環境を検出して、対応するすべての経路に状態を送る構成がおすすめです。\n\n```\nClaude Code hook / Codex 通知 ──▶ agent-status \u003cstate\u003e \u003cdetail\u003e\n        ├─ 常に: OSC 9 でデスクトップ通知（tmux 内なら allow-passthrough 前提）\n        ├─ iTerm2 なら: Session Status に状態を書く\n        ├─ cmux があれば: cmux notify\n        └─ herdr があれば: herdr に状態を報告\n```\n\nこうしておくと、端末は「交換可能な部品」になります。iTerm2 の公式統合は Claude Code だけが対象なので、Codex の状態も同じ Session Status に出したい人には、この自作スクリプトに価値があります。\n\n### 6.3 worktree はセキュリティ境界ではありません\n\nConductor の公式ドキュメントに明記されているとおり、worktree の分離は作業間の衝突を避けるためのものであり、エージェントを安全に閉じ込める仕組みではありません。必要な隔離レベルは、エージェントの並列数ではなく、どの程度まで権限の制限を緩和するかに応じて決めるのが安全です。\n\n```\n段5  クラウドサンドボックス / VM   Conductor Cloud, Orca の VM, Warp Factories\n段4  OS サンドボックス            Codex の Seatbelt / bubblewrap, Claude Code のサンドボックス\n段3  git worktree                  claude --worktree, Conductor, Orca, Codex アプリ, Traycer\n段2  tmux / herdr セッション       プロセスの寿命だけ分離\n段1  ペイン / タブ                  何も分離しない\n```\n\n`--dangerously-skip-permissions` や `danger-full-access` のような権限緩和モードで実行するなら、少なくとも段4以上の隔離を用意するのが望ましいでしょう。\n\n### 6.4 「エージェントが端末を操作できる」ことの裏面\n\ncmux、herdr、iTerm2（Python API / it2）、Orca CLI、tmux の `send-keys` には、いずれも **エージェント自身が別のペインへキー入力を送り、画面の内容を読み取れる**仕組みがあります。これは生産性を高める一方で、プロンプトインジェクションの影響を受けたエージェントが、隣のペインに表示された許可プロンプトへ「y」を送るための経路にもなり得ます。\n\nClaude Code は、チームメイト間のメッセージを「ユーザーによる承認ではない」ものとして扱う設計です。しかし、ターミナル API を経由したキー入力は、その制御の対象外です。信頼できないリポジトリを扱うエージェントは、別のワークスペースやマシンに分けましょう。また、権限緩和モードとこれらの API を同時に使わないなど、事前に運用ルールを定めておく必要があります。\n\n---\n\n## おわりに\n\n「AI コーディングのためのターミナル選び」を調べ始めたとき、私は、どれか1つの製品へ乗り換える問題だと考えていました。しかし、実際には、それぞれのツールが担う層を理解し、自分の運用に必要なものだけを組み合わせることが答えでした。\n\niTerm2 + VS Code を使っているなら、まずは iTerm2 3.7 の Claude Code 統合と、tmux 用の3行の設定から始めてください。それで不足を感じるようになったら herdr または cmux を追加し、PR 単位の並列作業が日常になったら Conductor または Orca を導入します。この順番なら、現在の環境を保ちながら、必要に応じて段階的に強化できます。\n\n最後に、この記事で取り上げたツールの多くは、まだバージョン 1.0 に達していないか、リリースから数か月しか経っていません。Agent Teams も実験的な機能です。導入前に、バージョン、料金、対応状況を必ず公式サイトで再確認してください。\n\n---\n\n### 参考リンク（一次情報）\n\n- Claude Code Docs: [ターミナル設定](https://code.claude.com/docs/en/terminal-config) / [Agent Teams](https://code.claude.com/docs/en/agent-teams) / [VS Code](https://code.claude.com/docs/en/vs-code)\n- iTerm2: [Claude Code Integration](https://iterm2.com/claude-code-integration.html) / [Session Status](https://iterm2.com/documentation-session-status.html) / [News](https://iterm2.com/news.html)\n- [cmux](https://cmux.com/agents/claude-code) / [herdr](https://herdr.dev/) / [Warp](https://www.warp.dev/) / [Zed ACP](https://zed.dev/acp)\n- [Orca Docs](https://www.onorca.dev/docs) / [Conductor Docs](https://www.conductor.build/docs) / [Conductor Pricing](https://www.conductor.build/pricing)\n- [Traycer](https://traycer.ai/) / [OpenCode](https://opencode.ai/) / [OpenCode Server](https://opencode.ai/docs/server)\n- [Codex Docs（設定）](https://developers.openai.com/codex/config-advanced) / [Codex サンドボックス](https://developers.openai.com/codex/agent-approvals-security)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:30:25+09:00","group":null,"id":"814441321c658e471ab5","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Terminal","versions":[]},{"name":"AI駆動開発","versions":[]},{"name":"コーディングエージェント","versions":[]}],"title":"AIコーディングのためのターミナル選び","updated_at":"2026-09-27T11:30:25+09:00","url":"https://qiita.com/cvusk/items/814441321c658e471ab5","user":{"description":"Python Golang Kotlin Java C# C++ Bash Linux MachineLearning DeepLearning Keras Tensorflow Docker Kubernetes AWS GCP Unity Android PyTorch AR MENSA \r\nhttps://amzn.to/3Vh0fad\r\nhttps://amzn.to/3ilUpG7\r\n","facebook_id":"yusuke.shibui","followees_count":525,"followers_count":279,"github_login_name":"shibuiwilliam","id":"cvusk","items_count":109,"linkedin_id":"yusuke-shibui-64337932?trk=nav_responsive_tab_profile_pic","location":"Tokyo","name":"","organization":"","permanent_id":55384,"profile_image_url":"https://qiita-image-store.s3.amazonaws.com/0/55384/profile-images/1486270624","team_only":false,"twitter_screen_name":null,"website_url":"https://github.com/shibuiwilliam"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"2:1-2:8\"\u003e\n\u003cspan id=\"概要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A6%82%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e概要\u003c/h1\u003e\n\u003cp data-sourcepos=\"4:1-4:193\"\u003ePlaywrightでE2Eテストを実行したところ、デスクトップ環境では成功しましたが、モバイル環境でのみリンクのクリックがタイムアウトしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-6:126\"\u003e原因は、モバイル表示時に大きな画像がリンクに重なり、クリックを妨げていたことでした。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"8:1-8:21\"\u003e\n\u003cspan id=\"やりたいこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%84%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eやりたいこと\u003c/h2\u003e\n\u003cp data-sourcepos=\"10:1-10:106\"\u003ePlaywrightでリンクをクリックし、別の画面へ遷移するE2Eテストを成功させたい。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"12:1-12:24\"\u003e\n\u003cspan id=\"実行したコード\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%9F%E3%82%B3%E3%83%BC%E3%83%89\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実行したコード\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"14:1-27:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nf\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eリンクをクリックすると別の画面に遷移する\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003easync \u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"nx\"\u003epage\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003esamplePage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eSamplePage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003edestinationPage\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nc\"\u003eDestinationPage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003esamplePage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egoto\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003esamplePage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egotoDestination\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003eexpect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003etoHaveURL\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e/sample\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003eexpect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003edestinationPage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetPageTitle\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"nf\"\u003etoBeVisible\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"29:1-29:77\"\u003ePage Objectでは、リンクを取得してクリックしていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"ts\" data-sourcepos=\"31:1-53:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eexport\u003c/span\u003e \u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eSamplePage\u003c/span\u003e \u003cspan class=\"kd\"\u003eextends\u003c/span\u003e \u003cspan class=\"nc\"\u003eBasePage\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"nx\"\u003egetList\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eLocator\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"nx\"\u003egetSampleLink\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003eLocator\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n  \u003cspan class=\"nf\"\u003econstructor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003ePage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n    \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetList\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003elist\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetSampleLink\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetList\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003elistitem\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n      \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egetByRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003elink\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sr\"\u003e/サンプル/\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"nf\"\u003egoto\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003egoto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e/\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"nf\"\u003egotoDestination\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003egetSampleLink\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eclick\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"55:1-55:15\"\u003e\n\u003cspan id=\"実行結果\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実行結果\u003c/h2\u003e\n\u003cp data-sourcepos=\"57:1-57:127\"\u003eデスクトップ環境では成功しましたが、モバイル環境（Pixel 5）でのみテストが失敗しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"59:1-59:18\"\u003e\n\u003cspan id=\"エラー内容\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%A8%E3%83%A9%E3%83%BC%E5%86%85%E5%AE%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eエラー内容\u003c/h2\u003e\n\u003cp data-sourcepos=\"61:1-61:60\"\u003eリンクのクリックでタイムアウトしました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"63:1-67:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eTest timeout of 30000ms exceeded.\n\nError: locator.click: Test timeout of 30000ms exceeded.\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"69:1-69:128\"\u003eCall logを確認すると、リンク自体は表示されていましたが、画像がクリックを妨げていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"71:1-76:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eelement is visible, enabled and stable\nscrolling into view if needed\ndone scrolling\n\u0026lt;img ... /\u0026gt; from \u0026lt;header\u0026gt;…\u0026lt;/header\u0026gt; subtree intercepts pointer events\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"78:1-78:15\"\u003e\n\u003cspan id=\"原因調査\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8E%9F%E5%9B%A0%E8%AA%BF%E6%9F%BB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e原因調査\u003c/h2\u003e\n\u003cp data-sourcepos=\"80:1-80:102\"\u003eモバイル環境で画面を確認したところ、画像が大きく表示されていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"82:1-82:110\"\u003ePlaywrightのCall logにも、画像がpointer eventsを妨げていることが表示されていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"txt\" data-sourcepos=\"84:1-86:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u0026lt;img ... /\u0026gt; from \u0026lt;header\u0026gt;…\u0026lt;/header\u0026gt; subtree intercepts pointer events\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"88:1-88:9\"\u003e\n\u003cspan id=\"原因\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8E%9F%E5%9B%A0\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e原因\u003c/h2\u003e\n\u003cp data-sourcepos=\"90:1-90:114\"\u003eモバイル表示時に画像が大きく表示され、リンクに重なっていたことが原因でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"92:1-92:175\"\u003eそのため、Playwrightがリンクをクリックしようとしても画像がクリックを妨げ、クリックできないままタイムアウトしていました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"94:1-94:12\"\u003e\n\u003cspan id=\"解決策\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A7%A3%E6%B1%BA%E7%AD%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e解決策\u003c/h2\u003e\n\u003cp data-sourcepos=\"96:1-96:108\"\u003eモバイル表示でもリンクに重ならないように、画像のサイズを小さくしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"98:1-98:132\"\u003e画像のサイズを修正したことで、リンクをクリックできるようになり、E2Eテストも成功しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"100:1-100:18\"\u003e\n\u003cspan id=\"学んだこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AD%A6%E3%82%93%E3%81%A0%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e学んだこと\u003c/h2\u003e\n\u003cp data-sourcepos=\"102:1-102:223\"\u003ePlaywrightのテストが特定の画面サイズでのみ失敗する場合は、テストコードだけでなく、レスポンシブ表示による要素の重なりも確認する必要があると分かりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"104:1-104:134\"\u003eまた、Call logの\u003ccode\u003eintercepts pointer events\u003c/code\u003eから、別の要素がクリックを妨げていることを確認できました。\u003c/p\u003e\n","body":"\n# 概要\n\nPlaywrightでE2Eテストを実行したところ、デスクトップ環境では成功しましたが、モバイル環境でのみリンクのクリックがタイムアウトしました。\n\n原因は、モバイル表示時に大きな画像がリンクに重なり、クリックを妨げていたことでした。\n\n## やりたいこと\n\nPlaywrightでリンクをクリックし、別の画面へ遷移するE2Eテストを成功させたい。\n\n## 実行したコード\n\n```ts\ntest(\"リンクをクリックすると別の画面に遷移する\", async ({ page }) =\u003e {\n  const samplePage = new SamplePage(page);\n  const destinationPage = new DestinationPage(page);\n\n  await samplePage.goto();\n\n  await samplePage.gotoDestination();\n\n  await expect(page).toHaveURL(\"/sample\");\n\n  await expect(destinationPage.getPageTitle).toBeVisible();\n});\n```\n\nPage Objectでは、リンクを取得してクリックしていました。\n\n```ts\nexport class SamplePage extends BasePage {\n  readonly getList: Locator;\n  readonly getSampleLink: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    this.getList = page.getByRole(\"list\");\n    this.getSampleLink = this.getList\n      .getByRole(\"listitem\")\n      .getByRole(\"link\", { name: /サンプル/ });\n  }\n\n  async goto() {\n    await this.page.goto(\"/\");\n  }\n\n  async gotoDestination() {\n    await this.getSampleLink.click();\n  }\n}\n```\n\n## 実行結果\n\nデスクトップ環境では成功しましたが、モバイル環境（Pixel 5）でのみテストが失敗しました。\n\n## エラー内容\n\nリンクのクリックでタイムアウトしました。\n\n```txt\nTest timeout of 30000ms exceeded.\n\nError: locator.click: Test timeout of 30000ms exceeded.\n```\n\nCall logを確認すると、リンク自体は表示されていましたが、画像がクリックを妨げていました。\n\n```txt\nelement is visible, enabled and stable\nscrolling into view if needed\ndone scrolling\n\u003cimg ... /\u003e from \u003cheader\u003e…\u003c/header\u003e subtree intercepts pointer events\n```\n\n## 原因調査\n\nモバイル環境で画面を確認したところ、画像が大きく表示されていました。\n\nPlaywrightのCall logにも、画像がpointer eventsを妨げていることが表示されていました。\n\n```txt\n\u003cimg ... /\u003e from \u003cheader\u003e…\u003c/header\u003e subtree intercepts pointer events\n```\n\n## 原因\n\nモバイル表示時に画像が大きく表示され、リンクに重なっていたことが原因でした。\n\nそのため、Playwrightがリンクをクリックしようとしても画像がクリックを妨げ、クリックできないままタイムアウトしていました。\n\n## 解決策\n\nモバイル表示でもリンクに重ならないように、画像のサイズを小さくしました。\n\n画像のサイズを修正したことで、リンクをクリックできるようになり、E2Eテストも成功しました。\n\n## 学んだこと\n\nPlaywrightのテストが特定の画面サイズでのみ失敗する場合は、テストコードだけでなく、レスポンシブ表示による要素の重なりも確認する必要があると分かりました。\n\nまた、Call logの`intercepts pointer events`から、別の要素がクリックを妨げていることを確認できました。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:22:05+09:00","group":null,"id":"1a8bd71cb11168d0c1eb","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Playwright","versions":[]},{"name":"TypeScript","versions":[]},{"name":"e2e","versions":[]}],"title":"PlaywrightのE2Eテストがモバイル環境でのみタイムアウトした","updated_at":"2026-09-27T11:22:05+09:00","url":"https://qiita.com/kishimin/items/1a8bd71cb11168d0c1eb","user":{"description":"web_engineer_ohi\r\n\r\nfrontend\r\n\r\nのんびりしてもいい","facebook_id":"","followees_count":5,"followers_count":2,"github_login_name":"kishimin-test","id":"kishimin","items_count":16,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":2284906,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/2284906/6d96213edd300d1ae7ac02208959e1cb53ac3dba/x_large.png?1778278832","team_only":false,"twitter_screen_name":"kishimin_test","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:9\"\u003e\n\u003cspan id=\"概要\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A6%82%E8%A6%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e概要\u003c/h2\u003e\n\u003cdiv data-sourcepos=\"3:1-5:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"4:1-4:92\"\u003eこの記事は生成AIで作成した記事を人間により加筆修正しています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"7:1-7:97\"\u003e「レンタルサーバーの期限まであと1か月、今度はちゃんと移行したい」\u003c/p\u003e\n\u003cp data-sourcepos=\"9:1-9:178\"\u003e数年書いてきた個人ブログを、共用レンタルサーバーで動かしていました。レンタルサーバー代は年間1万円以上かかっていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"11:1-12:210\"\u003e以前Amazon Q Developer時代にAWSへの移行に挑戦してみましたが、記事の画像が抜け落ち、レイアウトが崩れてしまい、結局原因が分からないまま移行を断念しました。\u003cbr\u003e\n「またあれをやるのか」と気が重かったのですが、レンタルサーバーの更新期限（支払日）が迫っているので今度は原因から解決していくことにしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-15:107\"\u003eそして、無事に移行できました！\u003cbr\u003e\n369記事、画像6,378ファイル、広告の配信まで含めて、崩れずに引っ越せました！\u003c/p\u003e\n\u003cp data-sourcepos=\"17:1-18:308\"\u003e前回の失敗の原因は \u003cstrong\u003e日本語ファイル名のUnicode正規化\u003c/strong\u003e でした。この原因に気づくまでに時間がかかりました💦\u003cbr\u003e\n他にもパーマリンクが全ページ404になったり、DBを一度壊したり、切替後にメールが完全に死んでいたことに気づいたりしました。同じことをやろうとする人が同じ穴に落ちないように、詰まったところを中心に書いていきます。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"20:1-24:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"21:1-23:338\"\u003e\u003cstrong\u003eAmazon Lightsailって何？\u003c/strong\u003e\u003cbr\u003e\nAWSが提供している、月額固定のサーバーサービスです。すごく雑に言うと「AWSが用意してくれた、値段が分かりやすいレンタルサーバー」です。\u003cbr\u003e\n通常のAWS（EC2など）は使った分だけ課金されるので事前に金額が読みにくいのですが、Lightsailは「月$12でCPU2個・メモリ2GB・ディスク60GB・転送3TB」のように定額です。WordPress入りのイメージが用意されているので、作成した瞬間からブログが動きます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"26:1-26:27\"\u003e\n\u003cspan id=\"やりたかったこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eやりたかったこと\u003c/h2\u003e\n\u003cp data-sourcepos=\"28:1-28:36\"\u003e図にするとこれだけです。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__4d200b9ac220154203b6a882e53fdc91\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__4d200b9ac220154203b6a882e53fdc91\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart LR\\n A[\\\"共用レンタルサーバー\u0026lt;br/\u0026gt;WordPress\\\"] --\u0026gt;|\\\"記事・画像・DB\\\"| B[\\\"Amazon Lightsail\u0026lt;br/\u0026gt;WordPress\\\"]\\n C[\\\"訪問者\\\"] -.-\u0026gt;|\\\"切替前\\\"| A\\n C -.-\u0026gt;|\\\"切替後\\\"| B\",\"key\":\"aa546047e86d3817648d1f0113fc190f\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"37:1-37:324\"\u003eやることは記事とデータを移して、見ている先を切り替えるだけです。ただ、このブログには広告を貼っていてわずかにアドセンス収益が出ているので、表示が崩れたり画像が消えたりすると直接損をします。そこが気を遣うところでした。\u003c/p\u003e\n\u003cp data-sourcepos=\"39:1-39:48\"\u003e移行対象はこれくらいの規模です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"41:1-48:48\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"41:1-41:19\"\u003e\n\u003cth data-sourcepos=\"41:2-41:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"41:11-41:18\"\u003e数量\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"43:1-43:22\"\u003e\n\u003ctd data-sourcepos=\"43:2-43:15\"\u003e投稿記事\u003c/td\u003e\n\u003ctd data-sourcepos=\"43:17-43:21\"\u003e369\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"44:1-44:34\"\u003e\n\u003ctd data-sourcepos=\"44:2-44:27\"\u003eメディア（画像）\u003c/td\u003e\n\u003ctd data-sourcepos=\"44:29-44:33\"\u003e922\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"45:1-45:63\"\u003e\n\u003ctd data-sourcepos=\"45:2-45:54\"\u003e画像の実ファイル（サムネイル含む）\u003c/td\u003e\n\u003ctd data-sourcepos=\"45:56-45:62\"\u003e6,378\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"46:1-46:62\"\u003e\n\u003ctd data-sourcepos=\"46:2-46:42\"\u003eインストール済みプラグイン\u003c/td\u003e\n\u003ctd data-sourcepos=\"46:44-46:61\"\u003e35（有効29）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"47:1-47:29\"\u003e\n\u003ctd data-sourcepos=\"47:2-47:21\"\u003eデータベース\u003c/td\u003e\n\u003ctd data-sourcepos=\"47:23-47:28\"\u003e72MB\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"48:1-48:48\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:39\"\u003eアップロード済みファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:41-48:47\"\u003e261MB\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"50:1-50:27\"\u003e\n\u003cspan id=\"移行方法を比べる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%A7%BB%E8%A1%8C%E6%96%B9%E6%B3%95%E3%82%92%E6%AF%94%E3%81%B9%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e移行方法を比べる\u003c/h2\u003e\n\u003cp data-sourcepos=\"52:1-52:55\"\u003eWordPressの移行方法は大きく3つあります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"54:1-58:91\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"54:1-54:61\"\u003e\n\u003cth data-sourcepos=\"54:2-54:9\"\u003e方法\u003c/th\u003e\n\u003cth data-sourcepos=\"54:11-54:18\"\u003e手間\u003c/th\u003e\n\u003cth data-sourcepos=\"54:20-54:39\"\u003e失敗しにくさ\u003c/th\u003e\n\u003cth data-sourcepos=\"54:41-54:60\"\u003e向いている人\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"56:1-56:100\"\u003e\n\u003ctd data-sourcepos=\"56:2-56:60\"\u003e移行プラグイン（All-in-One WP Migration など）\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:62-56:66\"\u003e小\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:68-56:72\"\u003e中\u003c/td\u003e\n\u003ctd data-sourcepos=\"56:74-56:99\"\u003eサイトが小さい人\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"57:1-57:111\"\u003e\n\u003ctd data-sourcepos=\"57:2-57:44\"\u003eDBダンプ + FTPでファイル直取得\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:46-57:50\"\u003e中\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:52-57:56\"\u003e高\u003c/td\u003e\n\u003ctd data-sourcepos=\"57:58-57:110\"\u003eサイトが大きい人、原因を追いたい人\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"58:1-58:91\"\u003e\n\u003ctd data-sourcepos=\"58:2-58:45\"\u003e記事のエクスポート機能で移す\u003c/td\u003e\n\u003ctd data-sourcepos=\"58:47-58:51\"\u003e大\u003c/td\u003e\n\u003ctd data-sourcepos=\"58:53-58:57\"\u003e低\u003c/td\u003e\n\u003ctd data-sourcepos=\"58:59-58:90\"\u003e記事だけ移せばよい人\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"60:1-60:355\"\u003e最初はボタン一発で済む移行プラグインを使うつもりでしたが、実際に試してみると、エクスポートしたファイルのサイズが90MBを超えて、ブラウザ経由のアップロードが不安定でした。しかも移行元のサーバーではダウンロードそのものが500エラーになりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"62:1-62:282\"\u003e結局 DBダンプ + FTPでファイルを直接取る方法にしました。手間は増えますが、どのファイルがどこへ行ったかを自分で確認できます。前回失敗したため、分からない原因をできるだけ減らしたかったためです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"64:1-64:78\"\u003e\n\u003cspan id=\"前回の失敗の原因は日本語ファイル名の正規化だった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E5%9B%9E%E3%81%AE%E5%A4%B1%E6%95%97%E3%81%AE%E5%8E%9F%E5%9B%A0%E3%81%AF%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%AE%E6%AD%A3%E8%A6%8F%E5%8C%96%E3%81%A0%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前回の失敗の原因は、日本語ファイル名の正規化だった\u003c/h2\u003e\n\u003cp data-sourcepos=\"66:1-66:42\"\u003eここが今回一番の山場でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"68:1-68:205\"\u003e移行元のサイトを調べていて、同じ画像に対して2種類のURLが存在することに気づきました。見た目はまったく同じ文字列なのに、バイト列が違います。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"70:1-73:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eぺいんとicon.jpg   ← パターンA\nぺいんとicon.jpg   ← パターンB（見た目は同じ）\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"75:1-75:52\"\u003e原因はUnicodeの正規化形の違いでした。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"77:1-87:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"78:1-79:112\"\u003e\u003cstrong\u003eUnicode正規化（NFC / NFD）って何？\u003c/strong\u003e\u003cbr\u003e\n「が」のような濁点付きの文字には、コンピュータ上の表現方法が2通りあります。\u003c/p\u003e\n\u003cul data-sourcepos=\"81:1-83:0\"\u003e\n\u003cli data-sourcepos=\"81:1-81:48\"\u003e\n\u003cstrong\u003eNFC\u003c/strong\u003e … 「が」を1文字として持つ\u003c/li\u003e\n\u003cli data-sourcepos=\"82:1-83:0\"\u003e\n\u003cstrong\u003eNFD\u003c/strong\u003e … 「か」+「濁点」の2文字として持つ\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"84:1-86:74\"\u003e画面に表示すると、どちらも「が」に見えます。人間の目には区別がつきません。\u003cbr\u003e\nところがファイルシステムやWebサーバーにとっては、まったく別のファイル名です。\u003cbr\u003e\nmacOSは伝統的にNFDを使い、WindowsやLinuxはNFCを使います。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"89:1-89:54\"\u003eつまりこういうことが起きていました。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__ad866d10b41be74a5eb2cc89941b263f\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__ad866d10b41be74a5eb2cc89941b263f\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart TD\\n A[\\\"Macから画像をアップロード\\\"] --\u0026gt; B[\\\"NFD形のファイル名で保存される\\\"]\\n C[\\\"WindowsやブラウザからアップロードNFC形\\\"] --\u0026gt; D[\\\"NFC形のファイル名で保存される\\\"]\\n B --\u0026gt; E[\\\"同じサイトの中に\u0026lt;br/\u0026gt;2種類の正規化形が混在\\\"]\\n D --\u0026gt; E\\n E --\u0026gt; F{\\\"移行ツールが\u0026lt;br/\u0026gt;ファイル名を変換すると\\\"}\\n F --\u0026gt;|\\\"NFDをNFCに正規化\\\"| X[\\\"DBが参照するパスと\u0026lt;br/\u0026gt;実ファイル名が一致せず404\\\"]\\n F --\u0026gt;|\\\"バイト列のまま転送\\\"| G[\\\"正しく表示される\\\"]\",\"key\":\"88cc519410e8699a89d7feae7151853e\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"102:1-102:283\"\u003e移行元のサイトを実測すると、DBが参照している6,378パスのうち 640ファイルがNFD形でした。前回の移行では、このNFD形のファイルが転送中にNFCへ変換され、DBから見て「存在しないファイル」になっていました。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"104:1-107:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"105:1-106:174\"\u003eこの問題は転送ツールの設定で起きます。ファイル名を勝手に正規化するツールを使うと、日本語ファイル名の画像だけが静かに消えます。\u003cbr\u003e\nしかも消えるのは一部のファイルだけなので、トップページを見ただけでは気づきません。古い記事を開いて初めて気づきます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"109:1-109:156\"\u003e対策はファイル転送時に文字コードの変換をさせないようにします。今回は \u003ccode\u003elftp\u003c/code\u003e を使い、次の2行を指定しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"111:1-115:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# 文字コードの自動変換を無効にする。これがないとファイル名が正規化されてしまう\u003c/span\u003e\n\u003cspan class=\"nb\"\u003eset \u003c/span\u003eftp:charset \u003cspan class=\"s2\"\u003e\"\"\u003c/span\u003e\n\u003cspan class=\"nb\"\u003eset \u003c/span\u003efile:charset \u003cspan class=\"s2\"\u003e\"\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"117:1-117:145\"\u003e転送後、DBが参照する6,378パスすべてについて、実ファイルが存在するかをスクリプトで突き合わせました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"119:1-123:33\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"119:1-119:43\"\u003e\n\u003cth data-sourcepos=\"119:2-119:15\"\u003e正規化形\u003c/th\u003e\n\u003cth data-sourcepos=\"119:17-119:33\"\u003eファイル数\u003c/th\u003e\n\u003cth data-sourcepos=\"119:35-119:42\"\u003e一致\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"121:1-121:32\"\u003e\n\u003ctd data-sourcepos=\"121:2-121:14\"\u003eASCIIのみ\u003c/td\u003e\n\u003ctd data-sourcepos=\"121:16-121:22\"\u003e4,034\u003c/td\u003e\n\u003ctd data-sourcepos=\"121:24-121:31\"\u003e全件\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"122:1-122:27\"\u003e\n\u003ctd data-sourcepos=\"122:2-122:9\"\u003eNFC形\u003c/td\u003e\n\u003ctd data-sourcepos=\"122:11-122:17\"\u003e1,704\u003c/td\u003e\n\u003ctd data-sourcepos=\"122:19-122:26\"\u003e全件\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"123:1-123:33\"\u003e\n\u003ctd data-sourcepos=\"123:2-123:9\"\u003eNFD形\u003c/td\u003e\n\u003ctd data-sourcepos=\"123:11-123:19\"\u003e\u003cstrong\u003e640\u003c/strong\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"123:21-123:32\"\u003e\u003cstrong\u003e全件\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"125:1-125:75\"\u003e全件バイト一致を確認してから次の工程に進みました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"127:1-127:52\"\u003e\n\u003cspan id=\"lightsailのイメージ選びで1つ罠がある\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#lightsail%E3%81%AE%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E9%81%B8%E3%81%B3%E3%81%A71%E3%81%A4%E7%BD%A0%E3%81%8C%E3%81%82%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eLightsailのイメージ選びで1つ罠がある\u003c/h2\u003e\n\u003cp data-sourcepos=\"129:1-129:148\"\u003eLightsailでWordPressを作るとき、イメージ（ブループリント）を選びます。ここで名前の似た2つが並んでいます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"131:1-134:49\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"131:1-131:34\"\u003e\n\u003cth data-sourcepos=\"131:2-131:15\"\u003eイメージ\u003c/th\u003e\n\u003cth data-sourcepos=\"131:17-131:24\"\u003e中身\u003c/th\u003e\n\u003cth data-sourcepos=\"131:26-131:33\"\u003e状態\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"133:1-133:107\"\u003e\n\u003ctd data-sourcepos=\"133:2-133:12\"\u003eWordPress\u003c/td\u003e\n\u003ctd data-sourcepos=\"133:14-133:25\"\u003eBitnami版\u003c/td\u003e\n\u003ctd data-sourcepos=\"133:27-133:106\"\u003e\u003cstrong\u003e2026年5月に更新終了。2026年11月以降は新規作成できない\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"134:1-134:49\"\u003e\n\u003ctd data-sourcepos=\"134:2-134:30\"\u003eWordPress（Lightsail版）\u003c/td\u003e\n\u003ctd data-sourcepos=\"134:32-134:39\"\u003eAWS製\u003c/td\u003e\n\u003ctd data-sourcepos=\"134:41-134:48\"\u003e現行\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"136:1-136:235\"\u003e長年の情報の多くはBitnami版向けに書かれています。ディレクトリ構成もコマンドも違うので、記事を参考にするときは注意が必要です。今回は現行のLightsail版を選びました。\u003c/p\u003e\n\u003cp data-sourcepos=\"138:1-138:90\"\u003eLightsail版の構成を実際に確認すると以下のようになっていました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"140:1-147:54\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"140:1-140:16\"\u003e\n\u003cth data-sourcepos=\"140:2-140:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"140:11-140:15\"\u003e値\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"142:1-142:18\"\u003e\n\u003ctd data-sourcepos=\"142:2-142:5\"\u003eOS\u003c/td\u003e\n\u003ctd data-sourcepos=\"142:7-142:17\"\u003eDebian 12\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"143:1-143:42\"\u003e\n\u003ctd data-sourcepos=\"143:2-143:18\"\u003eWebサーバー\u003c/td\u003e\n\u003ctd data-sourcepos=\"143:20-143:41\"\u003eApache 2.4 + mod_php\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"144:1-144:13\"\u003e\n\u003ctd data-sourcepos=\"144:2-144:6\"\u003ePHP\u003c/td\u003e\n\u003ctd data-sourcepos=\"144:8-144:12\"\u003e8.2\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"145:1-145:38\"\u003e\n\u003ctd data-sourcepos=\"145:2-145:21\"\u003eデータベース\u003c/td\u003e\n\u003ctd data-sourcepos=\"145:23-145:37\"\u003eMariaDB 10.11\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"146:1-146:40\"\u003e\n\u003ctd data-sourcepos=\"146:2-146:21\"\u003eWordPressの場所\u003c/td\u003e\n\u003ctd data-sourcepos=\"146:23-146:39\"\u003e\u003ccode\u003e/var/www/html\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"147:1-147:54\"\u003e\n\u003ctd data-sourcepos=\"147:2-147:26\"\u003ewp-config.php の場所\u003c/td\u003e\n\u003ctd data-sourcepos=\"147:28-147:53\"\u003e\u003ccode\u003e/var/www/wp-config.php\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"149:1-149:142\"\u003e移行元のPHPは7.2でした。PHP 7.2 から 8.2 への飛躍があるので、古いプラグインが動かない覚悟が必要です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"151:1-151:21\"\u003e\n\u003cspan id=\"全体の仕組み\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%85%A8%E4%BD%93%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e全体の仕組み\u003c/h2\u003e\n\u003cp data-sourcepos=\"153:1-153:51\"\u003eできあがった構成はこうなりました。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__d6b115c52f420bd709b9a3c976c963ef\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__d6b115c52f420bd709b9a3c976c963ef\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart TD\\n U[\\\"訪問者\\\"] --\u0026gt; R53\\n\\n subgraph \\\"ドメイン管理\\\"\\n REG[\\\"ドメイン登録業者\u0026lt;br/\u0026gt;ネームサーバーだけ変更\\\"]\\n R53[\\\"Amazon Route 53\u0026lt;br/\u0026gt;DNSレコードを管理\\\"]\\n end\\n\\n subgraph \\\"AWS\\\"\\n LS[\\\"Lightsail\u0026lt;br/\u0026gt;WordPress + Apache + MariaDB\\\"]\\n SES[\\\"Amazon SES\u0026lt;br/\u0026gt;メール送信\\\"]\\n end\\n\\n REG -.-\u0026gt;|\\\"委任\\\"| R53\\n R53 --\u0026gt; LS\\n LS --\u0026gt; SES\\n SES --\u0026gt; M[\\\"管理者のメールボックス\\\"]\",\"key\":\"546820a30e7b58fc73d8f329eefe4049\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"175:1-175:197\"\u003eドメインの登録先は変えていません。ネームサーバーをRoute 53に向けただけです。移管はそれ自体がリスクなので、DNSの管理権だけを移しました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"177:1-177:36\"\u003e\n\u003cspan id=\"小さいサイトで先に試す\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%B0%8F%E3%81%95%E3%81%84%E3%82%B5%E3%82%A4%E3%83%88%E3%81%A7%E5%85%88%E3%81%AB%E8%A9%A6%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e小さいサイトで先に試す\u003c/h2\u003e\n\u003cp data-sourcepos=\"179:1-179:203\"\u003e本番のブログは369記事あります。これをいきなり移行するのは怖かったので、同じサーバーで動かしていた記事28本の小さいサイトで先に通しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"181:1-181:66\"\u003e予備検証の段階で、次の問題が新たに出ました。\u003c/p\u003e\n\u003cul data-sourcepos=\"183:1-186:0\"\u003e\n\u003cli data-sourcepos=\"183:1-183:47\"\u003eパーマリンクが全ページ404になる\u003c/li\u003e\n\u003cli data-sourcepos=\"184:1-184:59\"\u003eプラグインの一斉有効化でサイトが落ちる\u003c/li\u003e\n\u003cli data-sourcepos=\"185:1-186:0\"\u003eDBを壊しかける\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"187:1-187:249\"\u003e検証が終わったらインスタンスは削除して、本番用を手順書どおりに作り直しました。設定を試行錯誤で汚した環境をそのまま本番にすると、何が効いているのか分からなくなります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"189:1-189:76\"\u003e\n\u003cspan id=\"つまづきポイント1-パーマリンクが全ページ404になる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%A5%E3%81%8D%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%881-%E3%83%91%E3%83%BC%E3%83%9E%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%8C%E5%85%A8%E3%83%9A%E3%83%BC%E3%82%B8404%E3%81%AB%E3%81%AA%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまづきポイント(1) パーマリンクが全ページ404になる\u003c/h2\u003e\n\u003cp data-sourcepos=\"191:1-191:93\"\u003eトップページは表示されるのに、記事を開くと全部404になりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"193:1-193:124\"\u003e原因はDebianの既定設定でした。\u003ccode\u003e/var/www/\u003c/code\u003e に対して \u003ccode\u003e.htaccess\u003c/code\u003e を読まない設定になっています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"apache\" data-sourcepos=\"195:1-200:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e# Debianの既定（/etc/apache2/apache2.conf）\u003c/span\u003e\n\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nl\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"sr\"\u003e /var/www/\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\n\u003c/span\u003e    \u003cspan class=\"nc\"\u003eAllowOverride\u003c/span\u003e \u003cspan class=\"ss\"\u003eNone\u003c/span\u003e\n\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nl\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"202:1-202:160\"\u003eWordPressのパーマリンクは \u003ccode\u003e.htaccess\u003c/code\u003e の書き換えルールで動いているので、これが読まれないと記事URLが解決できません。\u003c/p\u003e\n\u003cp data-sourcepos=\"204:1-204:54\"\u003e設定ファイルを追加して解決しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"apache\" data-sourcepos=\"206:1-212:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nl\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"sr\"\u003e /var/www/html\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\n\u003c/span\u003e    \u003cspan class=\"nc\"\u003eOptions\u003c/span\u003e \u003cspan class=\"ss\"\u003eFollowSymLinks\u003c/span\u003e\n    \u003cspan class=\"nc\"\u003eAllowOverride\u003c/span\u003e \u003cspan class=\"ss\"\u003eAll\u003c/span\u003e\n    \u003cspan class=\"nc\"\u003eRequire\u003c/span\u003e \u003cspan class=\"ss\"\u003eall\u003c/span\u003e granted\n\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nl\"\u003eDirectory\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"214:1-216:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"215:1-215:325\"\u003e\u003ccode\u003e.htaccess\u003c/code\u003e ファイル自体も、移行元からコピーせず新規に作り直しました。移行元の \u003ccode\u003e.htaccess\u003c/code\u003e には、そのレンタルサーバー固有の記述（独自のキャッシュ制御やアクセス制限）が入っていて、別の環境では意味がないどころか動作を壊します。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"218:1-218:93\"\u003e\n\u003cspan id=\"つまづきポイント2-触ってはいけないファイルを動かしてdbを壊す\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%A5%E3%81%8D%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%882-%E8%A7%A6%E3%81%A3%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E5%8B%95%E3%81%8B%E3%81%97%E3%81%A6db%E3%82%92%E5%A3%8A%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまづきポイント(2) 触ってはいけないファイルを動かしてDBを壊す\u003c/h2\u003e\n\u003cp data-sourcepos=\"220:1-220:206\"\u003eLightsail版には \u003ccode\u003e/opt/aws/wordpress/credentials.log\u003c/code\u003e というファイルがあります。初期パスワードが書かれているので、読み終わったら消すか移動したくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"222:1-222:74\"\u003eこれを移動したらサイトがDB接続エラーになりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"224:1-224:141\"\u003e\u003ccode\u003ewp-config.php\u003c/code\u003e を読むと、実行時にこのファイルを読んでDBパスワードを取り出す作りになっていたのです。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"226:1-229:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"227:1-228:153\"\u003e\u003ccode\u003e/opt/aws/wordpress/credentials.log\u003c/code\u003e は削除も移動もしないでください。設定ファイルが実行時に参照しているので、無くなるとサイトが落ちます。\u003cbr\u003e\n「パスワードが平文で置いてあるのは気持ち悪い」と思っても、触るのは仕組みを確認してからにしてください。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"231:1-231:97\"\u003e\n\u003cspan id=\"つまづきポイント3-プラグインを一斉に有効化してサイトが500になる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%A5%E3%81%8D%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%883-%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%B8%80%E6%96%89%E3%81%AB%E6%9C%89%E5%8A%B9%E5%8C%96%E3%81%97%E3%81%A6%E3%82%B5%E3%82%A4%E3%83%88%E3%81%8C500%E3%81%AB%E3%81%AA%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまづきポイント(3) プラグインを一斉に有効化してサイトが500になる\u003c/h2\u003e\n\u003cp data-sourcepos=\"233:1-233:146\"\u003eファイルを移してDBを入れ、プラグインをまとめて有効化したところ、サイト全体が500エラーになりました。\u003c/p\u003e\n\u003cp data-sourcepos=\"235:1-235:70\"\u003eエラーログを見ると原因は1つのプラグインでした。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"237:1-240:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ePHP Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported.\n  in .../vendor/twig/twig/lib/Twig/Node.php on line 42\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"242:1-242:195\"\u003e三項演算子の書き方がPHP 8で禁止されたため、構文エラーで落ちていました。PHP 7.2で動いていた古いプラグインが8.2で死ぬ、という典型例です。\u003c/p\u003e\n\u003cp data-sourcepos=\"244:1-244:62\"\u003e最終的にPHP 8.2で動かなかったのは4つでした。\u003c/p\u003e\n\u003ctable data-sourcepos=\"246:1-251:44\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"246:1-246:37\"\u003e\n\u003cth data-sourcepos=\"246:2-246:18\"\u003eプラグイン\u003c/th\u003e\n\u003cth data-sourcepos=\"246:20-246:27\"\u003e症状\u003c/th\u003e\n\u003cth data-sourcepos=\"246:29-246:36\"\u003e対応\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"248:1-248:78\"\u003e\n\u003ctd data-sourcepos=\"248:2-248:18\"\u003eSNSシェア系\u003c/td\u003e\n\u003ctd data-sourcepos=\"248:20-248:32\"\u003eFatal error\u003c/td\u003e\n\u003ctd data-sourcepos=\"248:34-248:77\"\u003e停止。テーマの標準機能で代替\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"249:1-249:89\"\u003e\n\u003ctd data-sourcepos=\"249:2-249:27\"\u003eカテゴリ一括変更\u003c/td\u003e\n\u003ctd data-sourcepos=\"249:29-249:37\"\u003eWarning\u003c/td\u003e\n\u003ctd data-sourcepos=\"249:39-249:88\"\u003e停止。管理用なので表示に影響なし\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"250:1-250:47\"\u003e\n\u003ctd data-sourcepos=\"250:2-250:27\"\u003e外部画像取り込み\u003c/td\u003e\n\u003ctd data-sourcepos=\"250:29-250:37\"\u003eWarning\u003c/td\u003e\n\u003ctd data-sourcepos=\"250:39-250:46\"\u003e停止\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"251:1-251:44\"\u003e\n\u003ctd data-sourcepos=\"251:2-251:24\"\u003e記事マップ表示\u003c/td\u003e\n\u003ctd data-sourcepos=\"251:26-251:34\"\u003eWarning\u003c/td\u003e\n\u003ctd data-sourcepos=\"251:36-251:43\"\u003e停止\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"253:1-253:101\"\u003e有効29個のうち23個が動き、Fatal errorもWarningも0件の状態に落ち着きました。\u003c/p\u003e\n\u003cp data-sourcepos=\"255:1-255:239\"\u003e一番心配していたのは、広告を PHPコードを直接書けるウィジェットで表示していた箇所です。2010年代のプラグインなので8.2で動く保証がありません。これは無事に動きました。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"257:1-259:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"258:1-258:247\"\u003eプラグインを移すときは、一斉に有効化せず DBの \u003ccode\u003eactive_plugins\u003c/code\u003e を空にした状態から1つずつ有効化してください。まとめて有効化すると、どれが原因で落ちたのか分からなくなります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"261:1-261:37\"\u003e\n\u003cspan id=\"dnsの切替は2段階に分ける\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#dns%E3%81%AE%E5%88%87%E6%9B%BF%E3%81%AF2%E6%AE%B5%E9%9A%8E%E3%81%AB%E5%88%86%E3%81%91%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eDNSの切替は2段階に分ける\u003c/h2\u003e\n\u003cp data-sourcepos=\"263:1-263:130\"\u003eここからが本番切替です。いきなりやると問題の切り分けができないので、2段階に分けました。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__4feed8813726e695594d2f9cd077e2e1\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__4feed8813726e695594d2f9cd077e2e1\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart TD\\n S[\\\"開始\\\"] --\u0026gt; P1[\\\"フェーズ1\u0026lt;br/\u0026gt;ネームサーバーをRoute 53へ\\\"]\\n P1 --\u0026gt; V1{\\\"レコードの内容は\u0026lt;br/\u0026gt;移行元と同一か\\\"}\\n V1 --\u0026gt;|\\\"同一\\\"| W[\\\"訪問者は従来のサーバーを見たまま\\\"]\\n W --\u0026gt; P2[\\\"フェーズ2\u0026lt;br/\u0026gt;Aレコードを新サーバーへ\\\"]\\n P2 --\u0026gt; V2{\\\"表示・画像・広告は\u0026lt;br/\u0026gt;正常か\\\"}\\n V2 --\u0026gt;|\\\"異常\\\"| RB[\\\"Aレコードを戻す\u0026lt;br/\u0026gt;約1分で復帰\\\"]\\n V2 --\u0026gt;|\\\"正常\\\"| DONE[\\\"切替完了\\\"]\",\"key\":\"12ad4f0696f7d4d51220b93e9aaad8e0\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"276:1-276:249\"\u003eフェーズ1では、移行元とまったく同じ内容のレコードをRoute 53に作ってから委任を切り替えます。訪問者から見た挙動は何も変わりません。ここでDNSの移管だけを確実に終わらせます。\u003c/p\u003e\n\u003cp data-sourcepos=\"278:1-278:292\"\u003eこのとき見落としやすいのがワイルドカードレコードです。移行元には \u003ccode\u003e*\u003c/code\u003e のAレコードがあり、これを写し忘れると一部のサブドメインが解決できなくなります。移行前のレコードは1つずつ実測して書き写しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"280:1-280:185\"\u003eフェーズ2でAレコードを新サーバーに向けます。TTLを60秒にしておいたので、反映も巻き戻しも1分で済みます。実測ではこうなりました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"282:1-287:24\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"282:1-282:40\"\u003e\n\u003cth data-sourcepos=\"282:2-282:15\"\u003eリゾルバ\u003c/th\u003e\n\u003cth data-sourcepos=\"282:17-282:39\"\u003e反映までの時間\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"284:1-284:32\"\u003e\n\u003ctd data-sourcepos=\"284:2-284:21\"\u003e権威サーバー\u003c/td\u003e\n\u003ctd data-sourcepos=\"284:23-284:31\"\u003e約1分\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"285:1-285:31\"\u003e\n\u003ctd data-sourcepos=\"285:2-285:20\"\u003eGoogle Public DNS\u003c/td\u003e\n\u003ctd data-sourcepos=\"285:22-285:30\"\u003e約1分\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"286:1-286:29\"\u003e\n\u003ctd data-sourcepos=\"286:2-286:18\"\u003eQuad9 / OpenDNS\u003c/td\u003e\n\u003ctd data-sourcepos=\"286:20-286:28\"\u003e約1分\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"287:1-287:24\"\u003e\n\u003ctd data-sourcepos=\"287:2-287:13\"\u003eCloudflare\u003c/td\u003e\n\u003ctd data-sourcepos=\"287:15-287:23\"\u003e約4分\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"289:1-289:38\"\u003e\n\u003cspan id=\"https証明書は切替前に取る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#https%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AF%E5%88%87%E6%9B%BF%E5%89%8D%E3%81%AB%E5%8F%96%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eHTTPS証明書は切替前に取る\u003c/h2\u003e\n\u003cp data-sourcepos=\"291:1-291:93\"\u003eここは事前に考えておかないと、証明書エラーの時間帯ができます。\u003c/p\u003e\n\u003cp data-sourcepos=\"293:1-293:68\"\u003eLet's Encryptの証明書を取る方法は主に2つあります。\u003c/p\u003e\n\u003ctable data-sourcepos=\"295:1-298:82\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"295:1-295:55\"\u003e\n\u003cth data-sourcepos=\"295:2-295:15\"\u003e認証方式\u003c/th\u003e\n\u003cth data-sourcepos=\"295:17-295:27\"\u003e仕組み\u003c/th\u003e\n\u003cth data-sourcepos=\"295:29-295:54\"\u003e切替前に取れるか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"297:1-297:99\"\u003e\n\u003ctd data-sourcepos=\"297:2-297:10\"\u003eHTTP-01\u003c/td\u003e\n\u003ctd data-sourcepos=\"297:12-297:79\"\u003eWebサーバーにファイルを置いて認証局に読ませる\u003c/td\u003e\n\u003ctd data-sourcepos=\"297:81-297:98\"\u003e\u003cstrong\u003e取れない\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"298:1-298:82\"\u003e\n\u003ctd data-sourcepos=\"298:2-298:9\"\u003eDNS-01\u003c/td\u003e\n\u003ctd data-sourcepos=\"298:11-298:69\"\u003eDNSにTXTレコードを置いて認証局に読ませる\u003c/td\u003e\n\u003ctd data-sourcepos=\"298:71-298:81\"\u003e取れる\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"300:1-300:249\"\u003eHTTP-01が使えないのは、認証局がAレコードを引いて接続してくるためです。切替前はAレコードが古いサーバーを指しているので、認証のリクエストは古いサーバーに届いてしまいます。\u003c/p\u003e\n\u003cp data-sourcepos=\"302:1-302:75\"\u003eつまり切替前に証明書を取るならDNS-01しかありません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"304:1-304:49\"\u003e\n\u003cspan id=\"iamユーザーを作らずにdns-01を通す\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#iam%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%82%92%E4%BD%9C%E3%82%89%E3%81%9A%E3%81%ABdns-01%E3%82%92%E9%80%9A%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eIAMユーザーを作らずにDNS-01を通す\u003c/h3\u003e\n\u003cp data-sourcepos=\"306:1-306:238\"\u003ecertbotにはRoute 53を自動操作するプラグインがあります。ただしLightsailのインスタンスには IAMロールを割り当てられないので、アクセスキーを持つIAMユーザーが必要になります。\u003c/p\u003e\n\u003cp data-sourcepos=\"308:1-308:255\"\u003e「取得したら鍵を消せば安全」と考えたのですが、うまくいきませんでした。鍵を消した時点で証明書の自動更新も止まります。ということで、最初から鍵を作らない方法を採用しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"310:1-310:173\"\u003ecertbotの \u003ccode\u003e--manual-auth-hook\u003c/code\u003e は「TXTレコードを登録する処理」を書く場所ですが、「登録されるのを待つ処理」を書いても動きます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"312:1-332:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e#!/bin/bash\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# certbot が CERTBOT_DOMAIN と CERTBOT_VALIDATION を環境変数で渡してくる\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# このフックはRoute 53を操作しない。要求された値が見えるまで権威サーバーに問い合わせて待つ\u003c/span\u003e\n\u003cspan class=\"nb\"\u003eset\u003c/span\u003e \u003cspan class=\"nt\"\u003e-uo\u003c/span\u003e pipefail\n\u003cspan class=\"nv\"\u003eD\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eCERTBOT_DOMAIN\u003c/span\u003e\u003cspan class=\"k\"\u003e:-}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"nv\"\u003eV\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eCERTBOT_VALIDATION\u003c/span\u003e\u003cspan class=\"k\"\u003e:-}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\n\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"REQ \u003c/span\u003e\u003cspan class=\"si\"\u003e$(\u003c/span\u003e\u003cspan class=\"nb\"\u003edate\u003c/span\u003e \u003cspan class=\"nt\"\u003e-Is\u003c/span\u003e\u003cspan class=\"si\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e _acme-challenge.\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eD\u003c/span\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eV\u003c/span\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e /home/admin/acme/requests.txt\n\n\u003cspan class=\"c\"\u003e# 最大15分待つ\u003c/span\u003e\n\u003cspan class=\"k\"\u003efor \u003c/span\u003ei \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"si\"\u003e$(\u003c/span\u003e\u003cspan class=\"nb\"\u003eseq \u003c/span\u003e1 90\u003cspan class=\"si\"\u003e)\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003edo\n  if \u003c/span\u003edig +short +time\u003cspan class=\"o\"\u003e=\u003c/span\u003e5 +tries\u003cspan class=\"o\"\u003e=\u003c/span\u003e1 TXT \u003cspan class=\"s2\"\u003e\"_acme-challenge.\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eD\u003c/span\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e @ns-xxxx.awsdns-xx.org \u003cspan class=\"se\"\u003e\\\u003c/span\u003e\n      | \u003cspan class=\"nb\"\u003etr\u003c/span\u003e \u003cspan class=\"nt\"\u003e-d\u003c/span\u003e \u003cspan class=\"s1\"\u003e'\"'\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-qx\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$V\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003ethen\n    \u003c/span\u003e\u003cspan class=\"nb\"\u003esleep \u003c/span\u003e10\n    \u003cspan class=\"nb\"\u003eexit \u003c/span\u003e0\n  \u003cspan class=\"k\"\u003efi\n  \u003c/span\u003e\u003cspan class=\"nb\"\u003esleep \u003c/span\u003e10\n\u003cspan class=\"k\"\u003edone\n\u003c/span\u003e\u003cspan class=\"nb\"\u003eexit \u003c/span\u003e1\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"334:1-334:320\"\u003eフックが要求をファイルに書いて待っている間に、別の場所からRoute 53のAPIでTXTレコードを登録します。値が見えたらフックが \u003ccode\u003eexit 0\u003c/code\u003e して、certbotが認証に進みます。これでサーバー上にAWSの認証情報を1つも置かずに証明書が取れました。\u003c/p\u003e\n\u003cp data-sourcepos=\"336:1-336:107\"\u003e実測では、TXTレコードを登録してから権威サーバーに見えるまで1〜2分でした。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"338:1-342:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"339:1-341:52\"\u003e\u003cstrong\u003e本番の前に \u003ccode\u003e--dry-run\u003c/code\u003e で通す\u003c/strong\u003e\u003cbr\u003e\ncertbotの \u003ccode\u003e--dry-run\u003c/code\u003e はテスト環境を使うので、本番の発行制限を消費しません。しかも認証フックは実際に実行されるので、待機ロジックをそのままリハーサルできます。\u003cbr\u003e\n1回試してから本番を叩くと安心です。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"344:1-344:40\"\u003e\n\u003cspan id=\"更新方式は切替後に変える\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9B%B4%E6%96%B0%E6%96%B9%E5%BC%8F%E3%81%AF%E5%88%87%E6%9B%BF%E5%BE%8C%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e更新方式は切替後に変える\u003c/h3\u003e\n\u003cp data-sourcepos=\"346:1-346:317\"\u003e初回はDNS-01で取りましたが、この設定のままだと自動更新のたびに人間を待つ状態になります。切替が終わればAレコードが新サーバーを指すのでHTTP-01が使えます。設定ファイルを書き換えて、追加の発行なしで自動更新に移しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"348:1-348:223\"\u003eここで1つ失敗があったので、注意です。設定ファイルにセクションをファイルの途中に挿入したため、後続の行がそのセクションの中身として読まれて壊れました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"350:1-353:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eFailed to renew certificate with error: Couldn't create root for server http-01\nchallenge responses: No such file or directory: 'https:/acme-v02.api.letsencrypt.org'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"355:1-355:287\"\u003eURLをディレクトリとして開こうとしています。INI形式なので、セクション見出し以降の行は全部そのセクションに属します。結果として \u003ccode\u003eserver = https://...\u003c/code\u003e が「ドメイン名 server の設定」として解釈されていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"357:1-357:102\"\u003eそもそもそのセクションは不要だったので、削除したら一発で通りました。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"359:1-361:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"360:1-360:319\"\u003eセクションを持つ設定ファイル（INI、TOMLなど）に項目を追加するときは、必ずファイルの末尾に置くか、セクションの構造を理解してから入れてください。行単位の置換でセクション見出しを挿入すると、後ろの行の意味が変わります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"363:1-363:57\"\u003e\n\u003cspan id=\"切替前に本番と同じ経路で検証する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%88%87%E6%9B%BF%E5%89%8D%E3%81%AB%E6%9C%AC%E7%95%AA%E3%81%A8%E5%90%8C%E3%81%98%E7%B5%8C%E8%B7%AF%E3%81%A7%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e切替前に「本番と同じ経路」で検証する\u003c/h2\u003e\n\u003cp data-sourcepos=\"365:1-365:99\"\u003e証明書が手に入ると、切替前に本番とまったく同じ経路を検証できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"367:1-367:240\"\u003eやり方は \u003ccode\u003ecurl --resolve\u003c/code\u003e と同じです。接続先IPは新サーバーに固定して、SNIとHostヘッダは本番のドメイン名を使います。Pythonなら標準ライブラリの接続処理を差し替えるだけです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"369:1-379:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e \u003cspan class=\"n\"\u003ehttp.client\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003esocket\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003essl\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003eTARGET_IP\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e（新サーバーのIP）\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePinnedHTTPSConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHTTPSConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\u003cspan class=\"s\"\u003e接続先IPを固定しつつ、SNIとHostヘッダには本来のホスト名を使う\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\"\"\u003c/span\u003e\n    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003econnect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esock\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esocket\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ecreate_connection\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eTARGET_IP\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eport\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etimeout\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n        \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esock\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ewrap_socket\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esock\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eserver_hostname\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ehost\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"381:1-381:129\"\u003eサーバー側から見ると本番アクセスと区別がつきません。これで切替前に全部確認できました。\u003c/p\u003e\n\u003ctable data-sourcepos=\"383:1-389:31\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"383:1-383:25\"\u003e\n\u003cth data-sourcepos=\"383:2-383:15\"\u003e検証項目\u003c/th\u003e\n\u003cth data-sourcepos=\"383:17-383:24\"\u003e結果\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"385:1-385:39\"\u003e\n\u003ctd data-sourcepos=\"385:2-385:26\"\u003e画像6,378ファイル\u003c/td\u003e\n\u003ctd data-sourcepos=\"385:28-385:38\"\u003e全件200\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"386:1-386:79\"\u003e\n\u003ctd data-sourcepos=\"386:2-386:57\"\u003e投稿・メディア・カテゴリ・タグの件数\u003c/td\u003e\n\u003ctd data-sourcepos=\"386:59-386:78\"\u003e移行元と一致\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"387:1-387:66\"\u003e\n\u003ctd data-sourcepos=\"387:2-387:44\"\u003e記事内の装飾要素（2,645箇所）\u003c/td\u003e\n\u003ctd data-sourcepos=\"387:46-387:65\"\u003e移行元と一致\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"388:1-388:36\"\u003e\n\u003ctd data-sourcepos=\"388:2-388:28\"\u003e\n\u003ccode\u003ehttp://\u003c/code\u003e の混在参照\u003c/td\u003e\n\u003ctd data-sourcepos=\"388:30-388:35\"\u003e0件\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"389:1-389:31\"\u003e\n\u003ctd data-sourcepos=\"389:2-389:21\"\u003e証明書の検証\u003c/td\u003e\n\u003ctd data-sourcepos=\"389:23-389:30\"\u003e正常\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"391:1-391:116\"\u003eさらに同じHostヘッダで移行元のIPにも接続すれば、新旧の応答を1対1で比べられます。\u003c/p\u003e\n\u003cp data-sourcepos=\"393:1-393:350\"\u003e検証中、カテゴリページの10件が404を返していて焦りました。移行元にも同じリクエストを投げたところ、移行元でも同じ10件が404 でした。日本語スラッグのカテゴリが元から404になっていた既存の不具合で、移行で壊したものではないと切り分けられました。\u003c/p\u003e\n\u003cp data-sourcepos=\"395:1-395:210\"\u003eこの方法のもう1つの利点は、ファイアウォールを自分のIPだけに絞ったまま検証が終わることです。外部に一度も露出させずに本番相当の確認ができます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"397:1-397:79\"\u003e\n\u003cspan id=\"つまづきポイント4-切替後メールが完全に死んでいた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%A5%E3%81%8D%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%884-%E5%88%87%E6%9B%BF%E5%BE%8C%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%8C%E5%AE%8C%E5%85%A8%E3%81%AB%E6%AD%BB%E3%82%93%E3%81%A7%E3%81%84%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまづきポイント(4) 切替後、メールが完全に死んでいた\u003c/h2\u003e\n\u003cp data-sourcepos=\"399:1-399:165\"\u003e表示は完璧で、画像も広告も検証済みでしたが、数日経ってメール設定を確認したところ、以下のようになっていました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"401:1-404:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003esh: 1: /usr/sbin/sendmail: not found\nwp_mail() の戻り値: false（送信失敗）\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"406:1-406:188\"\u003e\u003cstrong\u003eLightsailのWordPressイメージにはメール送信の仕組み（MTA）が入っていません。\u003c/strong\u003e つまり切替以降、次のメールが全部届いていませんでした。\u003c/p\u003e\n\u003cul data-sourcepos=\"408:1-411:0\"\u003e\n\u003cli data-sourcepos=\"408:1-408:41\"\u003eお問い合わせフォームの通知\u003c/li\u003e\n\u003cli data-sourcepos=\"409:1-409:20\"\u003eコメント通知\u003c/li\u003e\n\u003cli data-sourcepos=\"410:1-411:0\"\u003eパスワードリセットメール\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"412:1-412:204\"\u003e共用レンタルサーバーでは当然送れていたので、これは移行による機能の喪失です。しかも画面には何も出ません。サイトはまったく正常に見えます。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"414:1-418:3\" class=\"note alert\"\u003e\n\u003cspan class=\"fa fa-fw fa-times-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"415:1-417:119\"\u003e移行後のチェックリストに「メールが送れるか」を必ず入れてください。\u003cbr\u003e\n表示・画像・広告をどれだけ検証しても、メールは画面に出ないので気づけません。\u003cbr\u003e\n\u003ccode\u003ewp_mail()\u003c/code\u003e の戻り値と、エラー内容（\u003ccode\u003e$phpmailer-\u0026gt;ErrorInfo\u003c/code\u003e）の両方を確認するのが確実です。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"420:1-420:245\"\u003e紛らわしいのは、PHPの設定には \u003ccode\u003esendmail_path\u003c/code\u003e が入っていることです。設定を眺めただけでは送れそうに見えます。実体が存在しないだけなので、実際に送ってみないと分かりません。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"422:1-422:29\"\u003e\n\u003cspan id=\"amazon-sesで復旧する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#amazon-ses%E3%81%A7%E5%BE%A9%E6%97%A7%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eAmazon SESで復旧する\u003c/h3\u003e\n\u003cp data-sourcepos=\"424:1-424:180\"\u003eAWS上のサーバーは25番ポートが既定で制限されているので、自前でメールサーバーを立てても外に出せません。Amazon SESを使いました。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"426:1-430:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"427:1-429:216\"\u003e\u003cstrong\u003eAmazon SESって何？\u003c/strong\u003e\u003cbr\u003e\nAWSのメール配信サービスです。すごく雑に言うと「メールを送る部分だけを借りる」仕組みです。\u003cbr\u003e\n自分でメールサーバーを立てると、迷惑メール判定されない設定や、送信の失敗処理まで全部面倒を見る必要があります。SESはその部分を引き受けてくれます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"432:1-432:64\"\u003e\n\u003cspan id=\"送信制限の解除は本当に必要か先に確認する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%80%81%E4%BF%A1%E5%88%B6%E9%99%90%E3%81%AE%E8%A7%A3%E9%99%A4%E3%81%AF%E6%9C%AC%E5%BD%93%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%8B%E5%85%88%E3%81%AB%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e送信制限の解除は本当に必要か先に確認する\u003c/h3\u003e\n\u003cp data-sourcepos=\"434:1-434:256\"\u003eSESは初期状態だと「検証済みのアドレス宛にしか送れない」制限があります（1日200通まで）。解除には申請と待ち時間が必要なので、条件反射で申請する前に必要かどうかを確認しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"436:1-436:84\"\u003e判断基準は「任意のアドレスへ送る必要があるか」だけです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"438:1-443:82\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"438:1-438:46\"\u003e\n\u003cth data-sourcepos=\"438:2-438:9\"\u003e用途\u003c/th\u003e\n\u003cth data-sourcepos=\"438:11-438:18\"\u003e宛先\u003c/th\u003e\n\u003cth data-sourcepos=\"438:20-438:45\"\u003e制限下で足りるか\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"440:1-440:67\"\u003e\n\u003ctd data-sourcepos=\"440:2-440:30\"\u003eお問い合わせの通知\u003c/td\u003e\n\u003ctd data-sourcepos=\"440:32-440:54\"\u003e管理者（固定）\u003c/td\u003e\n\u003ctd data-sourcepos=\"440:56-440:66\"\u003e足りる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"441:1-441:58\"\u003e\n\u003ctd data-sourcepos=\"441:2-441:21\"\u003eコメント通知\u003c/td\u003e\n\u003ctd data-sourcepos=\"441:23-441:45\"\u003e管理者（固定）\u003c/td\u003e\n\u003ctd data-sourcepos=\"441:47-441:57\"\u003e足りる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"442:1-442:61\"\u003e\n\u003ctd data-sourcepos=\"442:2-442:30\"\u003eパスワードリセット\u003c/td\u003e\n\u003ctd data-sourcepos=\"442:32-442:48\"\u003e管理者自身\u003c/td\u003e\n\u003ctd data-sourcepos=\"442:50-442:60\"\u003e足りる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"443:1-443:82\"\u003e\n\u003ctd data-sourcepos=\"443:2-443:42\"\u003eフォーム送信者への自動返信\u003c/td\u003e\n\u003ctd data-sourcepos=\"443:44-443:66\"\u003e訪問者（任意）\u003c/td\u003e\n\u003ctd data-sourcepos=\"443:68-443:81\"\u003e足りない\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"445:1-445:272\"\u003eお問い合わせフォームの設定を確認すると、通知先は管理者のアドレス固定で、フォーム入力値が入るのは \u003ccode\u003eReply-To\u003c/code\u003e だけでした。\u003ccode\u003eReply-To\u003c/code\u003e は送信先ではないので、制限のままで足りると判断できました。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"447:1-447:52\"\u003e\n\u003cspan id=\"送信元は必ず自分のドメインにする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%80%81%E4%BF%A1%E5%85%83%E3%81%AF%E5%BF%85%E3%81%9A%E8%87%AA%E5%88%86%E3%81%AE%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e送信元は必ず自分のドメインにする\u003c/h3\u003e\n\u003cp data-sourcepos=\"449:1-449:285\"\u003e元の設定では送信元が管理者の個人メールアドレス（フリーメール）になっていました。これをそのままSESから送ると、他人のドメインを自分のサーバーから送る形になり、迷惑メール判定されやすくなります。\u003c/p\u003e\n\u003cp data-sourcepos=\"451:1-451:227\"\u003e送信元を \u003ccode\u003ewordpress@（自分のドメイン）\u003c/code\u003e に変えて、DKIM署名とSPFが整合するようにしました。\u003ccode\u003eReply-To\u003c/code\u003e はフォーム入力値のまま残るので、受信後にそのまま返信できます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"453:1-453:89\"\u003e\n\u003cspan id=\"つまづきポイント5-iamポリシーを絞りすぎて送信が拒否される\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%BE%E3%81%A5%E3%81%8D%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%885-iam%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%E3%82%92%E7%B5%9E%E3%82%8A%E3%81%99%E3%81%8E%E3%81%A6%E9%80%81%E4%BF%A1%E3%81%8C%E6%8B%92%E5%90%A6%E3%81%95%E3%82%8C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eつまづきポイント(5) IAMポリシーを絞りすぎて送信が拒否される\u003c/h3\u003e\n\u003cp data-sourcepos=\"455:1-455:129\"\u003eSESを使うためのIAMポリシーで、「最小権限にしよう」として送信元だけを対象に指定しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"457:1-463:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Effect\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"Allow\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Action\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"ses:SendRawEmail\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Resource\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"arn:aws:ses:ap-northeast-1:（アカウントID）:identity/（自分のドメイン）\"\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"465:1-465:42\"\u003e実際に送ると拒否されました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"467:1-471:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eSMTP Error: data not accepted. SMTP server error: DATA END command failed\nDetail: Access denied: User '...' is not authorized to perform 'ses:SendRawEmail'\non resource '...identity/（宛先のアドレス）'\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"473:1-473:234\"\u003eSESは送信元だけでなく宛先も権限評価の対象にします。送信制限がかかっている状態では宛先アドレスも登録済みの識別子として扱われるので、そこへの権限が必要でした。\u003c/p\u003e\n\u003cp data-sourcepos=\"475:1-475:135\"\u003e修正版のポリシーがこちらです。対象を広げる代わりに、条件で送信元アドレスを固定しています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"477:1-486:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Effect\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"Allow\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Action\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"ses:SendRawEmail\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"ses:SendEmail\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Resource\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"*\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"Condition\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"StringEquals\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"ses:FromAddress\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"wordpress@（自分のドメイン）\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"488:1-488:230\"\u003e対象が \u003ccode\u003e*\u003c/code\u003e でも、条件で送信元が固定されているので他のアドレスを騙って送ることはできません。絞り込みは対象リソースではなく条件で行うのがSESの正しい形でした。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"490:1-493:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"491:1-492:120\"\u003eIAMポリシーを絞ったら、必ず実際の操作で確認してください。\u003cbr\u003e\nポリシーの作成が成功したことは、その権限で目的の操作ができることを意味しません。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"495:1-495:52\"\u003e\n\u003cspan id=\"認証情報はデータベースに置かない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%81%AF%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AB%E7%BD%AE%E3%81%8B%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e認証情報はデータベースに置かない\u003c/h3\u003e\n\u003cp data-sourcepos=\"497:1-497:234\"\u003eSESのSMTP認証情報を設定するとき、プラグインの管理画面から入れるとデータベースに平文で保存されます。DBのバックアップを取るたびに秘密が複製されることになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"499:1-499:60\"\u003e\u003ccode\u003ewp-config.php\u003c/code\u003e に定数で書けばDBに入りません。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"php\" data-sourcepos=\"501:1-512:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_ON'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_MAILER'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'smtp'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_MAIL_FROM'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'wordpress@（自分のドメイン）'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_MAIL_FROM_FORCE'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SMTP_HOST'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'email-smtp.ap-northeast-1.amazonaws.com'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SMTP_PORT'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e587\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SSL'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'tls'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SMTP_AUTH'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SMTP_USER'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'（アクセスキーID）'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"nb\"\u003edefine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e \u003cspan class=\"s1\"\u003e'WPMS_SMTP_PASS'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'（SMTPパスワード）'\u003c/span\u003e \u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"514:1-514:203\"\u003e\u003ccode\u003eWPMS_MAIL_FROM_FORCE\u003c/code\u003e を有効にするのは必須でした。IAMポリシー側で送信元アドレスを固定しているので、送信元が揃っていないと権限エラーになります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"516:1-516:67\"\u003e\n\u003cspan id=\"紛らわしいポイント-ファイアウォールの全開放\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B4%9B%E3%82%89%E3%82%8F%E3%81%97%E3%81%84%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88-%E3%83%95%E3%82%A1%E3%82%A4%E3%82%A2%E3%82%A6%E3%82%A9%E3%83%BC%E3%83%AB%E3%81%AE%E5%85%A8%E9%96%8B%E6%94%BE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e紛らわしいポイント ファイアウォールの全開放\u003c/h2\u003e\n\u003cp data-sourcepos=\"518:1-518:145\"\u003e切替のときにファイアウォールを全開放しようとして、素直に \u003ccode\u003e0.0.0.0/0\u003c/code\u003e を指定したらエラーになりました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"520:1-523:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eInvalidInputException: You cannot specify a CIDR or a CIDR list alias together\nwith \"0.0.0.0/0\". Specifying \"0.0.0.0/0\" allows all CIDRs and CIDR list aliases.\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"525:1-525:203\"\u003eLightsailでは「許可するIPを指定しない」ことが全許可という仕様です。既存のルールに特定のIPが入っていると \u003ccode\u003e0.0.0.0/0\u003c/code\u003e との併記になって弾かれます。\u003c/p\u003e\n\u003cp data-sourcepos=\"527:1-527:95\"\u003e一度ルールを削除してから、IPを指定せずに開き直すのが正解でした。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"529:1-529:69\"\u003e\n\u003cspan id=\"検証で気づいた共用サーバーが持っていたもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%A4%9C%E8%A8%BC%E3%81%A7%E6%B0%97%E3%81%A5%E3%81%84%E3%81%9F%E5%85%B1%E7%94%A8%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E6%8C%81%E3%81%A3%E3%81%A6%E3%81%84%E3%81%9F%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e検証で気づいた、共用サーバーが持っていたもの\u003c/h2\u003e\n\u003cp data-sourcepos=\"531:1-531:207\"\u003e移行元と移行先のレスポンスヘッダを、同じHostヘッダで接続先IPだけ変えて比べてみました。表示が同じでも中身が違うかもしれないと思ったからです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"533:1-538:49\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"533:1-533:76\"\u003e\n\u003cth data-sourcepos=\"533:2-533:12\"\u003eヘッダ\u003c/th\u003e\n\u003cth data-sourcepos=\"533:14-533:48\"\u003e移行元（共用サーバー）\u003c/th\u003e\n\u003cth data-sourcepos=\"533:50-533:75\"\u003e移行先（Lightsail）\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"535:1-535:60\"\u003e\n\u003ctd data-sourcepos=\"535:2-535:11\"\u003e\u003ccode\u003eserver\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"535:13-535:32\"\u003e出していない\u003c/td\u003e\n\u003ctd data-sourcepos=\"535:34-535:59\"\u003e\u003ccode\u003eApache/2.4.68 (Debian)\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"536:1-536:38\"\u003e\n\u003ctd data-sourcepos=\"536:2-536:19\"\u003e\u003ccode\u003ex-cache-status\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"536:21-536:28\"\u003eあり\u003c/td\u003e\n\u003ctd data-sourcepos=\"536:30-536:37\"\u003eなし\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"537:1-537:39\"\u003e\n\u003ctd data-sourcepos=\"537:2-537:20\"\u003e\u003ccode\u003ex-mod-pagespeed\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"537:22-537:29\"\u003eあり\u003c/td\u003e\n\u003ctd data-sourcepos=\"537:31-537:38\"\u003eなし\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"538:1-538:49\"\u003e\n\u003ctd data-sourcepos=\"538:2-538:30\"\u003e\u003ccode\u003estrict-transport-security\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"538:32-538:39\"\u003eなし\u003c/td\u003e\n\u003ctd data-sourcepos=\"538:41-538:48\"\u003eなし\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"540:1-540:67\"\u003e検証を通じて明らかになったこと2つありました。\u003c/p\u003e\n\u003cp data-sourcepos=\"542:1-542:207\"\u003e１つ目は、Lightsailがサーバーのバージョンまで出していたことです。移行元は隠していたので、これは移行による劣化です。設定を追加して隠しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"apache\" data-sourcepos=\"544:1-547:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"nc\"\u003eServerTokens\u003c/span\u003e Prod\n\u003cspan class=\"nc\"\u003eServerSignature\u003c/span\u003e \u003cspan class=\"ss\"\u003eOff\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"549:1-549:367\"\u003e２つ目は、共用サーバーがサーバー側のページキャッシュと自動最適化を裏で効かせていたことです。移行先にはこれがないので、毎回PHPが動きます。CPU2個・メモリ2GBなので通常のアクセスでは問題ありませんが、共用サーバーが面倒を見てくれていた部分が見えました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"551:1-551:18\"\u003e\n\u003cspan id=\"かかる費用\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8B%E3%81%8B%E3%82%8B%E8%B2%BB%E7%94%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eかかる費用\u003c/h2\u003e\n\u003cp data-sourcepos=\"553:1-553:44\"\u003e料金は2026年9月時点のものです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"555:1-560:36\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"555:1-555:19\"\u003e\n\u003cth data-sourcepos=\"555:2-555:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"555:11-555:18\"\u003e単価\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"557:1-557:82\"\u003e\n\u003ctd data-sourcepos=\"557:2-557:71\"\u003eLightsail（CPU2個・メモリ2GB・ディスク60GB・転送3TB）\u003c/td\u003e\n\u003ctd data-sourcepos=\"557:73-557:81\"\u003e月 $12\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"558:1-558:50\"\u003e\n\u003ctd data-sourcepos=\"558:2-558:37\"\u003eAmazon Route 53 ホストゾーン\u003c/td\u003e\n\u003ctd data-sourcepos=\"558:39-558:49\"\u003e月 $0.50\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"559:1-559:47\"\u003e\n\u003ctd data-sourcepos=\"559:2-559:31\"\u003eAmazon SES（月数十通）\u003c/td\u003e\n\u003ctd data-sourcepos=\"559:33-559:46\"\u003e$0.01 未満\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"560:1-560:36\"\u003e\n\u003ctd data-sourcepos=\"560:2-560:26\"\u003eLet's Encrypt 証明書\u003c/td\u003e\n\u003ctd data-sourcepos=\"560:28-560:35\"\u003e無料\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"562:1-562:77\"\u003e合計で月 $12.5 ほど、1ドル150円なら年間で約22,500円です。\u003c/p\u003e\n\u003cp data-sourcepos=\"564:1-564:145\"\u003e正直、\u003cstrong\u003e移行元の共用レンタルサーバーは年13,200円のため、金額だけ見ると、移行して高くなっています。\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"566:1-566:232\"\u003e今回はAWSのクレジットがあるので自己負担は発生していませんが、「安くするための移行」ではありませんでした。共用レンタルサーバーの年1万円台は、かなり安いです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"568:1-568:24\"\u003e\n\u003cspan id=\"やってみた感想\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E6%84%9F%E6%83%B3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eやってみた感想\u003c/h2\u003e\n\u003cp data-sourcepos=\"570:1-570:105\"\u003e一番良かったのは、中で何が起きているか全部見えるようになったことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"572:1-572:312\"\u003e共用サーバーだと、画像が表示されない理由を調べようとしてもログに手が届きません。今回は6,378ファイルのパスを1つずつ突き合わせて、640ファイルがNFD形だと数えられました。前回の移行で諦めた原因も、無事に解決しました。\u003c/p\u003e\n\u003cp data-sourcepos=\"574:1-574:309\"\u003e同時に、共用レンタルサーバーがどれだけ面倒を見てくれていたかも分かりました。メールが送れるのは当たり前ではありませんし、ページキャッシュも勝手に効いていました。移行して初めて、無くなったものに気づきました。\u003c/p\u003e\n\u003cp data-sourcepos=\"576:1-577:129\"\u003e自分で組む価値があるのは、たぶんこういう人だと思います。\u003cbr\u003e\n中で何が起きているかを把握したい人。AWSの勉強も兼ねたい人。AWSクレジットが余っている人。\u003c/p\u003e\n\u003cp data-sourcepos=\"579:1-579:200\"\u003e逆に「ブログを書くことに集中したい」なら、共用レンタルサーバーのままが幸せです。月1,000円ちょっとで、メールもキャッシュも付いてきます。\u003c/p\u003e\n\u003cp data-sourcepos=\"581:1-581:294\"\u003eもし同じ移行をやるなら、順番だけは守ってほしいです。\u003cstrong\u003e小さいサイトで試す、切替前に本番と同じ経路で検証する、切替後にメールを確認する。\u003c/strong\u003e この3つを飛ばすと、私が踏んだ穴をそのまま踏むことになります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"583:1-583:18\"\u003e\n\u003cspan id=\"参考リンク\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考リンク\u003c/h2\u003e\n\u003cul data-sourcepos=\"585:1-592:116\"\u003e\n\u003cli data-sourcepos=\"585:1-585:73\"\u003e\u003ca href=\"https://aws.amazon.com/jp/lightsail/pricing/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAmazon Lightsail 料金\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"586:1-586:190\"\u003e\u003ca href=\"https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-tutorial-launching-and-configuring-wordpress.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eLightsail の WordPress インスタンスを起動する\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"587:1-587:177\"\u003e\u003ca href=\"https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-bitnami-blueprint-deprecation.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eBitnami 版ブループリントの提供終了について\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"588:1-588:93\"\u003e\u003ca href=\"https://eff-certbot.readthedocs.io/en/stable/using.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ecertbot のユーザーガイド\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"589:1-589:122\"\u003e\u003ca href=\"https://docs.aws.amazon.com/ja_jp/ses/latest/dg/smtp-credentials.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAmazon SES の SMTP 認証情報を取得する\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"590:1-590:134\"\u003e\u003ca href=\"https://docs.aws.amazon.com/ja_jp/ses/latest/dg/request-production-access.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAmazon SES のサンドボックス外への移動\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"591:1-591:86\"\u003e\u003ca href=\"https://unicode.org/reports/tr15/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eUnicode Normalization Forms（Unicode 公式）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"592:1-592:116\"\u003e\u003ca href=\"https://wordpress.org/documentation/article/moving-wordpress/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eWordPress の移行（公式ドキュメント）\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## 概要\n\n:::note warn\nこの記事は生成AIで作成した記事を人間により加筆修正しています。\n:::\n\n「レンタルサーバーの期限まであと1か月、今度はちゃんと移行したい」\n\n数年書いてきた個人ブログを、共用レンタルサーバーで動かしていました。レンタルサーバー代は年間1万円以上かかっていました。\n\n以前Amazon Q Developer時代にAWSへの移行に挑戦してみましたが、記事の画像が抜け落ち、レイアウトが崩れてしまい、結局原因が分からないまま移行を断念しました。\n「またあれをやるのか」と気が重かったのですが、レンタルサーバーの更新期限（支払日）が迫っているので今度は原因から解決していくことにしました。\n\nそして、無事に移行できました！\n369記事、画像6,378ファイル、広告の配信まで含めて、崩れずに引っ越せました！\n\n前回の失敗の原因は **日本語ファイル名のUnicode正規化** でした。この原因に気づくまでに時間がかかりました💦\n他にもパーマリンクが全ページ404になったり、DBを一度壊したり、切替後にメールが完全に死んでいたことに気づいたりしました。同じことをやろうとする人が同じ穴に落ちないように、詰まったところを中心に書いていきます。\n\n:::note info\n**Amazon Lightsailって何？**\nAWSが提供している、月額固定のサーバーサービスです。すごく雑に言うと「AWSが用意してくれた、値段が分かりやすいレンタルサーバー」です。\n通常のAWS（EC2など）は使った分だけ課金されるので事前に金額が読みにくいのですが、Lightsailは「月$12でCPU2個・メモリ2GB・ディスク60GB・転送3TB」のように定額です。WordPress入りのイメージが用意されているので、作成した瞬間からブログが動きます。\n:::\n\n## やりたかったこと\n\n図にするとこれだけです。\n\n```mermaid\nflowchart LR\n    A[\"共用レンタルサーバー\u003cbr/\u003eWordPress\"] --\u003e|\"記事・画像・DB\"| B[\"Amazon Lightsail\u003cbr/\u003eWordPress\"]\n    C[\"訪問者\"] -.-\u003e|\"切替前\"| A\n    C -.-\u003e|\"切替後\"| B\n```\n\nやることは記事とデータを移して、見ている先を切り替えるだけです。ただ、このブログには広告を貼っていてわずかにアドセンス収益が出ているので、表示が崩れたり画像が消えたりすると直接損をします。そこが気を遣うところでした。\n\n移行対象はこれくらいの規模です。\n\n| 項目 | 数量 |\n| --- | --- |\n| 投稿記事 | 369 |\n| メディア（画像） | 922 |\n| 画像の実ファイル（サムネイル含む） | 6,378 |\n| インストール済みプラグイン | 35（有効29） |\n| データベース | 72MB |\n| アップロード済みファイル | 261MB |\n\n## 移行方法を比べる\n\nWordPressの移行方法は大きく3つあります。\n\n| 方法 | 手間 | 失敗しにくさ | 向いている人 |\n| --- | --- | --- | --- |\n| 移行プラグイン（All-in-One WP Migration など） | 小 | 中 | サイトが小さい人 |\n| DBダンプ + FTPでファイル直取得 | 中 | 高 | サイトが大きい人、原因を追いたい人 |\n| 記事のエクスポート機能で移す | 大 | 低 | 記事だけ移せばよい人 |\n\n最初はボタン一発で済む移行プラグインを使うつもりでしたが、実際に試してみると、エクスポートしたファイルのサイズが90MBを超えて、ブラウザ経由のアップロードが不安定でした。しかも移行元のサーバーではダウンロードそのものが500エラーになりました。\n\n結局 DBダンプ + FTPでファイルを直接取る方法にしました。手間は増えますが、どのファイルがどこへ行ったかを自分で確認できます。前回失敗したため、分からない原因をできるだけ減らしたかったためです。\n\n## 前回の失敗の原因は、日本語ファイル名の正規化だった\n\nここが今回一番の山場でした。\n\n移行元のサイトを調べていて、同じ画像に対して2種類のURLが存在することに気づきました。見た目はまったく同じ文字列なのに、バイト列が違います。\n\n```text\nぺいんとicon.jpg   ← パターンA\nぺいんとicon.jpg   ← パターンB（見た目は同じ）\n```\n\n原因はUnicodeの正規化形の違いでした。\n\n:::note info\n**Unicode正規化（NFC / NFD）って何？**\n「が」のような濁点付きの文字には、コンピュータ上の表現方法が2通りあります。\n\n- **NFC** … 「が」を1文字として持つ\n- **NFD** … 「か」+「濁点」の2文字として持つ\n\n画面に表示すると、どちらも「が」に見えます。人間の目には区別がつきません。\nところがファイルシステムやWebサーバーにとっては、まったく別のファイル名です。\nmacOSは伝統的にNFDを使い、WindowsやLinuxはNFCを使います。\n:::\n\nつまりこういうことが起きていました。\n\n```mermaid\nflowchart TD\n    A[\"Macから画像をアップロード\"] --\u003e B[\"NFD形のファイル名で保存される\"]\n    C[\"WindowsやブラウザからアップロードNFC形\"] --\u003e D[\"NFC形のファイル名で保存される\"]\n    B --\u003e E[\"同じサイトの中に\u003cbr/\u003e2種類の正規化形が混在\"]\n    D --\u003e E\n    E --\u003e F{\"移行ツールが\u003cbr/\u003eファイル名を変換すると\"}\n    F --\u003e|\"NFDをNFCに正規化\"| X[\"DBが参照するパスと\u003cbr/\u003e実ファイル名が一致せず404\"]\n    F --\u003e|\"バイト列のまま転送\"| G[\"正しく表示される\"]\n```\n\n移行元のサイトを実測すると、DBが参照している6,378パスのうち 640ファイルがNFD形でした。前回の移行では、このNFD形のファイルが転送中にNFCへ変換され、DBから見て「存在しないファイル」になっていました。\n\n:::note alert\nこの問題は転送ツールの設定で起きます。ファイル名を勝手に正規化するツールを使うと、日本語ファイル名の画像だけが静かに消えます。\nしかも消えるのは一部のファイルだけなので、トップページを見ただけでは気づきません。古い記事を開いて初めて気づきます。\n:::\n\n対策はファイル転送時に文字コードの変換をさせないようにします。今回は `lftp` を使い、次の2行を指定しました。\n\n```bash\n# 文字コードの自動変換を無効にする。これがないとファイル名が正規化されてしまう\nset ftp:charset \"\"\nset file:charset \"\"\n```\n\n転送後、DBが参照する6,378パスすべてについて、実ファイルが存在するかをスクリプトで突き合わせました。\n\n| 正規化形 | ファイル数 | 一致 |\n| --- | --- | --- |\n| ASCIIのみ | 4,034 | 全件 |\n| NFC形 | 1,704 | 全件 |\n| NFD形 | **640** | **全件** |\n\n全件バイト一致を確認してから次の工程に進みました。\n\n## Lightsailのイメージ選びで1つ罠がある\n\nLightsailでWordPressを作るとき、イメージ（ブループリント）を選びます。ここで名前の似た2つが並んでいます。\n\n| イメージ | 中身 | 状態 |\n| --- | --- | --- |\n| WordPress | Bitnami版 | **2026年5月に更新終了。2026年11月以降は新規作成できない** |\n| WordPress（Lightsail版） | AWS製 | 現行 |\n\n長年の情報の多くはBitnami版向けに書かれています。ディレクトリ構成もコマンドも違うので、記事を参考にするときは注意が必要です。今回は現行のLightsail版を選びました。\n\nLightsail版の構成を実際に確認すると以下のようになっていました。\n\n| 項目 | 値 |\n| --- | --- |\n| OS | Debian 12 |\n| Webサーバー | Apache 2.4 + mod_php |\n| PHP | 8.2 |\n| データベース | MariaDB 10.11 |\n| WordPressの場所 | `/var/www/html` |\n| wp-config.php の場所 | `/var/www/wp-config.php` |\n\n移行元のPHPは7.2でした。PHP 7.2 から 8.2 への飛躍があるので、古いプラグインが動かない覚悟が必要です。\n\n## 全体の仕組み\n\nできあがった構成はこうなりました。\n\n```mermaid\nflowchart TD\n    U[\"訪問者\"] --\u003e R53\n\n    subgraph \"ドメイン管理\"\n        REG[\"ドメイン登録業者\u003cbr/\u003eネームサーバーだけ変更\"]\n        R53[\"Amazon Route 53\u003cbr/\u003eDNSレコードを管理\"]\n    end\n\n    subgraph \"AWS\"\n        LS[\"Lightsail\u003cbr/\u003eWordPress + Apache + MariaDB\"]\n        SES[\"Amazon SES\u003cbr/\u003eメール送信\"]\n    end\n\n    REG -.-\u003e|\"委任\"| R53\n    R53 --\u003e LS\n    LS --\u003e SES\n    SES --\u003e M[\"管理者のメールボックス\"]\n```\n\nドメインの登録先は変えていません。ネームサーバーをRoute 53に向けただけです。移管はそれ自体がリスクなので、DNSの管理権だけを移しました。\n\n## 小さいサイトで先に試す\n\n本番のブログは369記事あります。これをいきなり移行するのは怖かったので、同じサーバーで動かしていた記事28本の小さいサイトで先に通しました。\n\n予備検証の段階で、次の問題が新たに出ました。\n\n- パーマリンクが全ページ404になる\n- プラグインの一斉有効化でサイトが落ちる\n- DBを壊しかける\n\n検証が終わったらインスタンスは削除して、本番用を手順書どおりに作り直しました。設定を試行錯誤で汚した環境をそのまま本番にすると、何が効いているのか分からなくなります。\n\n## つまづきポイント(1) パーマリンクが全ページ404になる\n\nトップページは表示されるのに、記事を開くと全部404になりました。\n\n原因はDebianの既定設定でした。`/var/www/` に対して `.htaccess` を読まない設定になっています。\n\n```apache\n# Debianの既定（/etc/apache2/apache2.conf）\n\u003cDirectory /var/www/\u003e\n    AllowOverride None\n\u003c/Directory\u003e\n```\n\nWordPressのパーマリンクは `.htaccess` の書き換えルールで動いているので、これが読まれないと記事URLが解決できません。\n\n設定ファイルを追加して解決しました。\n\n```apache\n\u003cDirectory /var/www/html\u003e\n    Options FollowSymLinks\n    AllowOverride All\n    Require all granted\n\u003c/Directory\u003e\n```\n\n:::note warn\n`.htaccess` ファイル自体も、移行元からコピーせず新規に作り直しました。移行元の `.htaccess` には、そのレンタルサーバー固有の記述（独自のキャッシュ制御やアクセス制限）が入っていて、別の環境では意味がないどころか動作を壊します。\n:::\n\n## つまづきポイント(2) 触ってはいけないファイルを動かしてDBを壊す\n\nLightsail版には `/opt/aws/wordpress/credentials.log` というファイルがあります。初期パスワードが書かれているので、読み終わったら消すか移動したくなります。\n\nこれを移動したらサイトがDB接続エラーになりました。\n\n`wp-config.php` を読むと、実行時にこのファイルを読んでDBパスワードを取り出す作りになっていたのです。\n\n:::note alert\n`/opt/aws/wordpress/credentials.log` は削除も移動もしないでください。設定ファイルが実行時に参照しているので、無くなるとサイトが落ちます。\n「パスワードが平文で置いてあるのは気持ち悪い」と思っても、触るのは仕組みを確認してからにしてください。\n:::\n\n## つまづきポイント(3) プラグインを一斉に有効化してサイトが500になる\n\nファイルを移してDBを入れ、プラグインをまとめて有効化したところ、サイト全体が500エラーになりました。\n\nエラーログを見ると原因は1つのプラグインでした。\n\n```text\nPHP Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported.\n  in .../vendor/twig/twig/lib/Twig/Node.php on line 42\n```\n\n三項演算子の書き方がPHP 8で禁止されたため、構文エラーで落ちていました。PHP 7.2で動いていた古いプラグインが8.2で死ぬ、という典型例です。\n\n最終的にPHP 8.2で動かなかったのは4つでした。\n\n| プラグイン | 症状 | 対応 |\n| --- | --- | --- |\n| SNSシェア系 | Fatal error | 停止。テーマの標準機能で代替 |\n| カテゴリ一括変更 | Warning | 停止。管理用なので表示に影響なし |\n| 外部画像取り込み | Warning | 停止 |\n| 記事マップ表示 | Warning | 停止 |\n\n有効29個のうち23個が動き、Fatal errorもWarningも0件の状態に落ち着きました。\n\n一番心配していたのは、広告を PHPコードを直接書けるウィジェットで表示していた箇所です。2010年代のプラグインなので8.2で動く保証がありません。これは無事に動きました。\n\n:::note warn\nプラグインを移すときは、一斉に有効化せず DBの `active_plugins` を空にした状態から1つずつ有効化してください。まとめて有効化すると、どれが原因で落ちたのか分からなくなります。\n:::\n\n## DNSの切替は2段階に分ける\n\nここからが本番切替です。いきなりやると問題の切り分けができないので、2段階に分けました。\n\n```mermaid\nflowchart TD\n    S[\"開始\"] --\u003e P1[\"フェーズ1\u003cbr/\u003eネームサーバーをRoute 53へ\"]\n    P1 --\u003e V1{\"レコードの内容は\u003cbr/\u003e移行元と同一か\"}\n    V1 --\u003e|\"同一\"| W[\"訪問者は従来のサーバーを見たまま\"]\n    W --\u003e P2[\"フェーズ2\u003cbr/\u003eAレコードを新サーバーへ\"]\n    P2 --\u003e V2{\"表示・画像・広告は\u003cbr/\u003e正常か\"}\n    V2 --\u003e|\"異常\"| RB[\"Aレコードを戻す\u003cbr/\u003e約1分で復帰\"]\n    V2 --\u003e|\"正常\"| DONE[\"切替完了\"]\n```\n\nフェーズ1では、移行元とまったく同じ内容のレコードをRoute 53に作ってから委任を切り替えます。訪問者から見た挙動は何も変わりません。ここでDNSの移管だけを確実に終わらせます。\n\nこのとき見落としやすいのがワイルドカードレコードです。移行元には `*` のAレコードがあり、これを写し忘れると一部のサブドメインが解決できなくなります。移行前のレコードは1つずつ実測して書き写しました。\n\nフェーズ2でAレコードを新サーバーに向けます。TTLを60秒にしておいたので、反映も巻き戻しも1分で済みます。実測ではこうなりました。\n\n| リゾルバ | 反映までの時間 |\n| --- | --- |\n| 権威サーバー | 約1分 |\n| Google Public DNS | 約1分 |\n| Quad9 / OpenDNS | 約1分 |\n| Cloudflare | 約4分 |\n\n## HTTPS証明書は切替前に取る\n\nここは事前に考えておかないと、証明書エラーの時間帯ができます。\n\nLet's Encryptの証明書を取る方法は主に2つあります。\n\n| 認証方式 | 仕組み | 切替前に取れるか |\n| --- | --- | --- |\n| HTTP-01 | Webサーバーにファイルを置いて認証局に読ませる | **取れない** |\n| DNS-01 | DNSにTXTレコードを置いて認証局に読ませる | 取れる |\n\nHTTP-01が使えないのは、認証局がAレコードを引いて接続してくるためです。切替前はAレコードが古いサーバーを指しているので、認証のリクエストは古いサーバーに届いてしまいます。\n\nつまり切替前に証明書を取るならDNS-01しかありません。\n\n### IAMユーザーを作らずにDNS-01を通す\n\ncertbotにはRoute 53を自動操作するプラグインがあります。ただしLightsailのインスタンスには IAMロールを割り当てられないので、アクセスキーを持つIAMユーザーが必要になります。\n\n「取得したら鍵を消せば安全」と考えたのですが、うまくいきませんでした。鍵を消した時点で証明書の自動更新も止まります。ということで、最初から鍵を作らない方法を採用しました。\n\ncertbotの `--manual-auth-hook` は「TXTレコードを登録する処理」を書く場所ですが、「登録されるのを待つ処理」を書いても動きます。\n\n```bash\n#!/bin/bash\n# certbot が CERTBOT_DOMAIN と CERTBOT_VALIDATION を環境変数で渡してくる\n# このフックはRoute 53を操作しない。要求された値が見えるまで権威サーバーに問い合わせて待つ\nset -uo pipefail\nD=\"${CERTBOT_DOMAIN:-}\"\nV=\"${CERTBOT_VALIDATION:-}\"\n\necho \"REQ $(date -Is) _acme-challenge.${D} ${V}\" \u003e\u003e /home/admin/acme/requests.txt\n\n# 最大15分待つ\nfor i in $(seq 1 90); do\n  if dig +short +time=5 +tries=1 TXT \"_acme-challenge.${D}\" @ns-xxxx.awsdns-xx.org \\\n      | tr -d '\"' | grep -qx \"$V\"; then\n    sleep 10\n    exit 0\n  fi\n  sleep 10\ndone\nexit 1\n```\n\nフックが要求をファイルに書いて待っている間に、別の場所からRoute 53のAPIでTXTレコードを登録します。値が見えたらフックが `exit 0` して、certbotが認証に進みます。これでサーバー上にAWSの認証情報を1つも置かずに証明書が取れました。\n\n実測では、TXTレコードを登録してから権威サーバーに見えるまで1〜2分でした。\n\n:::note info\n**本番の前に `--dry-run` で通す**\ncertbotの `--dry-run` はテスト環境を使うので、本番の発行制限を消費しません。しかも認証フックは実際に実行されるので、待機ロジックをそのままリハーサルできます。\n1回試してから本番を叩くと安心です。\n:::\n\n### 更新方式は切替後に変える\n\n初回はDNS-01で取りましたが、この設定のままだと自動更新のたびに人間を待つ状態になります。切替が終わればAレコードが新サーバーを指すのでHTTP-01が使えます。設定ファイルを書き換えて、追加の発行なしで自動更新に移しました。\n\nここで1つ失敗があったので、注意です。設定ファイルにセクションをファイルの途中に挿入したため、後続の行がそのセクションの中身として読まれて壊れました。\n\n```text\nFailed to renew certificate with error: Couldn't create root for server http-01\nchallenge responses: No such file or directory: 'https:/acme-v02.api.letsencrypt.org'\n```\n\nURLをディレクトリとして開こうとしています。INI形式なので、セクション見出し以降の行は全部そのセクションに属します。結果として `server = https://...` が「ドメイン名 server の設定」として解釈されていました。\n\nそもそもそのセクションは不要だったので、削除したら一発で通りました。\n\n:::note warn\nセクションを持つ設定ファイル（INI、TOMLなど）に項目を追加するときは、必ずファイルの末尾に置くか、セクションの構造を理解してから入れてください。行単位の置換でセクション見出しを挿入すると、後ろの行の意味が変わります。\n:::\n\n## 切替前に「本番と同じ経路」で検証する\n\n証明書が手に入ると、切替前に本番とまったく同じ経路を検証できます。\n\nやり方は `curl --resolve` と同じです。接続先IPは新サーバーに固定して、SNIとHostヘッダは本番のドメイン名を使います。Pythonなら標準ライブラリの接続処理を差し替えるだけです。\n\n```python\nimport http.client, socket, ssl\n\nTARGET_IP = \"（新サーバーのIP）\"\n\nclass PinnedHTTPSConnection(http.client.HTTPSConnection):\n    \"\"\"接続先IPを固定しつつ、SNIとHostヘッダには本来のホスト名を使う\"\"\"\n    def connect(self):\n        self.sock = socket.create_connection((TARGET_IP, self.port), self.timeout)\n        self.sock = self._context.wrap_socket(self.sock, server_hostname=self.host)\n```\n\nサーバー側から見ると本番アクセスと区別がつきません。これで切替前に全部確認できました。\n\n| 検証項目 | 結果 |\n| --- | --- |\n| 画像6,378ファイル | 全件200 |\n| 投稿・メディア・カテゴリ・タグの件数 | 移行元と一致 |\n| 記事内の装飾要素（2,645箇所） | 移行元と一致 |\n| `http://` の混在参照 | 0件 |\n| 証明書の検証 | 正常 |\n\nさらに同じHostヘッダで移行元のIPにも接続すれば、新旧の応答を1対1で比べられます。\n\n検証中、カテゴリページの10件が404を返していて焦りました。移行元にも同じリクエストを投げたところ、移行元でも同じ10件が404 でした。日本語スラッグのカテゴリが元から404になっていた既存の不具合で、移行で壊したものではないと切り分けられました。\n\nこの方法のもう1つの利点は、ファイアウォールを自分のIPだけに絞ったまま検証が終わることです。外部に一度も露出させずに本番相当の確認ができます。\n\n## つまづきポイント(4) 切替後、メールが完全に死んでいた\n\n表示は完璧で、画像も広告も検証済みでしたが、数日経ってメール設定を確認したところ、以下のようになっていました。\n\n```text\nsh: 1: /usr/sbin/sendmail: not found\nwp_mail() の戻り値: false（送信失敗）\n```\n\n**LightsailのWordPressイメージにはメール送信の仕組み（MTA）が入っていません。** つまり切替以降、次のメールが全部届いていませんでした。\n\n- お問い合わせフォームの通知\n- コメント通知\n- パスワードリセットメール\n\n共用レンタルサーバーでは当然送れていたので、これは移行による機能の喪失です。しかも画面には何も出ません。サイトはまったく正常に見えます。\n\n:::note alert\n移行後のチェックリストに「メールが送れるか」を必ず入れてください。\n表示・画像・広告をどれだけ検証しても、メールは画面に出ないので気づけません。\n`wp_mail()` の戻り値と、エラー内容（`$phpmailer-\u003eErrorInfo`）の両方を確認するのが確実です。\n:::\n\n紛らわしいのは、PHPの設定には `sendmail_path` が入っていることです。設定を眺めただけでは送れそうに見えます。実体が存在しないだけなので、実際に送ってみないと分かりません。\n\n### Amazon SESで復旧する\n\nAWS上のサーバーは25番ポートが既定で制限されているので、自前でメールサーバーを立てても外に出せません。Amazon SESを使いました。\n\n:::note info\n**Amazon SESって何？**\nAWSのメール配信サービスです。すごく雑に言うと「メールを送る部分だけを借りる」仕組みです。\n自分でメールサーバーを立てると、迷惑メール判定されない設定や、送信の失敗処理まで全部面倒を見る必要があります。SESはその部分を引き受けてくれます。\n:::\n\n### 送信制限の解除は本当に必要か先に確認する\n\nSESは初期状態だと「検証済みのアドレス宛にしか送れない」制限があります（1日200通まで）。解除には申請と待ち時間が必要なので、条件反射で申請する前に必要かどうかを確認しました。\n\n判断基準は「任意のアドレスへ送る必要があるか」だけです。\n\n| 用途 | 宛先 | 制限下で足りるか |\n| --- | --- | --- |\n| お問い合わせの通知 | 管理者（固定） | 足りる |\n| コメント通知 | 管理者（固定） | 足りる |\n| パスワードリセット | 管理者自身 | 足りる |\n| フォーム送信者への自動返信 | 訪問者（任意） | 足りない |\n\nお問い合わせフォームの設定を確認すると、通知先は管理者のアドレス固定で、フォーム入力値が入るのは `Reply-To` だけでした。`Reply-To` は送信先ではないので、制限のままで足りると判断できました。\n\n### 送信元は必ず自分のドメインにする\n\n元の設定では送信元が管理者の個人メールアドレス（フリーメール）になっていました。これをそのままSESから送ると、他人のドメインを自分のサーバーから送る形になり、迷惑メール判定されやすくなります。\n\n送信元を `wordpress@（自分のドメイン）` に変えて、DKIM署名とSPFが整合するようにしました。`Reply-To` はフォーム入力値のまま残るので、受信後にそのまま返信できます。\n\n### つまづきポイント(5) IAMポリシーを絞りすぎて送信が拒否される\n\nSESを使うためのIAMポリシーで、「最小権限にしよう」として送信元だけを対象に指定しました。\n\n```json\n{\n  \"Effect\": \"Allow\",\n  \"Action\": [\"ses:SendRawEmail\"],\n  \"Resource\": \"arn:aws:ses:ap-northeast-1:（アカウントID）:identity/（自分のドメイン）\"\n}\n```\n\n実際に送ると拒否されました。\n\n```text\nSMTP Error: data not accepted. SMTP server error: DATA END command failed\nDetail: Access denied: User '...' is not authorized to perform 'ses:SendRawEmail'\non resource '...identity/（宛先のアドレス）'\n```\n\nSESは送信元だけでなく宛先も権限評価の対象にします。送信制限がかかっている状態では宛先アドレスも登録済みの識別子として扱われるので、そこへの権限が必要でした。\n\n修正版のポリシーがこちらです。対象を広げる代わりに、条件で送信元アドレスを固定しています。\n\n```json\n{\n  \"Effect\": \"Allow\",\n  \"Action\": [\"ses:SendRawEmail\", \"ses:SendEmail\"],\n  \"Resource\": \"*\",\n  \"Condition\": {\n    \"StringEquals\": { \"ses:FromAddress\": \"wordpress@（自分のドメイン）\" }\n  }\n}\n```\n\n対象が `*` でも、条件で送信元が固定されているので他のアドレスを騙って送ることはできません。絞り込みは対象リソースではなく条件で行うのがSESの正しい形でした。\n\n:::note warn\nIAMポリシーを絞ったら、必ず実際の操作で確認してください。\nポリシーの作成が成功したことは、その権限で目的の操作ができることを意味しません。\n:::\n\n### 認証情報はデータベースに置かない\n\nSESのSMTP認証情報を設定するとき、プラグインの管理画面から入れるとデータベースに平文で保存されます。DBのバックアップを取るたびに秘密が複製されることになります。\n\n`wp-config.php` に定数で書けばDBに入りません。\n\n```php\ndefine( 'WPMS_ON', true );\ndefine( 'WPMS_MAILER', 'smtp' );\ndefine( 'WPMS_MAIL_FROM', 'wordpress@（自分のドメイン）' );\ndefine( 'WPMS_MAIL_FROM_FORCE', true );\ndefine( 'WPMS_SMTP_HOST', 'email-smtp.ap-northeast-1.amazonaws.com' );\ndefine( 'WPMS_SMTP_PORT', 587 );\ndefine( 'WPMS_SSL', 'tls' );\ndefine( 'WPMS_SMTP_AUTH', true );\ndefine( 'WPMS_SMTP_USER', '（アクセスキーID）' );\ndefine( 'WPMS_SMTP_PASS', '（SMTPパスワード）' );\n```\n\n`WPMS_MAIL_FROM_FORCE` を有効にするのは必須でした。IAMポリシー側で送信元アドレスを固定しているので、送信元が揃っていないと権限エラーになります。\n\n## 紛らわしいポイント ファイアウォールの全開放\n\n切替のときにファイアウォールを全開放しようとして、素直に `0.0.0.0/0` を指定したらエラーになりました。\n\n```text\nInvalidInputException: You cannot specify a CIDR or a CIDR list alias together\nwith \"0.0.0.0/0\". Specifying \"0.0.0.0/0\" allows all CIDRs and CIDR list aliases.\n```\n\nLightsailでは「許可するIPを指定しない」ことが全許可という仕様です。既存のルールに特定のIPが入っていると `0.0.0.0/0` との併記になって弾かれます。\n\n一度ルールを削除してから、IPを指定せずに開き直すのが正解でした。\n\n## 検証で気づいた、共用サーバーが持っていたもの\n\n移行元と移行先のレスポンスヘッダを、同じHostヘッダで接続先IPだけ変えて比べてみました。表示が同じでも中身が違うかもしれないと思ったからです。\n\n| ヘッダ | 移行元（共用サーバー） | 移行先（Lightsail） |\n| --- | --- | --- |\n| `server` | 出していない | `Apache/2.4.68 (Debian)` |\n| `x-cache-status` | あり | なし |\n| `x-mod-pagespeed` | あり | なし |\n| `strict-transport-security` | なし | なし |\n\n検証を通じて明らかになったこと2つありました。\n\n１つ目は、Lightsailがサーバーのバージョンまで出していたことです。移行元は隠していたので、これは移行による劣化です。設定を追加して隠しました。\n\n```apache\nServerTokens Prod\nServerSignature Off\n```\n\n２つ目は、共用サーバーがサーバー側のページキャッシュと自動最適化を裏で効かせていたことです。移行先にはこれがないので、毎回PHPが動きます。CPU2個・メモリ2GBなので通常のアクセスでは問題ありませんが、共用サーバーが面倒を見てくれていた部分が見えました。\n\n## かかる費用\n\n料金は2026年9月時点のものです。\n\n| 項目 | 単価 |\n| --- | --- |\n| Lightsail（CPU2個・メモリ2GB・ディスク60GB・転送3TB） | 月 $12 |\n| Amazon Route 53 ホストゾーン | 月 $0.50 |\n| Amazon SES（月数十通） | $0.01 未満 |\n| Let's Encrypt 証明書 | 無料 |\n\n合計で月 $12.5 ほど、1ドル150円なら年間で約22,500円です。\n\n正直、**移行元の共用レンタルサーバーは年13,200円のため、金額だけ見ると、移行して高くなっています。**\n\n今回はAWSのクレジットがあるので自己負担は発生していませんが、「安くするための移行」ではありませんでした。共用レンタルサーバーの年1万円台は、かなり安いです。\n\n## やってみた感想\n\n一番良かったのは、中で何が起きているか全部見えるようになったことです。\n\n共用サーバーだと、画像が表示されない理由を調べようとしてもログに手が届きません。今回は6,378ファイルのパスを1つずつ突き合わせて、640ファイルがNFD形だと数えられました。前回の移行で諦めた原因も、無事に解決しました。\n\n同時に、共用レンタルサーバーがどれだけ面倒を見てくれていたかも分かりました。メールが送れるのは当たり前ではありませんし、ページキャッシュも勝手に効いていました。移行して初めて、無くなったものに気づきました。\n\n自分で組む価値があるのは、たぶんこういう人だと思います。\n中で何が起きているかを把握したい人。AWSの勉強も兼ねたい人。AWSクレジットが余っている人。\n\n逆に「ブログを書くことに集中したい」なら、共用レンタルサーバーのままが幸せです。月1,000円ちょっとで、メールもキャッシュも付いてきます。\n\nもし同じ移行をやるなら、順番だけは守ってほしいです。**小さいサイトで試す、切替前に本番と同じ経路で検証する、切替後にメールを確認する。** この3つを飛ばすと、私が踏んだ穴をそのまま踏むことになります。\n\n## 参考リンク\n\n- [Amazon Lightsail 料金](https://aws.amazon.com/jp/lightsail/pricing/)\n- [Lightsail の WordPress インスタンスを起動する](https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-tutorial-launching-and-configuring-wordpress.html)\n- [Bitnami 版ブループリントの提供終了について](https://docs.aws.amazon.com/ja_jp/lightsail/latest/userguide/amazon-lightsail-bitnami-blueprint-deprecation.html)\n- [certbot のユーザーガイド](https://eff-certbot.readthedocs.io/en/stable/using.html)\n- [Amazon SES の SMTP 認証情報を取得する](https://docs.aws.amazon.com/ja_jp/ses/latest/dg/smtp-credentials.html)\n- [Amazon SES のサンドボックス外への移動](https://docs.aws.amazon.com/ja_jp/ses/latest/dg/request-production-access.html)\n- [Unicode Normalization Forms（Unicode 公式）](https://unicode.org/reports/tr15/)\n- [WordPress の移行（公式ドキュメント）](https://wordpress.org/documentation/article/moving-wordpress/)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:21:59+09:00","group":null,"id":"22f601e20cd11ff9f3e2","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"WordPress","versions":[]},{"name":"AWS","versions":[]},{"name":"Lightsail","versions":[]}],"title":"一度失敗したWordPressの移行をAmazon Lightsailでやり直してみた","updated_at":"2026-09-27T11:24:37+09:00","url":"https://qiita.com/_YukiOgawa/items/22f601e20cd11ff9f3e2","user":{"description":"AWSとビールが好きです。","facebook_id":"","followees_count":4,"followers_count":19,"github_login_name":null,"id":"_YukiOgawa","items_count":37,"linkedin_id":"yuki-ogawa-453a541a6/","location":"","name":"Yuki Ogawa","organization":"","permanent_id":3706974,"profile_image_url":"https://pbs.twimg.com/profile_images/1743628527721922560/M2P-4eeP_bigger.jpg","team_only":false,"twitter_screen_name":"_YukiOgawa","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cdiv data-sourcepos=\"1:1-3:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"2:1-2:171\"\u003eこの記事は、筆者の開発メモをもとに AI（Claude）と下書きを作り、Supabase 公式の告知で事実を確認したうえで仕上げています。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"5:1-5:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"7:1-8:116\"\u003eSupabase は \u003cstrong\u003e2026-10-30 から、既存の全プロジェクトで public スキーマの新規テーブルへの権限（GRANT）の自動付与をやめます\u003c/strong\u003e。\u003cbr\u003e\nこれからは、テーブルを作っただけでは Data API（PostgREST / supabase-js）から触れません。\u003c/p\u003e\n\u003cp data-sourcepos=\"10:1-12:119\"\u003e筆者は期限前から、この状態を一度踏んでいます。\u003cbr\u003e\nRLS ポリシーを正しく書いたつもりなのに、新しく作ったテーブルへの操作が全部 \u003ccode\u003e42501\u003c/code\u003e で失敗しました。\u003cbr\u003e\nエラーコードが RLS 違反と同じなので、ポリシーの書き間違いを疑って遠回りしました。\u003c/p\u003e\n\u003cp data-sourcepos=\"14:1-15:67\"\u003eこの記事は、Supabase を使っていて SQL Editor や migration でテーブルを作っている方に向けたものです。\u003cbr\u003e\n読み終えると、次の2つができるようになります。\u003c/p\u003e\n\u003cul data-sourcepos=\"17:1-19:0\"\u003e\n\u003cli data-sourcepos=\"17:1-17:76\"\u003e自分のプロジェクトが影響を受けるかを SQL で確認する\u003c/li\u003e\n\u003cli data-sourcepos=\"18:1-19:0\"\u003emigration に GRANT を書く型を、そのまま持ち帰る\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"20:1-20:8\"\u003e\n\u003cspan id=\"tldr\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tldr\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTL;DR\u003c/h2\u003e\n\u003cul data-sourcepos=\"22:1-27:0\"\u003e\n\u003cli data-sourcepos=\"22:1-22:181\"\u003e\n\u003cstrong\u003e2026-10-30 以降、public の新規テーブルには \u003ccode\u003eanon\u003c/code\u003e / \u003ccode\u003eauthenticated\u003c/code\u003e / \u003ccode\u003eservice_role\u003c/code\u003e の権限が自動で付かない\u003c/strong\u003e。既存テーブルの権限はそのまま\u003c/li\u003e\n\u003cli data-sourcepos=\"23:1-23:123\"\u003e\n\u003cstrong\u003eGRANT と RLS は別の層\u003c/strong\u003e。GRANT が無いと、RLS を評価する前に \u003ccode\u003e42501 permission denied\u003c/code\u003e で弾かれる\u003c/li\u003e\n\u003cli data-sourcepos=\"24:1-24:108\"\u003e同じ \u003ccode\u003e42501\u003c/code\u003e でも、\u003cstrong\u003eメッセージを見れば GRANT 不足か RLS 違反かを見分けられる\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"25:1-25:160\"\u003e\n\u003cstrong\u003e新しいテーブルの migration には GRANT を必ず書く\u003c/strong\u003e。必要なロール・必要な操作だけに絞り、\u003ccode\u003eanon\u003c/code\u003e には安易に付けない\u003c/li\u003e\n\u003cli data-sourcepos=\"26:1-27:0\"\u003e本番が無事でも、\u003cstrong\u003emigration から環境を作り直すと全テーブルが \u003ccode\u003e42501\u003c/code\u003e になる\u003c/strong\u003eことがある。スキーマの正本にも GRANT を書いておく\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"28:1-28:24\"\u003e\n\u003cspan id=\"何が変わるのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BD%95%E3%81%8C%E5%A4%89%E3%82%8F%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e何が変わるのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"30:1-30:179\"\u003e公式の告知（\u003ca href=\"https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically\" rel=\"nofollow noopener\" target=\"_blank\"\u003eChangelog\u003c/a\u003e）から、要点をまとめます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"32:1-36:87\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"32:1-32:19\"\u003e\n\u003cth data-sourcepos=\"32:2-32:9\"\u003e日付\u003c/th\u003e\n\u003cth data-sourcepos=\"32:11-32:18\"\u003e内容\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"34:1-34:92\"\u003e\n\u003ctd data-sourcepos=\"34:2-34:13\"\u003e2026-04-28\u003c/td\u003e\n\u003ctd data-sourcepos=\"34:15-34:91\"\u003eプロジェクト作成時に新しい挙動を選べるようになった\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"35:1-35:93\"\u003e\n\u003ctd data-sourcepos=\"35:2-35:13\"\u003e2026-05-30\u003c/td\u003e\n\u003ctd data-sourcepos=\"35:15-35:92\"\u003e\n\u003cstrong\u003e新規プロジェクト\u003c/strong\u003eは新しい挙動がデフォルトになった\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"36:1-36:87\"\u003e\n\u003ctd data-sourcepos=\"36:2-36:13\"\u003e2026-10-30\u003c/td\u003e\n\u003ctd data-sourcepos=\"36:15-36:86\"\u003e\n\u003cstrong\u003e既存の全プロジェクト\u003c/strong\u003eに新しい挙動が適用される\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"38:1-38:42\"\u003e変更の中身は次のとおりです。\u003c/p\u003e\n\u003cul data-sourcepos=\"40:1-44:0\"\u003e\n\u003cli data-sourcepos=\"40:1-40:222\"\u003eこれまでは、public スキーマに作ったテーブルに \u003ccode\u003eanon\u003c/code\u003e / \u003ccode\u003eauthenticated\u003c/code\u003e / \u003ccode\u003eservice_role\u003c/code\u003e の \u003ccode\u003eSELECT, INSERT, UPDATE, DELETE\u003c/code\u003e が\u003cstrong\u003e既定の権限（default privileges）で自動的に\u003c/strong\u003e付いていた\u003c/li\u003e\n\u003cli data-sourcepos=\"41:1-41:139\"\u003eこれからは、この既定の権限が外れる。Data API から使うテーブルには、\u003cstrong\u003e明示的な GRANT が必要\u003c/strong\u003eになる\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-42:93\"\u003e\n\u003cstrong\u003e既存テーブルは影響を受けない\u003c/strong\u003e。付いている権限はそのまま残る\u003c/li\u003e\n\u003cli data-sourcepos=\"43:1-44:0\"\u003e対象は Data API（PostgREST / GraphQL）経由のアクセス。Postgres への直接接続は関係ない\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv data-sourcepos=\"45:1-47:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"46:1-46:339\"\u003e\u003ccode\u003eservice_role\u003c/code\u003e も対象です。サーバー側で \u003ccode\u003eservice_role\u003c/code\u003e キーを使っていると「RLS をバイパスするから大丈夫」と思いがちですが、\u003cstrong\u003eRLS をバイパスできても GRANT はバイパスできません\u003c/strong\u003e。サーバーからだけ使うテーブルでも、\u003ccode\u003eservice_role\u003c/code\u003e への GRANT が必要です。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"49:1-49:52\"\u003e影響が出るのは、大きく次の2つです。\u003c/p\u003e\n\u003col data-sourcepos=\"51:1-53:0\"\u003e\n\u003cli data-sourcepos=\"51:1-51:37\"\u003e\u003cstrong\u003eこれから作るテーブル\u003c/strong\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"52:1-53:0\"\u003e\n\u003cstrong\u003emigration やスキーマ定義から環境を作り直すとき\u003c/strong\u003e（新しいプロジェクト、ローカルでの \u003ccode\u003esupabase db reset\u003c/code\u003e など）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"54:1-55:264\"\u003e2つ目は見落としやすいポイントです。\u003cbr\u003e\n公式の Discussion でも、新しいデフォルトで \u003ccode\u003esupabase db reset\u003c/code\u003e をすると過去のテーブルに GRANT が付かない、という点が取り上げられています（\u003ca href=\"https://github.com/orgs/supabase/discussions/45329\" rel=\"nofollow noopener\" target=\"_blank\"\u003eDiscussion #45329\u003c/a\u003e）。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"57:1-57:29\"\u003e\n\u003cspan id=\"grant-と-rls-は別の層\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#grant-%E3%81%A8-rls-%E3%81%AF%E5%88%A5%E3%81%AE%E5%B1%A4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eGRANT と RLS は別の層\u003c/h2\u003e\n\u003cp data-sourcepos=\"59:1-60:88\"\u003eまず、なぜ「RLS を正しく書いたのに動かない」のかを整理します。\u003cbr\u003e\nPostgres は、Data API から来たリクエストを2段階でチェックします。\u003c/p\u003e\n\u003ciframe id=\"qiita-embed-content__428068bf0dfe0002b8c28770ff1bed4a\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__428068bf0dfe0002b8c28770ff1bed4a\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart LR\\n A[リクエスト\u0026lt;br/\u0026gt;anon / authenticated] --\u0026gt; B{テーブル権限\u0026lt;br/\u0026gt;GRANT}\\n B --\u0026gt;|無い| X[42501\u0026lt;br/\u0026gt;permission denied for table]\\n B --\u0026gt;|ある| C{RLS ポリシー}\\n C --\u0026gt;|合わない| Y[0件 or 42501\u0026lt;br/\u0026gt;new row violates RLS]\\n C --\u0026gt;|合う| D[行を返す・書き込む]\",\"key\":\"45fb552ffb9b91562893035189b568f8\"}'\u003e\n\u003c/iframe\u003e\n\n\u003ctable data-sourcepos=\"71:1-74:155\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"71:1-71:31\"\u003e\n\u003cth data-sourcepos=\"71:2-71:6\"\u003e層\u003c/th\u003e\n\u003cth data-sourcepos=\"71:8-71:24\"\u003e決めること\u003c/th\u003e\n\u003cth data-sourcepos=\"71:26-71:30\"\u003e例\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"73:1-73:166\"\u003e\n\u003ctd data-sourcepos=\"73:2-73:32\"\u003eテーブル権限（GRANT）\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:34-73:114\"\u003eそのロールが、そのテーブルを\u003cstrong\u003eそもそも操作してよいか\u003c/strong\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:116-73:165\"\u003e\n\u003ccode\u003eauthenticated\u003c/code\u003e は \u003ccode\u003enotes\u003c/code\u003e に INSERT できる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"74:1-74:155\"\u003e\n\u003ctd data-sourcepos=\"74:2-74:42\"\u003eRLS（行レベルセキュリティ）\u003c/td\u003e\n\u003ctd data-sourcepos=\"74:44-74:118\"\u003eそのロールが、\u003cstrong\u003eどの行を\u003c/strong\u003e見てよいか・書いてよいか\u003c/td\u003e\n\u003ctd data-sourcepos=\"74:120-74:154\"\u003e自分の \u003ccode\u003eowner_id\u003c/code\u003e の行だけ\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"76:1-78:87\"\u003eGRANT は外側の門で、RLS はその内側の門です。\u003cbr\u003e\n\u003cstrong\u003e外側の門が閉じていると、内側の RLS は評価すらされません\u003c/strong\u003e。\u003cbr\u003e\nだから、ポリシーをどれだけ正しく書いても \u003ccode\u003e42501\u003c/code\u003e のままです。\u003c/p\u003e\n\u003cp data-sourcepos=\"80:1-80:195\"\u003e公式ドキュメントも「公開するオブジェクトには両方を使うこと」と書いています（\u003ca href=\"https://supabase.com/docs/guides/api/securing-your-api\" rel=\"nofollow noopener\" target=\"_blank\"\u003eSecuring your API\u003c/a\u003e）。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"82:1-82:60\"\u003e\n\u003cspan id=\"実際にハマった例期限前から起きていた\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E4%BE%8B%E6%9C%9F%E9%99%90%E5%89%8D%E3%81%8B%E3%82%89%E8%B5%B7%E3%81%8D%E3%81%A6%E3%81%84%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際にハマった例：期限前から起きていた\u003c/h2\u003e\n\u003cp data-sourcepos=\"84:1-85:148\"\u003e筆者の場合、既存のテーブルは問題なく動いていました。\u003cbr\u003e\nところが、後から SQL Editor で \u003ccode\u003ecreate table\u003c/code\u003e した新しいテーブルだけ、アプリからの操作がすべて失敗しました。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"87:1-90:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecode: 42501\nmessage: permission denied for table xxx\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"92:1-94:123\"\u003eRLS を有効にしてポリシーも書いた直後だったので、まずポリシーを疑いました。\u003cbr\u003e\nしかし、原因は\u003cstrong\u003e新しいテーブルに \u003ccode\u003eauthenticated\u003c/code\u003e への GRANT が付いていなかった\u003c/strong\u003eことでした。\u003cbr\u003e\n既存テーブルには以前から権限が付いていたため、それまで問題が表に出なかったのです。\u003c/p\u003e\n\u003cp data-sourcepos=\"96:1-96:180\"\u003e振り返ると、今回の廃止は「条件によってはすでに起きていたこと」が、全プロジェクトの新規テーブルに広がる変更だと言えます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"98:1-98:60\"\u003e\n\u003cspan id=\"42501-の見分け方同じコードで原因が2つ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#42501-%E3%81%AE%E8%A6%8B%E5%88%86%E3%81%91%E6%96%B9%E5%90%8C%E3%81%98%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E5%8E%9F%E5%9B%A0%E3%81%8C2%E3%81%A4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e\u003ccode\u003e42501\u003c/code\u003e の見分け方：同じコードで原因が2つ\u003c/h2\u003e\n\u003cp data-sourcepos=\"100:1-102:48\"\u003e\u003ccode\u003e42501\u003c/code\u003e は「権限不足」を表す Postgres のエラーコードです。\u003cbr\u003e\nGRANT が無いときも、RLS に弾かれたときも同じコードになります。\u003cbr\u003e\n見分けるにはメッセージを見ます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"104:1-107:143\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"104:1-104:37\"\u003e\n\u003cth data-sourcepos=\"104:2-104:18\"\u003eメッセージ\u003c/th\u003e\n\u003cth data-sourcepos=\"104:20-104:27\"\u003e原因\u003c/th\u003e\n\u003cth data-sourcepos=\"104:29-104:36\"\u003e対処\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"106:1-106:99\"\u003e\n\u003ctd data-sourcepos=\"106:2-106:36\"\u003e\u003ccode\u003epermission denied for table xxx\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"106:38-106:77\"\u003eテーブル権限（GRANT）が無い\u003c/td\u003e\n\u003ctd data-sourcepos=\"106:79-106:98\"\u003eGRANT を付ける\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"107:1-107:143\"\u003e\n\u003ctd data-sourcepos=\"107:2-107:63\"\u003e\u003ccode\u003enew row violates row-level security policy for table \"xxx\"\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"107:65-107:97\"\u003eRLS ポリシーに合わない\u003c/td\u003e\n\u003ctd data-sourcepos=\"107:99-107:142\"\u003eポリシーまたはクエリを見直す\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"109:1-110:103\"\u003e新しい挙動では、GRANT 不足のときに \u003cstrong\u003e\u003ccode\u003ehint\u003c/code\u003e に必要な GRANT 文が入って返ってきます\u003c/strong\u003e（公式 Changelog より）。\u003cbr\u003e\nエラーオブジェクトは \u003ccode\u003emessage\u003c/code\u003e だけでなく \u003ccode\u003ehint\u003c/code\u003e まで見るのがおすすめです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"typescript\" data-sourcepos=\"112:1-122:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"nx\"\u003eerror\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nx\"\u003esupabase\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"k\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003enotes\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003einsert\u003c/span\u003e\u003cspan class=\"p\"\u003e({\u003c/span\u003e \u003cspan class=\"na\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"s1\"\u003etest\u003c/span\u003e\u003cspan class=\"dl\"\u003e'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"na\"\u003eowner_id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nx\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ehint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// 42501 / permission denied for table notes          → GRANT が無い\u003c/span\u003e\n  \u003cspan class=\"c1\"\u003e// 42501 / new row violates row-level security policy → RLS に合わない\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"124:1-124:47\"\u003e\n\u003cspan id=\"補足rls-側でよくある落とし穴\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%A3%9C%E8%B6%B3rls-%E5%81%B4%E3%81%A7%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e補足：RLS 側でよくある落とし穴\u003c/h3\u003e\n\u003cp data-sourcepos=\"126:1-126:121\"\u003eGRANT とは別の原因ですが、見分けるときに一緒に知っておくと便利なものを1つ挙げます。\u003c/p\u003e\n\u003cp data-sourcepos=\"128:1-129:202\"\u003esupabase-js の \u003ccode\u003einsert().select()\u003c/code\u003e は、SQL でいうと \u003ccode\u003eINSERT ... RETURNING\u003c/code\u003e です。\u003cbr\u003e\nこのとき Postgres は、\u003cstrong\u003e挿入した行が SELECT ポリシーも満たすこと\u003c/strong\u003eを求めます（\u003ca href=\"https://www.postgresql.org/docs/current/sql-createpolicy.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePostgreSQL: CREATE POLICY\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"131:1-132:122\"\u003eたとえば SELECT ポリシーが「トリガーで後から作られる別テーブルの行」を前提にしていると、INSERT ポリシーは満たしていても失敗します。\u003cbr\u003e\nINSERT ポリシーを何度見直しても直らないときは、SELECT ポリシーも確認してみてください。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"134:1-134:66\"\u003e\n\u003cspan id=\"自分のプロジェクトが影響を受けるか確かめる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%87%AA%E5%88%86%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%8C%E5%BD%B1%E9%9F%BF%E3%82%92%E5%8F%97%E3%81%91%E3%82%8B%E3%81%8B%E7%A2%BA%E3%81%8B%E3%82%81%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e自分のプロジェクトが影響を受けるか確かめる\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"136:1-136:40\"\u003e\n\u003cspan id=\"既存テーブルの権限を見る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%97%A2%E5%AD%98%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E6%A8%A9%E9%99%90%E3%82%92%E8%A6%8B%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e既存テーブルの権限を見る\u003c/h3\u003e\n\u003cp data-sourcepos=\"138:1-138:53\"\u003eSQL Editor で次のどちらかを実行します。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"140:1-147:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e-- テーブルごとの権限（ACL）をまとめて見る\u003c/span\u003e\n\u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"n\"\u003erelname\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erelacl\u003c/span\u003e\n\u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003epg_class\u003c/span\u003e\n\u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003erelnamespace\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e'public'\u003c/span\u003e\u003cspan class=\"p\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eregnamespace\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003erelkind\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e'r'\u003c/span\u003e\n\u003cspan class=\"k\"\u003eorder\u003c/span\u003e \u003cspan class=\"k\"\u003eby\u003c/span\u003e \u003cspan class=\"n\"\u003erelname\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"149:1-150:165\"\u003e\u003ccode\u003erelacl\u003c/code\u003e に \u003ccode\u003eauthenticated=arwd...\u003c/code\u003e のような表示があれば付与済みです。\u003cbr\u003e\n文字の意味は、\u003ccode\u003er\u003c/code\u003e=SELECT、\u003ccode\u003ea\u003c/code\u003e=INSERT、\u003ccode\u003ew\u003c/code\u003e=UPDATE、\u003ccode\u003ed\u003c/code\u003e=DELETE です（\u003ca href=\"https://www.postgresql.org/docs/current/ddl-priv.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePostgreSQL: Privileges\u003c/a\u003e）。\u003c/p\u003e\n\u003cp data-sourcepos=\"152:1-152:88\"\u003eロールと操作を1行ずつ見たい場合は、こちらが読みやすいです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"154:1-160:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"k\"\u003etable_name\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003egrantee\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eprivilege_type\u003c/span\u003e\n\u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003einformation_schema\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erole_table_grants\u003c/span\u003e\n\u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003etable_schema\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e'public'\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003egrantee\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e'anon'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'authenticated'\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e'service_role'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003cspan class=\"k\"\u003eorder\u003c/span\u003e \u003cspan class=\"k\"\u003eby\u003c/span\u003e \u003cspan class=\"k\"\u003etable_name\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003egrantee\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eprivilege_type\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"162:1-162:28\"\u003e\n\u003cspan id=\"確認するポイント\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e確認するポイント\u003c/h3\u003e\n\u003ctable data-sourcepos=\"164:1-168:120\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"164:1-164:40\"\u003e\n\u003cth data-sourcepos=\"164:2-164:21\"\u003e確認すること\u003c/th\u003e\n\u003cth data-sourcepos=\"164:23-164:30\"\u003e状態\u003c/th\u003e\n\u003cth data-sourcepos=\"164:32-164:39\"\u003e判断\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"166:1-166:103\"\u003e\n\u003ctd data-sourcepos=\"166:2-166:60\"\u003e本番の既存テーブルに権限が付いているか\u003c/td\u003e\n\u003ctd data-sourcepos=\"166:62-166:78\"\u003e付いている\u003c/td\u003e\n\u003ctd data-sourcepos=\"166:80-166:102\"\u003e当面は影響なし\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"167:1-167:149\"\u003e\n\u003ctd data-sourcepos=\"167:2-167:67\"\u003emigration / スキーマ定義に GRANT が書かれているか\u003c/td\u003e\n\u003ctd data-sourcepos=\"167:69-167:91\"\u003e書かれていない\u003c/td\u003e\n\u003ctd data-sourcepos=\"167:93-167:148\"\u003e\u003cstrong\u003e環境を作り直すと全テーブルで \u003ccode\u003e42501\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"168:1-168:120\"\u003e\n\u003ctd data-sourcepos=\"168:2-168:54\"\u003e今後テーブルを追加する予定があるか\u003c/td\u003e\n\u003ctd data-sourcepos=\"168:56-168:63\"\u003eある\u003c/td\u003e\n\u003ctd data-sourcepos=\"168:65-168:119\"\u003emigration に GRANT を書く運用に切り替える\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"170:1-171:117\"\u003e本番が無事でも、2行目に当てはまるなら対応しておく価値があります。\u003cbr\u003e\n「本番の権限は、過去の自動付与のおかげで付いているだけ」という状態だからです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"173:1-173:47\"\u003e\n\u003cspan id=\"対策migration-に-grant-を明示する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AF%BE%E7%AD%96migration-%E3%81%AB-grant-%E3%82%92%E6%98%8E%E7%A4%BA%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e対策：migration に GRANT を明示する\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"175:1-175:45\"\u003e\n\u003cspan id=\"新しいテーブルの-migration-の型\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%96%B0%E3%81%97%E3%81%84%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE-migration-%E3%81%AE%E5%9E%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e新しいテーブルの migration の型\u003c/h3\u003e\n\u003cp data-sourcepos=\"177:1-178:102\"\u003e\u003cstrong\u003eテーブル作成・RLS 有効化・ポリシー・GRANT を、同じ migration ファイルにセットで書きます\u003c/strong\u003e。\u003cbr\u003e\n別ファイルに分けると、GRANT だけ適用し忘れる事故が起きやすくなります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"180:1-201:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e \u003cspan class=\"k\"\u003etable\u003c/span\u003e \u003cspan class=\"n\"\u003eif\u003c/span\u003e \u003cspan class=\"k\"\u003enot\u003c/span\u003e \u003cspan class=\"k\"\u003eexists\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enotes\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\n  \u003cspan class=\"n\"\u003eid\u003c/span\u003e \u003cspan class=\"n\"\u003euuid\u003c/span\u003e \u003cspan class=\"k\"\u003eprimary\u003c/span\u003e \u003cspan class=\"k\"\u003ekey\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"n\"\u003egen_random_uuid\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n  \u003cspan class=\"n\"\u003eowner_id\u003c/span\u003e \u003cspan class=\"n\"\u003euuid\u003c/span\u003e \u003cspan class=\"k\"\u003enot\u003c/span\u003e \u003cspan class=\"k\"\u003enull\u003c/span\u003e \u003cspan class=\"k\"\u003ereferences\u003c/span\u003e \u003cspan class=\"n\"\u003eauth\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eusers\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003edelete\u003c/span\u003e \u003cspan class=\"k\"\u003ecascade\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"n\"\u003ebody\u003c/span\u003e \u003cspan class=\"nb\"\u003etext\u003c/span\u003e \u003cspan class=\"k\"\u003enot\u003c/span\u003e \u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"n\"\u003ecreated_at\u003c/span\u003e \u003cspan class=\"n\"\u003etimestamptz\u003c/span\u003e \u003cspan class=\"k\"\u003enot\u003c/span\u003e \u003cspan class=\"k\"\u003enull\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"n\"\u003enow\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003ealter\u003c/span\u003e \u003cspan class=\"k\"\u003etable\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enotes\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e \u003cspan class=\"k\"\u003erow\u003c/span\u003e \u003cspan class=\"k\"\u003elevel\u003c/span\u003e \u003cspan class=\"k\"\u003esecurity\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"nv\"\u003e\"notes: 自分の行だけ読める\"\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enotes\u003c/span\u003e\n  \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"k\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"n\"\u003eauth\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euid\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eowner_id\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"nv\"\u003e\"notes: 自分の行だけ書ける\"\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enotes\u003c/span\u003e\n  \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003einsert\u003c/span\u003e \u003cspan class=\"k\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ewith\u003c/span\u003e \u003cspan class=\"k\"\u003echeck\u003c/span\u003e \u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"n\"\u003eauth\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003euid\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eowner_id\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e-- テーブル権限（RLS の前段）。2026-10-30 以降は自動で付かないので明示する。\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e-- ログインユーザーだけが使うテーブルなので anon には付けない。\u003c/span\u003e\n\u003cspan class=\"k\"\u003egrant\u003c/span\u003e \u003cspan class=\"k\"\u003eselect\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003einsert\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003edelete\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003etable\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003enotes\u003c/span\u003e \u003cspan class=\"k\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"203:1-203:257\"\u003eポリシー内の \u003ccode\u003eauth.uid()\u003c/code\u003e を \u003ccode\u003e(select auth.uid())\u003c/code\u003e で包んでいるのは、行ごとの再評価を避けるためです（\u003ca href=\"https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select\" rel=\"nofollow noopener\" target=\"_blank\"\u003eRow Level Security\u003c/a\u003e）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"205:1-205:31\"\u003e\n\u003cspan id=\"付ける権限の決め方\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BB%98%E3%81%91%E3%82%8B%E6%A8%A9%E9%99%90%E3%81%AE%E6%B1%BA%E3%82%81%E6%96%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e付ける権限の決め方\u003c/h3\u003e\n\u003cp data-sourcepos=\"207:1-208:64\"\u003e自動付与は「3ロールに4操作すべて」でした。\u003cbr\u003e\n明示するなら、\u003cstrong\u003e必要なものだけ\u003c/strong\u003eに絞れます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"210:1-215:108\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"210:1-210:49\"\u003e\n\u003cth data-sourcepos=\"210:2-210:30\"\u003eテーブルの使われ方\u003c/th\u003e\n\u003cth data-sourcepos=\"210:32-210:48\"\u003e付けるもの\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"212:1-212:98\"\u003e\n\u003ctd data-sourcepos=\"212:2-212:54\"\u003eログインユーザーだけが読み書きする\u003c/td\u003e\n\u003ctd data-sourcepos=\"212:56-212:97\"\u003e\n\u003ccode\u003eauthenticated\u003c/code\u003e に必要な操作だけ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"213:1-213:106\"\u003e\n\u003ctd data-sourcepos=\"213:2-213:48\"\u003e未ログインでも読める公開データ\u003c/td\u003e\n\u003ctd data-sourcepos=\"213:50-213:105\"\u003e\n\u003ccode\u003eanon\u003c/code\u003e に \u003ccode\u003eselect\u003c/code\u003e だけ（＋RLS で行を絞る）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"214:1-214:46\"\u003e\n\u003ctd data-sourcepos=\"214:2-214:27\"\u003e読むだけのマスタ\u003c/td\u003e\n\u003ctd data-sourcepos=\"214:29-214:45\"\u003e\n\u003ccode\u003eselect\u003c/code\u003e だけ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"215:1-215:108\"\u003e\n\u003ctd data-sourcepos=\"215:2-215:56\"\u003eサーバー（\u003ccode\u003eservice_role\u003c/code\u003e）からも操作する\u003c/td\u003e\n\u003ctd data-sourcepos=\"215:58-215:107\"\u003e\n\u003ccode\u003eservice_role\u003c/code\u003e にも必要な操作を付ける\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"217:1-218:104\"\u003e\u003ccode\u003eanon\u003c/code\u003e に付けると、未ログインでのアクセス経路が1つ増えます。\u003cbr\u003e\nRLS で守られていても、\u003cstrong\u003e使わない経路は最初から作らない\u003c/strong\u003e方が安全側です。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"220:1-220:50\"\u003e\n\u003cspan id=\"テーブル以外にも-grant-が要るもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E4%BB%A5%E5%A4%96%E3%81%AB%E3%82%82-grant-%E3%81%8C%E8%A6%81%E3%82%8B%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eテーブル以外にも GRANT が要るもの\u003c/h3\u003e\n\u003cp data-sourcepos=\"222:1-222:114\"\u003e公式ドキュメントと Discussion では、テーブル以外にも次のものが挙げられています。\u003c/p\u003e\n\u003cul data-sourcepos=\"224:1-227:0\"\u003e\n\u003cli data-sourcepos=\"224:1-224:54\"\u003e\n\u003cstrong\u003e関数（RPC）\u003c/strong\u003e: \u003ccode\u003eexecute\u003c/code\u003e 権限を明示する\u003c/li\u003e\n\u003cli data-sourcepos=\"225:1-225:89\"\u003e\n\u003cstrong\u003eビュー\u003c/strong\u003e: テーブルと同様に GRANT が要る。同じ migration で付ける\u003c/li\u003e\n\u003cli data-sourcepos=\"226:1-227:0\"\u003e\n\u003cstrong\u003eシーケンス\u003c/strong\u003e: 既定の権限の取り消し対象に含まれる。\u003ccode\u003eserial\u003c/code\u003e 系の列を使う場合は確認する\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"228:1-229:98\"\u003e関数は、呼べるロールを絞る書き方にしておくと安心です。\u003cbr\u003e\nPostgres の関数は、既定で \u003ccode\u003ePUBLIC\u003c/code\u003e（全ロール）に \u003ccode\u003eexecute\u003c/code\u003e が付くためです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"231:1-234:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003erevoke\u003c/span\u003e \u003cspan class=\"k\"\u003eexecute\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emy_rpc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euuid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eanon\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003egrant\u003c/span\u003e  \u003cspan class=\"k\"\u003eexecute\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003emy_rpc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euuid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch3 data-sourcepos=\"236:1-236:64\"\u003e\n\u003cspan id=\"スキーマの正本にも既存テーブルぶんを書く\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E6%AD%A3%E6%9C%AC%E3%81%AB%E3%82%82%E6%97%A2%E5%AD%98%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%B6%E3%82%93%E3%82%92%E6%9B%B8%E3%81%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eスキーマの正本にも既存テーブルぶんを書く\u003c/h3\u003e\n\u003cp data-sourcepos=\"238:1-240:84\"\u003eスキーマ全体を1ファイル（\u003ccode\u003eschema.sql\u003c/code\u003e など）で管理している場合は、\u003cstrong\u003e既存の全テーブルぶんの GRANT も書いておきます\u003c/strong\u003e。\u003cbr\u003e\n本番にはすでに付いているので、実行しても何も変わりません。\u003cbr\u003e\n効くのは、そのファイルから環境を作り直したときだけです。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"242:1-242:76\"\u003e\n\u003cspan id=\"小技supabase-のロールが無い環境でも落ちない書き方\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%B0%8F%E6%8A%80supabase-%E3%81%AE%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%8C%E7%84%A1%E3%81%84%E7%92%B0%E5%A2%83%E3%81%A7%E3%82%82%E8%90%BD%E3%81%A1%E3%81%AA%E3%81%84%E6%9B%B8%E3%81%8D%E6%96%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e小技：Supabase のロールが無い環境でも落ちない書き方\u003c/h3\u003e\n\u003cp data-sourcepos=\"244:1-246:110\"\u003e型生成や CI のために、素の Postgres コンテナへスキーマを流すことがあります。\u003cbr\u003e\nその環境には \u003ccode\u003eauthenticated\u003c/code\u003e などの Supabase 固有ロールが無いため、GRANT がエラーになります。\u003cbr\u003e\n\u003ccode\u003epg_roles\u003c/code\u003e で存在を確認してから付けると、両方の環境で同じファイルを使えます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"248:1-256:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003edo\u003c/span\u003e \u003cspan class=\"err\"\u003e$$\u003c/span\u003e\n\u003cspan class=\"k\"\u003ebegin\u003c/span\u003e\n  \u003cspan class=\"n\"\u003eif\u003c/span\u003e \u003cspan class=\"k\"\u003eexists\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003epg_roles\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003erolname\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e'authenticated'\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ethen\u003c/span\u003e\n    \u003cspan class=\"k\"\u003egrant\u003c/span\u003e \u003cspan class=\"k\"\u003eselect\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003einsert\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003edelete\u003c/span\u003e\n      \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"k\"\u003etable\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etable_a\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etable_b\u003c/span\u003e \u003cspan class=\"k\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eend\u003c/span\u003e \u003cspan class=\"n\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003eend\u003c/span\u003e \u003cspan class=\"err\"\u003e$$\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv data-sourcepos=\"258:1-260:3\" class=\"note warn\"\u003e\n\u003cspan class=\"fa fa-fw fa-exclamation-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"259:1-259:346\"\u003e\u003ccode\u003ecreate policy ... to authenticated\u003c/code\u003e のようにポリシー側でもロールを参照している場合は、この書き方だけでは足りません。ポリシー作成時にもロールが必要になるため、その環境では先に \u003ccode\u003ecreate role authenticated;\u003c/code\u003e などでスタブのロールを作っておく必要があります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"262:1-262:48\"\u003e\n\u003cspan id=\"期限前に新しい挙動で試しておく\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%9F%E9%99%90%E5%89%8D%E3%81%AB%E6%96%B0%E3%81%97%E3%81%84%E6%8C%99%E5%8B%95%E3%81%A7%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%8A%E3%81%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e期限前に新しい挙動で試しておく\u003c/h2\u003e\n\u003cp data-sourcepos=\"264:1-265:94\"\u003e10/30 を待たずに、既存プロジェクトを新しい挙動に切り替えることもできます。\u003cbr\u003e\n公式 Changelog では、SQL Editor で次を実行する方法が案内されています。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"sql\" data-sourcepos=\"267:1-272:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"k\"\u003ealter\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"k\"\u003eprivileges\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003erole\u003c/span\u003e \u003cspan class=\"n\"\u003epostgres\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"k\"\u003eschema\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\n  \u003cspan class=\"k\"\u003erevoke\u003c/span\u003e \u003cspan class=\"k\"\u003eselect\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003einsert\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003edelete\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"n\"\u003etables\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003eanon\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eservice_role\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"k\"\u003ealter\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e \u003cspan class=\"k\"\u003eprivileges\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003erole\u003c/span\u003e \u003cspan class=\"n\"\u003epostgres\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"k\"\u003eschema\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e\n  \u003cspan class=\"k\"\u003erevoke\u003c/span\u003e \u003cspan class=\"k\"\u003eusage\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eselect\u003c/span\u003e \u003cspan class=\"k\"\u003eon\u003c/span\u003e \u003cspan class=\"n\"\u003esequences\u003c/span\u003e \u003cspan class=\"k\"\u003efrom\u003c/span\u003e \u003cspan class=\"n\"\u003eanon\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eservice_role\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"274:1-275:198\"\u003eこれは\u003cstrong\u003e以後に作るテーブル\u003c/strong\u003eの既定の権限を変えるもので、既存テーブルの権限は外しません。\u003cbr\u003e\nいきなり本番で試すのが不安なら、開発用のプロジェクトで実行してから、テーブルを1つ作って \u003ccode\u003e42501\u003c/code\u003e と \u003ccode\u003ehint\u003c/code\u003e を確認してみるのがおすすめです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"277:1-277:45\"\u003e\n\u003cspan id=\"ついでにやっておくとよいこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A4%E3%81%84%E3%81%A7%E3%81%AB%E3%82%84%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8F%E3%81%A8%E3%82%88%E3%81%84%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eついでにやっておくとよいこと\u003c/h2\u003e\n\u003cul data-sourcepos=\"279:1-282:0\"\u003e\n\u003cli data-sourcepos=\"279:1-279:186\"\u003e\n\u003cstrong\u003e既定の権限に頼らない\u003c/strong\u003e: 「勝手に付く」前提をやめる。migration を読めば権限が全部分かる状態にしておくと、レビューもしやすくなる\u003c/li\u003e\n\u003cli data-sourcepos=\"280:1-280:245\"\u003e\n\u003cstrong\u003e\u003ccode\u003eanon\u003c/code\u003e に何が付いているかを棚卸しする\u003c/strong\u003e: 自動付与で付いていた不要な \u003ccode\u003eanon\u003c/code\u003e 権限は、外しておくと安全側になる。ただし、外す前に未ログインで使う機能が無いかを必ず確認する\u003c/li\u003e\n\u003cli data-sourcepos=\"281:1-282:0\"\u003e\n\u003cstrong\u003eSecurity Advisor を確認する\u003c/strong\u003e: ダッシュボードの Advisors で、RLS 未設定のテーブルなどの警告も合わせて見ておく\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"283:1-283:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"285:1-290:0\"\u003e\n\u003cli data-sourcepos=\"285:1-285:132\"\u003e2026-10-30 以降、public の新規テーブルには \u003ccode\u003eanon\u003c/code\u003e / \u003ccode\u003eauthenticated\u003c/code\u003e / \u003ccode\u003eservice_role\u003c/code\u003e の権限が自動で付かない\u003c/li\u003e\n\u003cli data-sourcepos=\"286:1-286:104\"\u003eGRANT は外側の門、RLS は内側の門。GRANT が無いと RLS 以前に \u003ccode\u003e42501\u003c/code\u003e で弾かれる\u003c/li\u003e\n\u003cli data-sourcepos=\"287:1-287:111\"\u003e\n\u003ccode\u003e42501\u003c/code\u003e は \u003ccode\u003emessage\u003c/code\u003e と \u003ccode\u003ehint\u003c/code\u003e で原因を見分ける。\u003ccode\u003epermission denied for table\u003c/code\u003e なら GRANT 不足\u003c/li\u003e\n\u003cli data-sourcepos=\"288:1-288:143\"\u003e新しいテーブルの migration には、RLS・ポリシーと一緒に GRANT を書く。付けるのは必要なロール・操作だけ\u003c/li\u003e\n\u003cli data-sourcepos=\"289:1-290:0\"\u003e本番が無事でも、スキーマの正本に GRANT が無ければ作り直した環境で壊れる。今のうちに書いておく\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"291:1-291:9\"\u003e\n\u003cspan id=\"参考\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考\u003c/h2\u003e\n\u003cul data-sourcepos=\"293:1-298:99\"\u003e\n\u003cli data-sourcepos=\"293:1-293:212\"\u003e\u003ca href=\"https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically\" rel=\"nofollow noopener\" target=\"_blank\"\u003eBreaking Change: Tables not exposed to Data and GraphQL API automatically（Supabase Changelog）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"294:1-294:106\"\u003e\u003ca href=\"https://github.com/orgs/supabase/discussions/45329\" rel=\"nofollow noopener\" target=\"_blank\"\u003eDiscussion #45329（Supabase GitHub Discussions）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"295:1-295:96\"\u003e\u003ca href=\"https://supabase.com/docs/guides/api/securing-your-api\" rel=\"nofollow noopener\" target=\"_blank\"\u003eSecuring your API（Supabase Docs）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"296:1-296:112\"\u003e\u003ca href=\"https://supabase.com/docs/guides/database/postgres/row-level-security\" rel=\"nofollow noopener\" target=\"_blank\"\u003eRow Level Security（Supabase Docs）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"297:1-297:110\"\u003e\u003ca href=\"https://www.postgresql.org/docs/current/sql-createpolicy.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCREATE POLICY（PostgreSQL Documentation）\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"298:1-298:99\"\u003e\u003ca href=\"https://www.postgresql.org/docs/current/ddl-priv.html\" rel=\"nofollow noopener\" target=\"_blank\"\u003ePrivileges（PostgreSQL Documentation）\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":":::note info\nこの記事は、筆者の開発メモをもとに AI（Claude）と下書きを作り、Supabase 公式の告知で事実を確認したうえで仕上げています。\n:::\n\n## はじめに\n\nSupabase は **2026-10-30 から、既存の全プロジェクトで public スキーマの新規テーブルへの権限（GRANT）の自動付与をやめます**。\nこれからは、テーブルを作っただけでは Data API（PostgREST / supabase-js）から触れません。\n\n筆者は期限前から、この状態を一度踏んでいます。\nRLS ポリシーを正しく書いたつもりなのに、新しく作ったテーブルへの操作が全部 `42501` で失敗しました。\nエラーコードが RLS 違反と同じなので、ポリシーの書き間違いを疑って遠回りしました。\n\nこの記事は、Supabase を使っていて SQL Editor や migration でテーブルを作っている方に向けたものです。\n読み終えると、次の2つができるようになります。\n\n- 自分のプロジェクトが影響を受けるかを SQL で確認する\n- migration に GRANT を書く型を、そのまま持ち帰る\n\n## TL;DR\n\n- **2026-10-30 以降、public の新規テーブルには `anon` / `authenticated` / `service_role` の権限が自動で付かない**。既存テーブルの権限はそのまま\n- **GRANT と RLS は別の層**。GRANT が無いと、RLS を評価する前に `42501 permission denied` で弾かれる\n- 同じ `42501` でも、**メッセージを見れば GRANT 不足か RLS 違反かを見分けられる**\n- **新しいテーブルの migration には GRANT を必ず書く**。必要なロール・必要な操作だけに絞り、`anon` には安易に付けない\n- 本番が無事でも、**migration から環境を作り直すと全テーブルが `42501` になる**ことがある。スキーマの正本にも GRANT を書いておく\n\n## 何が変わるのか\n\n公式の告知（[Changelog](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically)）から、要点をまとめます。\n\n| 日付 | 内容 |\n|---|---|\n| 2026-04-28 | プロジェクト作成時に新しい挙動を選べるようになった |\n| 2026-05-30 | **新規プロジェクト**は新しい挙動がデフォルトになった |\n| 2026-10-30 | **既存の全プロジェクト**に新しい挙動が適用される |\n\n変更の中身は次のとおりです。\n\n- これまでは、public スキーマに作ったテーブルに `anon` / `authenticated` / `service_role` の `SELECT, INSERT, UPDATE, DELETE` が**既定の権限（default privileges）で自動的に**付いていた\n- これからは、この既定の権限が外れる。Data API から使うテーブルには、**明示的な GRANT が必要**になる\n- **既存テーブルは影響を受けない**。付いている権限はそのまま残る\n- 対象は Data API（PostgREST / GraphQL）経由のアクセス。Postgres への直接接続は関係ない\n\n:::note warn\n`service_role` も対象です。サーバー側で `service_role` キーを使っていると「RLS をバイパスするから大丈夫」と思いがちですが、**RLS をバイパスできても GRANT はバイパスできません**。サーバーからだけ使うテーブルでも、`service_role` への GRANT が必要です。\n:::\n\n影響が出るのは、大きく次の2つです。\n\n1. **これから作るテーブル**\n2. **migration やスキーマ定義から環境を作り直すとき**（新しいプロジェクト、ローカルでの `supabase db reset` など）\n\n2つ目は見落としやすいポイントです。\n公式の Discussion でも、新しいデフォルトで `supabase db reset` をすると過去のテーブルに GRANT が付かない、という点が取り上げられています（[Discussion #45329](https://github.com/orgs/supabase/discussions/45329)）。\n\n## GRANT と RLS は別の層\n\nまず、なぜ「RLS を正しく書いたのに動かない」のかを整理します。\nPostgres は、Data API から来たリクエストを2段階でチェックします。\n\n```mermaid\nflowchart LR\n    A[リクエスト\u003cbr/\u003eanon / authenticated] --\u003e B{テーブル権限\u003cbr/\u003eGRANT}\n    B --\u003e|無い| X[42501\u003cbr/\u003epermission denied for table]\n    B --\u003e|ある| C{RLS ポリシー}\n    C --\u003e|合わない| Y[0件 or 42501\u003cbr/\u003enew row violates RLS]\n    C --\u003e|合う| D[行を返す・書き込む]\n```\n\n| 層 | 決めること | 例 |\n|---|---|---|\n| テーブル権限（GRANT） | そのロールが、そのテーブルを**そもそも操作してよいか** | `authenticated` は `notes` に INSERT できる |\n| RLS（行レベルセキュリティ） | そのロールが、**どの行を**見てよいか・書いてよいか | 自分の `owner_id` の行だけ |\n\nGRANT は外側の門で、RLS はその内側の門です。\n**外側の門が閉じていると、内側の RLS は評価すらされません**。\nだから、ポリシーをどれだけ正しく書いても `42501` のままです。\n\n公式ドキュメントも「公開するオブジェクトには両方を使うこと」と書いています（[Securing your API](https://supabase.com/docs/guides/api/securing-your-api)）。\n\n## 実際にハマった例：期限前から起きていた\n\n筆者の場合、既存のテーブルは問題なく動いていました。\nところが、後から SQL Editor で `create table` した新しいテーブルだけ、アプリからの操作がすべて失敗しました。\n\n```text\ncode: 42501\nmessage: permission denied for table xxx\n```\n\nRLS を有効にしてポリシーも書いた直後だったので、まずポリシーを疑いました。\nしかし、原因は**新しいテーブルに `authenticated` への GRANT が付いていなかった**ことでした。\n既存テーブルには以前から権限が付いていたため、それまで問題が表に出なかったのです。\n\n振り返ると、今回の廃止は「条件によってはすでに起きていたこと」が、全プロジェクトの新規テーブルに広がる変更だと言えます。\n\n## `42501` の見分け方：同じコードで原因が2つ\n\n`42501` は「権限不足」を表す Postgres のエラーコードです。\nGRANT が無いときも、RLS に弾かれたときも同じコードになります。\n見分けるにはメッセージを見ます。\n\n| メッセージ | 原因 | 対処 |\n|---|---|---|\n| `permission denied for table xxx` | テーブル権限（GRANT）が無い | GRANT を付ける |\n| `new row violates row-level security policy for table \"xxx\"` | RLS ポリシーに合わない | ポリシーまたはクエリを見直す |\n\n新しい挙動では、GRANT 不足のときに **`hint` に必要な GRANT 文が入って返ってきます**（公式 Changelog より）。\nエラーオブジェクトは `message` だけでなく `hint` まで見るのがおすすめです。\n\n```typescript\nconst { error } = await supabase\n  .from('notes')\n  .insert({ body: 'test', owner_id: userId })\n\nif (error) {\n  console.log(error.code, error.message, error.hint)\n  // 42501 / permission denied for table notes          → GRANT が無い\n  // 42501 / new row violates row-level security policy → RLS に合わない\n}\n```\n\n### 補足：RLS 側でよくある落とし穴\n\nGRANT とは別の原因ですが、見分けるときに一緒に知っておくと便利なものを1つ挙げます。\n\nsupabase-js の `insert().select()` は、SQL でいうと `INSERT ... RETURNING` です。\nこのとき Postgres は、**挿入した行が SELECT ポリシーも満たすこと**を求めます（[PostgreSQL: CREATE POLICY](https://www.postgresql.org/docs/current/sql-createpolicy.html)）。\n\nたとえば SELECT ポリシーが「トリガーで後から作られる別テーブルの行」を前提にしていると、INSERT ポリシーは満たしていても失敗します。\nINSERT ポリシーを何度見直しても直らないときは、SELECT ポリシーも確認してみてください。\n\n## 自分のプロジェクトが影響を受けるか確かめる\n\n### 既存テーブルの権限を見る\n\nSQL Editor で次のどちらかを実行します。\n\n```sql\n-- テーブルごとの権限（ACL）をまとめて見る\nselect relname, relacl\nfrom pg_class\nwhere relnamespace = 'public'::regnamespace\n  and relkind = 'r'\norder by relname;\n```\n\n`relacl` に `authenticated=arwd...` のような表示があれば付与済みです。\n文字の意味は、`r`=SELECT、`a`=INSERT、`w`=UPDATE、`d`=DELETE です（[PostgreSQL: Privileges](https://www.postgresql.org/docs/current/ddl-priv.html)）。\n\nロールと操作を1行ずつ見たい場合は、こちらが読みやすいです。\n\n```sql\nselect table_name, grantee, privilege_type\nfrom information_schema.role_table_grants\nwhere table_schema = 'public'\n  and grantee in ('anon', 'authenticated', 'service_role')\norder by table_name, grantee, privilege_type;\n```\n\n### 確認するポイント\n\n| 確認すること | 状態 | 判断 |\n|---|---|---|\n| 本番の既存テーブルに権限が付いているか | 付いている | 当面は影響なし |\n| migration / スキーマ定義に GRANT が書かれているか | 書かれていない | **環境を作り直すと全テーブルで `42501`** |\n| 今後テーブルを追加する予定があるか | ある | migration に GRANT を書く運用に切り替える |\n\n本番が無事でも、2行目に当てはまるなら対応しておく価値があります。\n「本番の権限は、過去の自動付与のおかげで付いているだけ」という状態だからです。\n\n## 対策：migration に GRANT を明示する\n\n### 新しいテーブルの migration の型\n\n**テーブル作成・RLS 有効化・ポリシー・GRANT を、同じ migration ファイルにセットで書きます**。\n別ファイルに分けると、GRANT だけ適用し忘れる事故が起きやすくなります。\n\n```sql\ncreate table if not exists public.notes (\n  id uuid primary key default gen_random_uuid(),\n  owner_id uuid not null references auth.users (id) on delete cascade,\n  body text not null,\n  created_at timestamptz not null default now()\n);\n\nalter table public.notes enable row level security;\n\ncreate policy \"notes: 自分の行だけ読める\" on public.notes\n  for select to authenticated\n  using ((select auth.uid()) = owner_id);\n\ncreate policy \"notes: 自分の行だけ書ける\" on public.notes\n  for insert to authenticated\n  with check ((select auth.uid()) = owner_id);\n\n-- テーブル権限（RLS の前段）。2026-10-30 以降は自動で付かないので明示する。\n-- ログインユーザーだけが使うテーブルなので anon には付けない。\ngrant select, insert, update, delete on table public.notes to authenticated;\n```\n\nポリシー内の `auth.uid()` を `(select auth.uid())` で包んでいるのは、行ごとの再評価を避けるためです（[Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select)）。\n\n### 付ける権限の決め方\n\n自動付与は「3ロールに4操作すべて」でした。\n明示するなら、**必要なものだけ**に絞れます。\n\n| テーブルの使われ方 | 付けるもの |\n|---|---|\n| ログインユーザーだけが読み書きする | `authenticated` に必要な操作だけ |\n| 未ログインでも読める公開データ | `anon` に `select` だけ（＋RLS で行を絞る） |\n| 読むだけのマスタ | `select` だけ |\n| サーバー（`service_role`）からも操作する | `service_role` にも必要な操作を付ける |\n\n`anon` に付けると、未ログインでのアクセス経路が1つ増えます。\nRLS で守られていても、**使わない経路は最初から作らない**方が安全側です。\n\n### テーブル以外にも GRANT が要るもの\n\n公式ドキュメントと Discussion では、テーブル以外にも次のものが挙げられています。\n\n- **関数（RPC）**: `execute` 権限を明示する\n- **ビュー**: テーブルと同様に GRANT が要る。同じ migration で付ける\n- **シーケンス**: 既定の権限の取り消し対象に含まれる。`serial` 系の列を使う場合は確認する\n\n関数は、呼べるロールを絞る書き方にしておくと安心です。\nPostgres の関数は、既定で `PUBLIC`（全ロール）に `execute` が付くためです。\n\n```sql\nrevoke execute on function public.my_rpc(uuid) from public, anon;\ngrant  execute on function public.my_rpc(uuid) to authenticated;\n```\n\n### スキーマの正本にも既存テーブルぶんを書く\n\nスキーマ全体を1ファイル（`schema.sql` など）で管理している場合は、**既存の全テーブルぶんの GRANT も書いておきます**。\n本番にはすでに付いているので、実行しても何も変わりません。\n効くのは、そのファイルから環境を作り直したときだけです。\n\n### 小技：Supabase のロールが無い環境でも落ちない書き方\n\n型生成や CI のために、素の Postgres コンテナへスキーマを流すことがあります。\nその環境には `authenticated` などの Supabase 固有ロールが無いため、GRANT がエラーになります。\n`pg_roles` で存在を確認してから付けると、両方の環境で同じファイルを使えます。\n\n```sql\ndo $$\nbegin\n  if exists (select 1 from pg_roles where rolname = 'authenticated') then\n    grant select, insert, update, delete\n      on table public.table_a, public.table_b to authenticated;\n  end if;\nend $$;\n```\n\n:::note warn\n`create policy ... to authenticated` のようにポリシー側でもロールを参照している場合は、この書き方だけでは足りません。ポリシー作成時にもロールが必要になるため、その環境では先に `create role authenticated;` などでスタブのロールを作っておく必要があります。\n:::\n\n## 期限前に新しい挙動で試しておく\n\n10/30 を待たずに、既存プロジェクトを新しい挙動に切り替えることもできます。\n公式 Changelog では、SQL Editor で次を実行する方法が案内されています。\n\n```sql\nalter default privileges for role postgres in schema public\n  revoke select, insert, update, delete on tables from anon, authenticated, service_role;\nalter default privileges for role postgres in schema public\n  revoke usage, select on sequences from anon, authenticated, service_role;\n```\n\nこれは**以後に作るテーブル**の既定の権限を変えるもので、既存テーブルの権限は外しません。\nいきなり本番で試すのが不安なら、開発用のプロジェクトで実行してから、テーブルを1つ作って `42501` と `hint` を確認してみるのがおすすめです。\n\n## ついでにやっておくとよいこと\n\n- **既定の権限に頼らない**: 「勝手に付く」前提をやめる。migration を読めば権限が全部分かる状態にしておくと、レビューもしやすくなる\n- **`anon` に何が付いているかを棚卸しする**: 自動付与で付いていた不要な `anon` 権限は、外しておくと安全側になる。ただし、外す前に未ログインで使う機能が無いかを必ず確認する\n- **Security Advisor を確認する**: ダッシュボードの Advisors で、RLS 未設定のテーブルなどの警告も合わせて見ておく\n\n## まとめ\n\n- 2026-10-30 以降、public の新規テーブルには `anon` / `authenticated` / `service_role` の権限が自動で付かない\n- GRANT は外側の門、RLS は内側の門。GRANT が無いと RLS 以前に `42501` で弾かれる\n- `42501` は `message` と `hint` で原因を見分ける。`permission denied for table` なら GRANT 不足\n- 新しいテーブルの migration には、RLS・ポリシーと一緒に GRANT を書く。付けるのは必要なロール・操作だけ\n- 本番が無事でも、スキーマの正本に GRANT が無ければ作り直した環境で壊れる。今のうちに書いておく\n\n## 参考\n\n- [Breaking Change: Tables not exposed to Data and GraphQL API automatically（Supabase Changelog）](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically)\n- [Discussion #45329（Supabase GitHub Discussions）](https://github.com/orgs/supabase/discussions/45329)\n- [Securing your API（Supabase Docs）](https://supabase.com/docs/guides/api/securing-your-api)\n- [Row Level Security（Supabase Docs）](https://supabase.com/docs/guides/database/postgres/row-level-security)\n- [CREATE POLICY（PostgreSQL Documentation）](https://www.postgresql.org/docs/current/sql-createpolicy.html)\n- [Privileges（PostgreSQL Documentation）](https://www.postgresql.org/docs/current/ddl-priv.html)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:16:03+09:00","group":null,"id":"aedf32942bb49849f92b","likes_count":0,"private":false,"reactions_count":0,"stocks_count":1,"tags":[{"name":"Supabase","versions":[]},{"name":"PostgreSQL","versions":[]},{"name":"SQL","versions":[]},{"name":"Security","versions":[]},{"name":"個人開発","versions":[]}],"title":"Supabase の自動 GRANT が 2026-10-30 に廃止：RLS を書いたのに `42501` になる理由と、migration に GRANT を書く型","updated_at":"2026-09-27T11:17:24+09:00","url":"https://qiita.com/satoshi_061/items/aedf32942bb49849f92b","user":{"description":null,"facebook_id":null,"followees_count":4,"followers_count":1,"github_login_name":null,"id":"satoshi_061","items_count":17,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":590349,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590349/profile-images/1606889993","team_only":false,"twitter_screen_name":"nskwsatoshi_061","website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:311\"\u003e顧客案件でClaude Codeに作業を任せるとき、「こうしてほしい」は書いても、「これはしないでほしい」は書き忘れがちです。そして、書いたつもりでも、守られたかどうかを確かめる手段がないと、次の案件で同じ場所が抜けます。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:432\"\u003eこの記事では、\u003ccode\u003eCLAUDE.md\u003c/code\u003e(Claude Codeがセッション開始時に読み込む、プロジェクト用の指示ファイル)に「やらないこと」を書くときの考え方と、書くだけでは足りないものを hook と事後検査に分ける方法をまとめます。例は、この記事のために作った架空のプロジェクトのものです。実際の案件のルールは載せていません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"7:1-7:9\"\u003e\n\u003cspan id=\"環境\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%92%B0%E5%A2%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e環境\u003c/h2\u003e\n\u003cul data-sourcepos=\"9:1-11:0\"\u003e\n\u003cli data-sourcepos=\"9:1-9:51\"\u003emacOS 26.6.2、Node.js v22.22.3、git 2.54.0、jq\u003c/li\u003e\n\u003cli data-sourcepos=\"10:1-11:0\"\u003eClaude Code 2.1.283(2026-09-27に確認)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"12:1-12:99\"\u003ehook や \u003ccode\u003eCLAUDE.md\u003c/code\u003e の挙動は、バージョンによって変わる可能性があります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"14:1-14:36\"\u003e\n\u003cspan id=\"この記事をざっくり図解\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92%E3%81%96%E3%81%A3%E3%81%8F%E3%82%8A%E5%9B%B3%E8%A7%A3\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの記事をざっくり図解\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"16:1-22:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e[CLAUDE.md]  方針と理由を伝える。守るかどうかはAIの判断に任せる\n     ↓ 取り返しがつかないものだけ\n[PreToolUse hook]  コマンドを実行する前に機械的に止める\n     ↓ 実行後に確かめたいものだけ\n[事後検査(grep / pre-commit / CI)]  差分に禁止事項が入っていないかを調べる\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"24:1-24:29\"\u003e\n\u003cspan id=\"1-claudemdに書くもの\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-claudemd%E3%81%AB%E6%9B%B8%E3%81%8F%E3%82%82%E3%81%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1 CLAUDE.mdに書くもの\u003c/h2\u003e\n\u003cp data-sourcepos=\"26:1-26:177\"\u003e私は、書く内容を3つに分けています。「やること(規約)」「やらないこと」「確認してから進めること」です。次が架空の例です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"markdown\" data-sourcepos=\"28:1-52:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"gh\"\u003e# CLAUDE.md(架空の受発注管理システム用の例)\u003c/span\u003e\n\n\u003cspan class=\"gu\"\u003e## このプロジェクトについて\u003c/span\u003e\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e 受発注データを管理する社内向けWebアプリ。Node.js 22 / PostgreSQL\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e テストは \u003cspan class=\"sb\"\u003e`npm test`\u003c/span\u003e、Lintは \u003cspan class=\"sb\"\u003e`npm run lint`\u003c/span\u003e\n\n\u003cspan class=\"gu\"\u003e## やること(規約)\u003c/span\u003e\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e ブランチ名は \u003cspan class=\"sb\"\u003e`feature/{チケット番号}-{短い説明}`\u003c/span\u003e\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e コミットメッセージは Conventional Commits。AIの署名や生成元の記載は入れない\n\n\u003cspan class=\"gu\"\u003e## やらないこと\u003c/span\u003e\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e main に直接 push しない。force push もしない\n\u003cspan class=\"p\"\u003e  -\u003c/span\u003e 理由: 履歴を戻せなくなる。代わりに: ブランチを切って PR にする\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"sb\"\u003e`--no-verify`\u003c/span\u003e でコミット時の検査を飛ばさない\n\u003cspan class=\"p\"\u003e  -\u003c/span\u003e 理由: 検査の意味がなくなる。代わりに: 落ちた原因を直す\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e 顧客名、担当者名、契約金額を、コード・コメント・コミットメッセージに書かない\n\u003cspan class=\"p\"\u003e  -\u003c/span\u003e 理由: リポジトリは委託先とも共有される。代わりに: 案件コード(PJ-XXXX)を使う\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e \u003cspan class=\"sb\"\u003e`.env`\u003c/span\u003e など認証情報のファイルを読まない、書かない\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e 本番DBに接続しない。確認は検証環境で行う\n\n\u003cspan class=\"gu\"\u003e## 確認してから進めること\u003c/span\u003e\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e DBのマイグレーションを追加するとき\n\u003cspan class=\"p\"\u003e-\u003c/span\u003e 既存APIのレスポンス形式を変えるとき\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003ch2 data-sourcepos=\"54:1-54:41\"\u003e\n\u003cspan id=\"2-やらないことの書き方\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E3%82%84%E3%82%89%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2 「やらないこと」の書き方\u003c/h2\u003e\n\u003cp data-sourcepos=\"56:1-56:64\"\u003e書くときに気をつけているのは、次の3点です。\u003c/p\u003e\n\u003col data-sourcepos=\"58:1-61:0\"\u003e\n\u003cli data-sourcepos=\"58:1-58:228\"\u003e\n\u003cstrong\u003e理由と、代わりにやることをセットで書く。\u003c/strong\u003e 「push しない」だけだと、AIは代替手段を自分で探します。「PR にする」まで書いてあれば、そちらに寄せてくれます。\u003c/li\u003e\n\u003cli data-sourcepos=\"59:1-59:352\"\u003e\n\u003cstrong\u003e数を絞る。\u003c/strong\u003e 公式ドキュメントは、1つの \u003ccode\u003eCLAUDE.md\u003c/code\u003e を200行以下にすることを勧めていて、長くなるほど遵守率が下がるとも書いています。矛盾する指示があると、どちらかが任意に選ばれる、という記述もあります。なんでも書くと、大事な行が埋もれます。\u003c/li\u003e\n\u003cli data-sourcepos=\"60:1-61:0\"\u003e\n\u003cstrong\u003e検査できる形で書く。\u003c/strong\u003e 「適切に対応する」ではなく、「\u003ccode\u003e--no-verify\u003c/code\u003e を使わない」のように、あとで grep で確かめられる文にします。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-sourcepos=\"62:1-62:29\"\u003e\n\u003cspan id=\"3-どこで守らせるか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E3%81%A9%E3%81%93%E3%81%A7%E5%AE%88%E3%82%89%E3%81%9B%E3%82%8B%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3 どこで守らせるか\u003c/h2\u003e\n\u003cp data-sourcepos=\"64:1-64:105\"\u003eすべてを hook にする必要はありません。書いた規則を、置き場所で分けます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"66:1-73:86\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"66:1-66:34\"\u003e\n\u003cth data-sourcepos=\"66:2-66:9\"\u003e規則\u003c/th\u003e\n\u003cth data-sourcepos=\"66:11-66:24\"\u003e置き場所\u003c/th\u003e\n\u003cth data-sourcepos=\"66:26-66:33\"\u003e理由\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"68:1-68:116\"\u003e\n\u003ctd data-sourcepos=\"68:2-68:48\"\u003emain への push、force push、\u003ccode\u003e--no-verify\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-sourcepos=\"68:50-68:55\"\u003ehook\u003c/td\u003e\n\u003ctd data-sourcepos=\"68:57-68:115\"\u003eコマンド文字列に現れ、事故ると戻せない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"69:1-69:103\"\u003e\n\u003ctd data-sourcepos=\"69:2-69:27\"\u003e顧客名を書かない\u003c/td\u003e\n\u003ctd data-sourcepos=\"69:29-69:42\"\u003e事後検査\u003c/td\u003e\n\u003ctd data-sourcepos=\"69:44-69:102\"\u003eコマンドには現れず、書かれた内容に出る\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"70:1-70:91\"\u003e\n\u003ctd data-sourcepos=\"70:2-70:30\"\u003e認証情報の読み書き\u003c/td\u003e\n\u003ctd data-sourcepos=\"70:32-70:37\"\u003ehook\u003c/td\u003e\n\u003ctd data-sourcepos=\"70:39-70:90\"\u003eツール呼び出しで判定できる(別記事)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"71:1-71:113\"\u003e\n\u003ctd data-sourcepos=\"71:2-71:29\"\u003e本番DBに接続しない\u003c/td\u003e\n\u003ctd data-sourcepos=\"71:31-71:50\"\u003e権限で分ける\u003c/td\u003e\n\u003ctd data-sourcepos=\"71:52-71:112\"\u003ehook より前に、そもそも認証情報を渡さない\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"72:1-72:90\"\u003e\n\u003ctd data-sourcepos=\"72:2-72:39\"\u003eブランチ名、コミット形式\u003c/td\u003e\n\u003ctd data-sourcepos=\"72:41-72:56\"\u003eCLAUDE.md + CI\u003c/td\u003e\n\u003ctd data-sourcepos=\"72:58-72:89\"\u003e守られなくても直せる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"73:1-73:86\"\u003e\n\u003ctd data-sourcepos=\"73:2-73:36\"\u003e「確認してから進める」\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:38-73:55\"\u003eCLAUDE.md のみ\u003c/td\u003e\n\u003ctd data-sourcepos=\"73:57-73:85\"\u003e機械で判定できない\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"75:1-75:245\"\u003e公式ドキュメントにも、\u003ccode\u003eCLAUDE.md\u003c/code\u003e は「強制される設定としては扱われず、コンテキストとして読まれる」とあり、実行を確実に止めたいなら PreToolUse hook を使うよう書かれています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"77:1-77:21\"\u003e\n\u003cspan id=\"4-hookで止める\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-hook%E3%81%A7%E6%AD%A2%E3%82%81%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4 hookで止める\u003c/h2\u003e\n\u003cp data-sourcepos=\"79:1-79:120\"\u003eまず、止める規則を3つだけ書いた hook です。Bash の実行前に、コマンド文字列を見ます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"javascript\" data-sourcepos=\"81:1-131:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.claude/hooks/block-risky-bash.cjs\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"cp\"\u003e#!/usr/bin/env node\n\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003euse strict\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e// 「やらないこと」のうち、事故ると取り返しがつかないものだけをここで止める\u003c/span\u003e\n\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003eRULES\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eforce push\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b\u003c/span\u003e\u003cspan class=\"sr\"\u003egit\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\u003cspan class=\"sr\"\u003e+push\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b[^\u003c/span\u003e\u003cspan class=\"sr\"\u003e;\u0026amp;|\u003c/span\u003e\u003cspan class=\"se\"\u003e]\u003c/span\u003e\u003cspan class=\"sr\"\u003e*\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s(\u003c/span\u003e\u003cspan class=\"sr\"\u003e--force\u003c/span\u003e\u003cspan class=\"se\"\u003e(?![\\w\u003c/span\u003e\u003cspan class=\"sr\"\u003e-\u003c/span\u003e\u003cspan class=\"se\"\u003e])\u003c/span\u003e\u003cspan class=\"sr\"\u003e|-f\u003c/span\u003e\u003cspan class=\"se\"\u003e(?![\\w\u003c/span\u003e\u003cspan class=\"sr\"\u003e-\u003c/span\u003e\u003cspan class=\"se\"\u003e]))\u003c/span\u003e\u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eforce push は禁止です。必要なら人間に確認してください。\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003emain への直接 push\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b\u003c/span\u003e\u003cspan class=\"sr\"\u003egit\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\u003cspan class=\"sr\"\u003e+push\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b[^\u003c/span\u003e\u003cspan class=\"sr\"\u003e;\u0026amp;|\u003c/span\u003e\u003cspan class=\"se\"\u003e]\u003c/span\u003e\u003cspan class=\"sr\"\u003e*\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s(\u003c/span\u003e\u003cspan class=\"sr\"\u003eorigin\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\u003cspan class=\"sr\"\u003e+\u003c/span\u003e\u003cspan class=\"se\"\u003e)?\u003c/span\u003e\u003cspan class=\"sr\"\u003emain\u003c/span\u003e\u003cspan class=\"se\"\u003e(?![\\w\u003c/span\u003e\u003cspan class=\"sr\"\u003e.\u003c/span\u003e\u003cspan class=\"se\"\u003e/\u003c/span\u003e\u003cspan class=\"sr\"\u003e-\u003c/span\u003e\u003cspan class=\"se\"\u003e])\u003c/span\u003e\u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003emain への直接 push は禁止です。ブランチを切って PR にしてください。\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e検査の回避\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b\u003c/span\u003e\u003cspan class=\"sr\"\u003egit\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\u003cspan class=\"sr\"\u003e+commit\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b[^\u003c/span\u003e\u003cspan class=\"sr\"\u003e;\u0026amp;|\u003c/span\u003e\u003cspan class=\"se\"\u003e]\u003c/span\u003e\u003cspan class=\"sr\"\u003e*\u003c/span\u003e\u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\u003cspan class=\"sr\"\u003e--no-verify\u003c/span\u003e\u003cspan class=\"se\"\u003e\\b\u003c/span\u003e\u003cspan class=\"sr\"\u003e/\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"na\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003e--no-verify は禁止です。検査で落ちた理由を直してください。\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003ereadStdin\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"k\"\u003eawait \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003echunk\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"nx\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003estdin\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"nx\"\u003echunk\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nx\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"kd\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n  \u003cspan class=\"kd\"\u003elet\u003c/span\u003e \u003cspan class=\"nx\"\u003epayload\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003etry\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003epayload\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003eJSON\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eparse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"nf\"\u003ereadStdin\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"nx\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eexit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 読めない入力は止めない（通常の許可フローに任せる）\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n  \u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003ecommand\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003epayload\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"nx\"\u003etool_input\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"nx\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n  \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003epayload\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"nx\"\u003etool_name\u003c/span\u003e \u003cspan class=\"o\"\u003e!==\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003eBash\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"k\"\u003etypeof\u003c/span\u003e \u003cspan class=\"nx\"\u003ecommand\u003c/span\u003e \u003cspan class=\"o\"\u003e!==\u003c/span\u003e \u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"s2\"\u003estring\u003c/span\u003e\u003cspan class=\"dl\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nx\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eexit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\n  \u003cspan class=\"k\"\u003efor \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"nx\"\u003erule\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"nx\"\u003eRULES\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n    \u003cspan class=\"k\"\u003eif \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003erule\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003econsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e`BLOCKED (\u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003erule\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e): \u003c/span\u003e\u003cspan class=\"p\"\u003e${\u003c/span\u003e\u003cspan class=\"nx\"\u003erule\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e`\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n      \u003cspan class=\"nx\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eexit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n  \u003cspan class=\"nx\"\u003eprocess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eexit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"133:1-133:216\"\u003e\u003ccode\u003e--force-with-lease\u003c/code\u003e は止めない設計にしています。\u003ccode\u003emain\u003c/code\u003e の後ろに続く文字を見ているのは、\u003ccode\u003e\\b\u003c/code\u003e だけだと \u003ccode\u003emain-backup\u003c/code\u003e のようなブランチ名まで止めてしまうためです。\u003c/p\u003e\n\u003cp data-sourcepos=\"135:1-135:33\"\u003e流し込んで確かめます。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"137:1-156:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e#!/usr/bin/env bash\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# 各コマンドをhookに流して、終了コードを見る\u003c/span\u003e\ncheck\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n  jq \u003cspan class=\"nt\"\u003e-n\u003c/span\u003e \u003cspan class=\"nt\"\u003e--arg\u003c/span\u003e c \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s1\"\u003e'{tool_name:\"Bash\",tool_input:{command:$c}}'\u003c/span\u003e | node block-risky-bash.cjs 2\u0026gt;/dev/null\n  \u003cspan class=\"nb\"\u003eprintf\u003c/span\u003e \u003cspan class=\"s1\"\u003e'exit=%s  %s\\n'\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$?\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$1\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"o\"\u003e}\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin feature/login'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push --force origin feature/login'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push -f origin feature/login'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push --force-with-lease origin feature/login'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin main'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git commit -m \"fix\" --no-verify'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin +feature/login'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin HEAD:main'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'sh -c \"git push -f\"'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'./scripts/deploy.sh'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin main-backup'\u003c/span\u003e\ncheck \u003cspan class=\"s1\"\u003e'git push origin main'\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"158:1-171:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eexit=0  git push origin feature/login\nexit=2  git push --force origin feature/login\nexit=2  git push -f origin feature/login\nexit=0  git push --force-with-lease origin feature/login\nexit=2  git push origin main\nexit=2  git commit -m \"fix\" --no-verify\nexit=0  git push origin +feature/login\nexit=0  git push origin HEAD:main\nexit=2  sh -c \"git push -f\"\nexit=0  ./scripts/deploy.sh\nexit=0  git push origin main-backup\nexit=2  git push origin main\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"173:1-173:85\"\u003e登録は \u003ccode\u003esettings.json\u003c/code\u003e です(スクリプトには \u003ccode\u003echmod +x\u003c/code\u003e が必要です)。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"json\" data-sourcepos=\"175:1-188:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.claude/settings.json\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"PreToolUse\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"matcher\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"Bash\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"hooks\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"w\"\u003e\n          \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"type\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nl\"\u003e\"command\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-risky-bash.cjs\"\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n        \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n      \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n    \u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n  \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"190:1-190:26\"\u003e\n\u003cspan id=\"5-事後に検査する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E4%BA%8B%E5%BE%8C%E3%81%AB%E6%A4%9C%E6%9F%BB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5 事後に検査する\u003c/h2\u003e\n\u003cp data-sourcepos=\"192:1-192:312\"\u003e「顧客名を書かない」は、コマンドには現れず差分の中身に出るので、コミット前に検査します。語のリストはリポジトリの外に置きます。リポジトリに顧客名を書かないための規則なのに、リストを入れたら本末転倒だからです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"194:1-209:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003echeck-banned-words.sh\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c\"\u003e#!/usr/bin/env bash\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# ステージ済みの差分に、書いてはいけない語（顧客名など）が入っていないか調べる\u003c/span\u003e\n\u003cspan class=\"c\"\u003e# 語のリストはリポジトリの外に置く（リポジトリに顧客名を書かないため）\u003c/span\u003e\n\u003cspan class=\"nb\"\u003eset\u003c/span\u003e \u003cspan class=\"nt\"\u003e-euo\u003c/span\u003e pipefail\n\n\u003cspan class=\"nv\"\u003eLIST\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"k\"\u003e${\u003c/span\u003e\u003cspan class=\"nv\"\u003eBANNED_WORDS_FILE\u003c/span\u003e\u003cspan class=\"k\"\u003e:-\u003c/span\u003e\u003cspan class=\"nv\"\u003e$HOME\u003c/span\u003e\u003cspan class=\"p\"\u003e/.config/project-banned-words.txt\u003c/span\u003e\u003cspan class=\"k\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\u003cspan class=\"o\"\u003e[\u003c/span\u003e \u003cspan class=\"nt\"\u003e-f\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$LIST\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e||\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"banned words file not found: \u003c/span\u003e\u003cspan class=\"nv\"\u003e$LIST\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u0026amp;2\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"nb\"\u003eexit \u003c/span\u003e1\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\n\u003cspan class=\"c\"\u003e# 追加された行だけを対象にする（+++ のファイル名行は除く）\u003c/span\u003e\n\u003cspan class=\"k\"\u003eif \u003c/span\u003egit diff \u003cspan class=\"nt\"\u003e--cached\u003c/span\u003e \u003cspan class=\"nt\"\u003e-U0\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-E\u003c/span\u003e \u003cspan class=\"s1\"\u003e'^\\+'\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-v\u003c/span\u003e \u003cspan class=\"s1\"\u003e'^+++'\u003c/span\u003e | \u003cspan class=\"nb\"\u003egrep\u003c/span\u003e \u003cspan class=\"nt\"\u003e-i\u003c/span\u003e \u003cspan class=\"nt\"\u003e-F\u003c/span\u003e \u003cspan class=\"nt\"\u003e-f\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"nv\"\u003e$LIST\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003ethen\n  \u003c/span\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"NG: 書いてはいけない語が含まれています。上の行を直してください。\"\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u0026amp;2\n  \u003cspan class=\"nb\"\u003eexit \u003c/span\u003e1\n\u003cspan class=\"k\"\u003efi\n\u003c/span\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"OK\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"211:1-211:147\"\u003e一時リポジトリで確かめると、通常の差分は \u003ccode\u003eOK\u003c/code\u003e、禁止語を含む差分は行を表示して終了コード1になります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"213:1-217:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003eOK\n+// acmetrading 用の暫定対応\nNG: 書いてはいけない語が含まれています。上の行を直してください。\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"219:1-219:131\"\u003epre-commit hook や CI に組み込めば、AI が書いた変更も人が書いた変更も、同じ基準で確認できます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"221:1-221:21\"\u003e\n\u003cspan id=\"できないこと\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eできないこと\u003c/h2\u003e\n\u003cul data-sourcepos=\"223:1-228:0\"\u003e\n\u003cli data-sourcepos=\"223:1-223:190\"\u003e\n\u003cstrong\u003e\u003ccode\u003eCLAUDE.md\u003c/code\u003e は強制ではありません。\u003c/strong\u003e 長い指示や、矛盾する指示は守られにくくなります。書いたから安心、とは考えないほうがよいです。\u003c/li\u003e\n\u003cli data-sourcepos=\"224:1-224:513\"\u003e\n\u003cstrong\u003ehook が見ているのは、ツール呼び出しだけです。\u003c/strong\u003e 上のテストでも、抜け道が見つかりました。\u003ccode\u003egit push origin +feature/login\u003c/code\u003e(\u003ccode\u003e+\u003c/code\u003e を付けた refspec は force push と同じ動きです)、\u003ccode\u003egit push origin HEAD:main\u003c/code\u003e、中で force push をするシェルスクリプト(\u003ccode\u003e./scripts/deploy.sh\u003c/code\u003e)は、いずれも exit=0 で通ります。正規表現を足せば塞げるものもありますが、コマンド文字列で判定する以上、完全にはなりません。\u003c/li\u003e\n\u003cli data-sourcepos=\"225:1-225:227\"\u003e\n\u003cstrong\u003ehook 自体を AI が書き換えられる状態では、意味が薄れます。\u003c/strong\u003e hook ファイルの編集を権限で制限するなどの併用を検討してください(この記事では検証していません)。\u003c/li\u003e\n\u003cli data-sourcepos=\"226:1-226:184\"\u003e\n\u003cstrong\u003e事後検査は、追加された行の文字列一致だけです。\u003c/strong\u003e 略称や表記ゆれは通りますし、語のリストは人が更新し続ける必要があります。\u003c/li\u003e\n\u003cli data-sourcepos=\"227:1-228:0\"\u003e実際の案件で使っているルールの一覧や検知パターンは、案件ごとに調整しているため、この記事には載せていません。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"229:1-229:12\"\u003e\n\u003cspan id=\"最後に\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%9C%80%E5%BE%8C%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e最後に\u003c/h2\u003e\n\u003cp data-sourcepos=\"231:1-231:404\"\u003e「やらないこと」は、書く場所によって守られ方が変わります。理由を添えて \u003ccode\u003eCLAUDE.md\u003c/code\u003e に書き、事故ると戻せないものは hook で止め、内容に出るものは事後に検査する。この3段に分けるだけでも、書き忘れや抜けは見つけやすくなります。気になったところから、手元の環境で試してみてください。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"233:1-233:15\"\u003e\n\u003cspan id=\"参考文献\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考文献\u003c/h2\u003e\n\u003cul data-sourcepos=\"235:1-236:105\"\u003e\n\u003cli data-sourcepos=\"235:1-235:124\"\u003eClaude Code Docs「How Claude remembers your project」(CLAUDE.md、サイズと矛盾の注意、hook との使い分け)\u003c/li\u003e\n\u003cli data-sourcepos=\"236:1-236:105\"\u003eClaude Code Docs「Hooks reference」(PreToolUse の入力JSON、exit code 2、settings.json の書式)\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## はじめに\n\n顧客案件でClaude Codeに作業を任せるとき、「こうしてほしい」は書いても、「これはしないでほしい」は書き忘れがちです。そして、書いたつもりでも、守られたかどうかを確かめる手段がないと、次の案件で同じ場所が抜けます。\n\nこの記事では、`CLAUDE.md`(Claude Codeがセッション開始時に読み込む、プロジェクト用の指示ファイル)に「やらないこと」を書くときの考え方と、書くだけでは足りないものを hook と事後検査に分ける方法をまとめます。例は、この記事のために作った架空のプロジェクトのものです。実際の案件のルールは載せていません。\n\n## 環境\n\n- macOS 26.6.2、Node.js v22.22.3、git 2.54.0、jq\n- Claude Code 2.1.283(2026-09-27に確認)\n\nhook や `CLAUDE.md` の挙動は、バージョンによって変わる可能性があります。\n\n## この記事をざっくり図解\n\n```text\n[CLAUDE.md]  方針と理由を伝える。守るかどうかはAIの判断に任せる\n     ↓ 取り返しがつかないものだけ\n[PreToolUse hook]  コマンドを実行する前に機械的に止める\n     ↓ 実行後に確かめたいものだけ\n[事後検査(grep / pre-commit / CI)]  差分に禁止事項が入っていないかを調べる\n```\n\n## 1 CLAUDE.mdに書くもの\n\n私は、書く内容を3つに分けています。「やること(規約)」「やらないこと」「確認してから進めること」です。次が架空の例です。\n\n```markdown\n# CLAUDE.md(架空の受発注管理システム用の例)\n\n## このプロジェクトについて\n- 受発注データを管理する社内向けWebアプリ。Node.js 22 / PostgreSQL\n- テストは `npm test`、Lintは `npm run lint`\n\n## やること(規約)\n- ブランチ名は `feature/{チケット番号}-{短い説明}`\n- コミットメッセージは Conventional Commits。AIの署名や生成元の記載は入れない\n\n## やらないこと\n- main に直接 push しない。force push もしない\n  - 理由: 履歴を戻せなくなる。代わりに: ブランチを切って PR にする\n- `--no-verify` でコミット時の検査を飛ばさない\n  - 理由: 検査の意味がなくなる。代わりに: 落ちた原因を直す\n- 顧客名、担当者名、契約金額を、コード・コメント・コミットメッセージに書かない\n  - 理由: リポジトリは委託先とも共有される。代わりに: 案件コード(PJ-XXXX)を使う\n- `.env` など認証情報のファイルを読まない、書かない\n- 本番DBに接続しない。確認は検証環境で行う\n\n## 確認してから進めること\n- DBのマイグレーションを追加するとき\n- 既存APIのレスポンス形式を変えるとき\n```\n\n## 2 「やらないこと」の書き方\n\n書くときに気をつけているのは、次の3点です。\n\n1. **理由と、代わりにやることをセットで書く。** 「push しない」だけだと、AIは代替手段を自分で探します。「PR にする」まで書いてあれば、そちらに寄せてくれます。\n2. **数を絞る。** 公式ドキュメントは、1つの `CLAUDE.md` を200行以下にすることを勧めていて、長くなるほど遵守率が下がるとも書いています。矛盾する指示があると、どちらかが任意に選ばれる、という記述もあります。なんでも書くと、大事な行が埋もれます。\n3. **検査できる形で書く。** 「適切に対応する」ではなく、「`--no-verify` を使わない」のように、あとで grep で確かめられる文にします。\n\n## 3 どこで守らせるか\n\nすべてを hook にする必要はありません。書いた規則を、置き場所で分けます。\n\n| 規則 | 置き場所 | 理由 |\n|---|---|---|\n| main への push、force push、`--no-verify` | hook | コマンド文字列に現れ、事故ると戻せない |\n| 顧客名を書かない | 事後検査 | コマンドには現れず、書かれた内容に出る |\n| 認証情報の読み書き | hook | ツール呼び出しで判定できる(別記事) |\n| 本番DBに接続しない | 権限で分ける | hook より前に、そもそも認証情報を渡さない |\n| ブランチ名、コミット形式 | CLAUDE.md + CI | 守られなくても直せる |\n| 「確認してから進める」 | CLAUDE.md のみ | 機械で判定できない |\n\n公式ドキュメントにも、`CLAUDE.md` は「強制される設定としては扱われず、コンテキストとして読まれる」とあり、実行を確実に止めたいなら PreToolUse hook を使うよう書かれています。\n\n## 4 hookで止める\n\nまず、止める規則を3つだけ書いた hook です。Bash の実行前に、コマンド文字列を見ます。\n\n```javascript:.claude/hooks/block-risky-bash.cjs\n#!/usr/bin/env node\n\"use strict\";\n\n// 「やらないこと」のうち、事故ると取り返しがつかないものだけをここで止める\nconst RULES = [\n  {\n    name: \"force push\",\n    pattern: /\\bgit\\s+push\\b[^;\u0026|]*\\s(--force(?![\\w-])|-f(?![\\w-]))/,\n    message: \"force push は禁止です。必要なら人間に確認してください。\",\n  },\n  {\n    name: \"main への直接 push\",\n    pattern: /\\bgit\\s+push\\b[^;\u0026|]*\\s(origin\\s+)?main(?![\\w./-])/,\n    message: \"main への直接 push は禁止です。ブランチを切って PR にしてください。\",\n  },\n  {\n    name: \"検査の回避\",\n    pattern: /\\bgit\\s+commit\\b[^;\u0026|]*\\s--no-verify\\b/,\n    message: \"--no-verify は禁止です。検査で落ちた理由を直してください。\",\n  },\n];\n\nasync function readStdin() {\n  let data = \"\";\n  for await (const chunk of process.stdin) data += chunk;\n  return data;\n}\n\nasync function main() {\n  let payload;\n  try {\n    payload = JSON.parse(await readStdin());\n  } catch {\n    process.exit(0); // 読めない入力は止めない（通常の許可フローに任せる）\n  }\n\n  const command = payload?.tool_input?.command;\n  if (payload?.tool_name !== \"Bash\" || typeof command !== \"string\") process.exit(0);\n\n  for (const rule of RULES) {\n    if (rule.pattern.test(command)) {\n      console.error(`BLOCKED (${rule.name}): ${rule.message}`);\n      process.exit(2);\n    }\n  }\n  process.exit(0);\n}\n\nmain();\n```\n\n`--force-with-lease` は止めない設計にしています。`main` の後ろに続く文字を見ているのは、`\\b` だけだと `main-backup` のようなブランチ名まで止めてしまうためです。\n\n流し込んで確かめます。\n\n```bash\n#!/usr/bin/env bash\n# 各コマンドをhookに流して、終了コードを見る\ncheck() {\n  jq -n --arg c \"$1\" '{tool_name:\"Bash\",tool_input:{command:$c}}' | node block-risky-bash.cjs 2\u003e/dev/null\n  printf 'exit=%s  %s\\n' \"$?\" \"$1\"\n}\ncheck 'git push origin feature/login'\ncheck 'git push --force origin feature/login'\ncheck 'git push -f origin feature/login'\ncheck 'git push --force-with-lease origin feature/login'\ncheck 'git push origin main'\ncheck 'git commit -m \"fix\" --no-verify'\ncheck 'git push origin +feature/login'\ncheck 'git push origin HEAD:main'\ncheck 'sh -c \"git push -f\"'\ncheck './scripts/deploy.sh'\ncheck 'git push origin main-backup'\ncheck 'git push origin main'\n```\n\n```text\nexit=0  git push origin feature/login\nexit=2  git push --force origin feature/login\nexit=2  git push -f origin feature/login\nexit=0  git push --force-with-lease origin feature/login\nexit=2  git push origin main\nexit=2  git commit -m \"fix\" --no-verify\nexit=0  git push origin +feature/login\nexit=0  git push origin HEAD:main\nexit=2  sh -c \"git push -f\"\nexit=0  ./scripts/deploy.sh\nexit=0  git push origin main-backup\nexit=2  git push origin main\n```\n\n登録は `settings.json` です(スクリプトには `chmod +x` が必要です)。\n\n```json:.claude/settings.json\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"Bash\",\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-risky-bash.cjs\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\n## 5 事後に検査する\n\n「顧客名を書かない」は、コマンドには現れず差分の中身に出るので、コミット前に検査します。語のリストはリポジトリの外に置きます。リポジトリに顧客名を書かないための規則なのに、リストを入れたら本末転倒だからです。\n\n```bash:check-banned-words.sh\n#!/usr/bin/env bash\n# ステージ済みの差分に、書いてはいけない語（顧客名など）が入っていないか調べる\n# 語のリストはリポジトリの外に置く（リポジトリに顧客名を書かないため）\nset -euo pipefail\n\nLIST=\"${BANNED_WORDS_FILE:-$HOME/.config/project-banned-words.txt}\"\n[ -f \"$LIST\" ] || { echo \"banned words file not found: $LIST\" \u003e\u00262; exit 1; }\n\n# 追加された行だけを対象にする（+++ のファイル名行は除く）\nif git diff --cached -U0 | grep -E '^\\+' | grep -v '^+++' | grep -i -F -f \"$LIST\"; then\n  echo \"NG: 書いてはいけない語が含まれています。上の行を直してください。\" \u003e\u00262\n  exit 1\nfi\necho \"OK\"\n```\n\n一時リポジトリで確かめると、通常の差分は `OK`、禁止語を含む差分は行を表示して終了コード1になります。\n\n```text\nOK\n+// acmetrading 用の暫定対応\nNG: 書いてはいけない語が含まれています。上の行を直してください。\n```\n\npre-commit hook や CI に組み込めば、AI が書いた変更も人が書いた変更も、同じ基準で確認できます。\n\n## できないこと\n\n- **`CLAUDE.md` は強制ではありません。** 長い指示や、矛盾する指示は守られにくくなります。書いたから安心、とは考えないほうがよいです。\n- **hook が見ているのは、ツール呼び出しだけです。** 上のテストでも、抜け道が見つかりました。`git push origin +feature/login`(`+` を付けた refspec は force push と同じ動きです)、`git push origin HEAD:main`、中で force push をするシェルスクリプト(`./scripts/deploy.sh`)は、いずれも exit=0 で通ります。正規表現を足せば塞げるものもありますが、コマンド文字列で判定する以上、完全にはなりません。\n- **hook 自体を AI が書き換えられる状態では、意味が薄れます。** hook ファイルの編集を権限で制限するなどの併用を検討してください(この記事では検証していません)。\n- **事後検査は、追加された行の文字列一致だけです。** 略称や表記ゆれは通りますし、語のリストは人が更新し続ける必要があります。\n- 実際の案件で使っているルールの一覧や検知パターンは、案件ごとに調整しているため、この記事には載せていません。\n\n## 最後に\n\n「やらないこと」は、書く場所によって守られ方が変わります。理由を添えて `CLAUDE.md` に書き、事故ると戻せないものは hook で止め、内容に出るものは事後に検査する。この3段に分けるだけでも、書き忘れや抜けは見つけやすくなります。気になったところから、手元の環境で試してみてください。\n\n## 参考文献\n\n- Claude Code Docs「How Claude remembers your project」(CLAUDE.md、サイズと矛盾の注意、hook との使い分け)\n- Claude Code Docs「Hooks reference」(PreToolUse の入力JSON、exit code 2、settings.json の書式)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:08:23+09:00","group":null,"id":"c0201b3eb73901d280a2","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"ClaudeCode","versions":[]},{"name":"生成AI","versions":[]},{"name":"開発プロセス","versions":[]},{"name":"チーム開発","versions":[]},{"name":"ドキュメント","versions":[]}],"title":"CLAUDE.mdに「やらないこと」を書く　顧客案件でAIに守らせるルールの決め方","updated_at":"2026-09-27T11:08:23+09:00","url":"https://qiita.com/vaon-luandv/items/c0201b3eb73901d280a2","user":{"description":"ハノイのソフトウェア開発チームVAON Viet Namの創業者。日本のお客様向けの開発に取り組んでいます。要件定義、FAQチャットボット、AI・画像認識のPoCまわりで、実際にやったこと、うまくいかなかったことを書きます。","facebook_id":"","followees_count":5,"followers_count":2,"github_login_name":null,"id":"vaon-luandv","items_count":13,"linkedin_id":"van-luan-dang","location":"ハノイ","name":"ダン・ヴァン ルアン","organization":"VAON Viet Nam","permanent_id":4504632,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocLJ3qUJ6othJOtFsxVbDfYA3W2Q1vzNo8_-QM8uwHwtLatH6uJI=s96-c","team_only":false,"twitter_screen_name":null,"website_url":"https://vaon.com.vn/ja"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:15\"\u003e\n\u003cspan id=\"はじめに\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eはじめに\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-4:226\"\u003e依存パッケージの更新を放置しないために、Dependabot の PR を自動マージするようにしました。\u003cbr\u003e\n自動マージそのものは数行で書けます。ただ、\u003cstrong\u003e「マージ → main で CI → 本番デプロイ」までつなげると、エラーも出さずに静かに壊れる箇所がいくつもありました\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"6:1-7:112\"\u003eこの記事は、GitHub Actions と Dependabot を使っている個人開発者・小規模チーム向けです。\u003cbr\u003e\nそのまま持ち込めるコードに加えて、「なぜそう書くのか」を1つずつ説明します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"9:1-9:8\"\u003e\n\u003cspan id=\"tldr\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#tldr\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eTL;DR\u003c/h2\u003e\n\u003cul data-sourcepos=\"11:1-16:0\"\u003e\n\u003cli data-sourcepos=\"11:1-11:190\"\u003e\n\u003cstrong\u003e\u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e でマージすると、main への push で CI が起動しない\u003c/strong\u003e。E2E もデプロイも走らず、エラーも出ない。マージには fine-grained PAT を使う\u003c/li\u003e\n\u003cli data-sourcepos=\"12:1-12:212\"\u003e\n\u003cstrong\u003eトリガーは \u003ccode\u003epull_request\u003c/code\u003e ではなく \u003ccode\u003eworkflow_run\u003c/code\u003e（CI 完了）にする\u003c/strong\u003e。Dependabot が起動したワークフローには読み取り専用トークンしか渡らず、Secrets も渡らない\u003c/li\u003e\n\u003cli data-sourcepos=\"13:1-13:203\"\u003e\n\u003cstrong\u003ePAT を持つワークフローでは、PR のコードを checkout も実行もしない\u003c/strong\u003e。依存のインストールスクリプトに書き込み権限付きのトークンを晒さないため\u003c/li\u003e\n\u003cli data-sourcepos=\"14:1-14:199\"\u003e\n\u003cstrong\u003eDependabot の PR では Actions Secrets が渡らず、E2E が必ず落ちる\u003c/strong\u003e。PR 段階では E2E をスキップし、main への push では必ず回してデプロイのゲートにする\u003c/li\u003e\n\u003cli data-sourcepos=\"15:1-16:0\"\u003e\n\u003cstrong\u003ePAT の期限切れ・空のシークレット・放置 PR は「静かに」壊れる\u003c/strong\u003e。疎通確認ジョブと放置 PR 通知で検知する\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"17:1-17:9\"\u003e\n\u003cspan id=\"前提\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E6%8F%90\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前提\u003c/h2\u003e\n\u003cul data-sourcepos=\"19:1-23:0\"\u003e\n\u003cli data-sourcepos=\"19:1-19:99\"\u003eGitHub Actions で CI（lint / 型チェック / テスト / ビルド / E2E）を回している\u003c/li\u003e\n\u003cli data-sourcepos=\"20:1-21:144\"\u003emain への push で CI が全部通ったときだけ、CI の \u003ccode\u003edeploy\u003c/code\u003e ジョブが本番デプロイする\n\u003cul data-sourcepos=\"21:3-21:144\"\u003e\n\u003cli data-sourcepos=\"21:3-21:144\"\u003eホスティング側の Git 連携による自動デプロイは止めておく（CI を通らずにデプロイされてしまうため）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"22:1-23:0\"\u003e例は npm ですが、言語やフレームワークは問いません\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"24:1-24:12\"\u003e\n\u003cspan id=\"全体像\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%85%A8%E4%BD%93%E5%83%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e全体像\u003c/h2\u003e\n\u003ciframe id=\"qiita-embed-content__11f3670068536ce98793dc5814ba19d4\" src=\"https://qiita.com/embed-contents/mermaid#qiita-embed-content__11f3670068536ce98793dc5814ba19d4\" style=\"width:100%;\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" data-content='{\"data\":\"flowchart TD\\n A[週1回: Dependabot が npm と GitHub Actions をチェック] --\u0026gt; B{更新の種類}\\n B --\u0026gt;|minor / patch| C[1本のまとめPR\u0026lt;br/\u0026gt;dependabot/\u0026amp;lt;eco\u0026amp;gt;/minor-and-patch-\u0026amp;lt;hash\u0026amp;gt;]\\n B --\u0026gt;|major| D[個別PR\u0026lt;br/\u0026gt;自動マージしない・人間がレビュー]\\n C --\u0026gt; E[CI 緑]\\n E --\u0026gt;|workflow_run| F[auto-merge\u0026lt;br/\u0026gt;PAT で squash マージ]\\n F --\u0026gt; G[main への push → CI（E2E 含む）]\\n G --\u0026gt;|全部緑| H[本番デプロイ]\\n G --\u0026gt;|どれか赤| I[デプロイしない\u0026lt;br/\u0026gt;本番は前回の正常版のまま]\\n J[スキャン前日: 放置PR通知] -.-\u0026gt; D\\n J -.-\u0026gt; C\\n K[手動: token-check\u0026lt;br/\u0026gt;PAT の疎通確認] -.-\u0026gt; F\",\"key\":\"a15ad350f33f667202ea07643c605d1e\"}'\u003e\n\u003c/iframe\u003e\n\n\u003cp data-sourcepos=\"41:1-42:155\"\u003e目的は「常に最新であること」ではありません。\u003cbr\u003e\n\u003cstrong\u003eまとめて上げる羽目にならないよう小さく継続的に追従し、1回あたりの移行コストとリスクを下げる\u003c/strong\u003eことです。\u003c/p\u003e\n\u003cp data-sourcepos=\"44:1-44:46\"\u003e登場するファイルは3つ + CI です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"46:1-51:103\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"46:1-46:25\"\u003e\n\u003cth data-sourcepos=\"46:2-46:15\"\u003eファイル\u003c/th\u003e\n\u003cth data-sourcepos=\"46:17-46:24\"\u003e役割\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"48:1-48:65\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:27\"\u003e\u003ccode\u003e.github/dependabot.yml\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:29-48:64\"\u003e更新のグループ化と ignore\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"49:1-49:104\"\u003e\n\u003ctd data-sourcepos=\"49:2-49:48\"\u003e\u003ccode\u003e.github/workflows/dependabot-auto-merge.yml\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:50-49:103\"\u003eCI 完了後の自動マージ + PAT の疎通確認\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:95\"\u003e\n\u003ctd data-sourcepos=\"50:2-50:51\"\u003e\u003ccode\u003e.github/workflows/dependabot-review-notify.yml\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:53-50:94\"\u003e人間の対応が必要な PR の通知\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:103\"\u003e\n\u003ctd data-sourcepos=\"51:2-51:29\"\u003e\u003ccode\u003e.github/workflows/ci.yml\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:31-51:102\"\u003e既存の CI（E2E の条件とデプロイのゲートだけ触る）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"53:1-53:64\"\u003e\n\u003cspan id=\"1-dependabot-の設定グループ化と-ignore-の基準\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-dependabot-%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E5%8C%96%E3%81%A8-ignore-%E3%81%AE%E5%9F%BA%E6%BA%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. Dependabot の設定：グループ化と ignore の基準\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"55:1-94:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.github/dependabot.yml\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003eversion\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\n\u003cspan class=\"na\"\u003eupdates\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003epackage-ecosystem\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003enpm\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edirectory\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e/\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eschedule\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003einterval\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eweekly\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eday\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emonday\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etime\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e09:00\"\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etimezone\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eAsia/Tokyo\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eopen-pull-requests-limit\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\n    \u003cspan class=\"na\"\u003egroups\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# minor / patch は1本のPRにまとめる。メジャーだけ個別PR。\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# グループ名はブランチ名 dependabot/npm_and_yarn/minor-and-patch-\u0026lt;hash\u0026gt; に現れ、\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# 自動マージ判定に使われる。名前を変えるときはワークフロー側も直すこと。\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eminor-and-patch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"na\"\u003epatterns\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e*\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eupdate-types\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003eminor\u003c/span\u003e\u003cspan class=\"pi\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003epatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eignore\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# 型定義は実行環境の Node メジャーと揃える。Node 本体を上げるときに手動で。\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003edependency-name\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e@types/node\"\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eupdate-types\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eversion-update:semver-major\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# 例: 上流が未対応で CI が落ちるもの。対応版が出たら外す（外す条件を必ず書く）。\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# - dependency-name: \"eslint\"\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e#   update-types: [\"version-update:semver-major\"]\u003c/span\u003e\n\n  \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003epackage-ecosystem\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003egithub-actions\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edirectory\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e/\"\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eschedule\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"na\"\u003einterval\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eweekly\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eday\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003emonday\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etime\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e09:00\"\u003c/span\u003e\n      \u003cspan class=\"na\"\u003etimezone\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eAsia/Tokyo\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eopen-pull-requests-limit\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\n    \u003cspan class=\"na\"\u003egroups\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"c1\"\u003e# npm 側と同じグループ名にする（ワークフローがブランチ名で判定するため）\u003c/span\u003e\n      \u003cspan class=\"na\"\u003eminor-and-patch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n        \u003cspan class=\"na\"\u003epatterns\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e*\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eupdate-types\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003eminor\u003c/span\u003e\u003cspan class=\"pi\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003epatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"96:1-96:79\"\u003e\n\u003cspan id=\"ブランチ名を自動マージしてよいことの証明にする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E5%90%8D%E3%82%92%E8%87%AA%E5%8B%95%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%97%E3%81%A6%E3%82%88%E3%81%84%E3%81%93%E3%81%A8%E3%81%AE%E8%A8%BC%E6%98%8E%E3%81%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eブランチ名を「自動マージしてよい」ことの証明にする\u003c/h3\u003e\n\u003cp data-sourcepos=\"98:1-98:121\"\u003eグループの \u003ccode\u003eupdate-types\u003c/code\u003e を minor / patch に限定すると、グループ名がブランチ名に入ります。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"100:1-102:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003edependabot/npm_and_yarn/minor-and-patch-\u0026lt;hash\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"104:1-104:187\"\u003eつまり、\u003cstrong\u003eブランチ名がそのまま「メジャー更新を含まない」ことの証明になる\u003c/strong\u003eので、ワークフロー側はブランチ名だけで判定できます。\u003c/p\u003e\n\u003cp data-sourcepos=\"106:1-106:25\"\u003e注意点は2つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"108:1-110:0\"\u003e\n\u003cli data-sourcepos=\"108:1-108:75\"\u003eエコシステムが複数あるときは\u003cstrong\u003eグループ名を揃える\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"109:1-110:0\"\u003eグループ名を変えるときはワークフローの判定条件も直す。忘れないよう、\u003cstrong\u003e両方のファイルにコメントで相互参照を書いておく\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"111:1-111:162\"\u003eなお、ブランチ名は手で真似できるので、ワークフロー側では「作成者が Dependabot であること」も確認します（後述）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"113:1-113:45\"\u003e\n\u003cspan id=\"メジャー更新を-ignore-する基準\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%A1%E3%82%B8%E3%83%A3%E3%83%BC%E6%9B%B4%E6%96%B0%E3%82%92-ignore-%E3%81%99%E3%82%8B%E5%9F%BA%E6%BA%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eメジャー更新を ignore する基準\u003c/h3\u003e\n\u003cp data-sourcepos=\"115:1-115:56\"\u003eignore するのは次の2種類に絞っています。\u003c/p\u003e\n\u003cp data-sourcepos=\"117:1-117:59\"\u003e\u003cstrong\u003e① 「新しいほど良い」とは限らない依存\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"119:1-120:166\"\u003e代表例が \u003ccode\u003e@types/node\u003c/code\u003e のような実行環境の型定義です。本番の Node より新しいメジャーを入れると、実行環境に存在しない API が型の上では OK になります。\u003cbr\u003e\n結果、\u003cstrong\u003e型チェックは緑なのに本番で落ちる\u003c/strong\u003eという最悪のパターンになります。実行環境を上げるときに手動で揃えます。\u003c/p\u003e\n\u003cp data-sourcepos=\"122:1-122:72\"\u003e\u003cstrong\u003e② 上流が未対応で、上げると CI が必ず落ちるもの\u003c/strong\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"124:1-124:132\"\u003eリンターや TypeScript のメジャー更新で、プラグインや周辺ツールが追いついていないケースです。\u003c/p\u003e\n\u003cp data-sourcepos=\"126:1-126:206\"\u003eここで学んだのは、\u003cstrong\u003epeer 宣言や \u003ccode\u003enpm install --dry-run\u003c/code\u003e が通っても互換性の証明にはならない\u003c/strong\u003eということです。実際に上げて CI で確かめるしかありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"128:1-128:141\"\u003eそして ignore には**「いつ外すか」の条件を必ずコメントで書きます**。書かないと永久に放置されます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"130:1-130:42\"\u003e\n\u003cspan id=\"2-自動マージのワークフロー\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E8%87%AA%E5%8B%95%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 自動マージのワークフロー\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"132:1-198:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.github/workflows/dependabot-auto-merge.yml\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eDependabot auto-merge\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# pull_request ではなく workflow_run: Dependabot 起動の実行は読み取り専用トークンのため。\u003c/span\u003e\n\u003cspan class=\"c1\"\u003e# PR のコードは checkout しない: PAT を依存のインストールスクリプトに晒さないため。\u003c/span\u003e\n\u003cspan class=\"na\"\u003eon\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eworkflow_run\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eworkflows\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eCI\"\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003etypes\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003ecompleted\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebranches\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edependabot/*/minor-and-patch-*\"\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eworkflow_dispatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# token-check 用\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003epermissions\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003econtents\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eread\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# マージは PAT で行うので既定トークンは読み取りのみ\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003ejobs\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eauto-merge\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eubuntu-latest\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eif\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e\u0026gt;-\u003c/span\u003e\n      \u003cspan class=\"s\"\u003e${{ github.event_name == 'workflow_run'\u003c/span\u003e\n      \u003cspan class=\"s\"\u003e\u0026amp;\u0026amp; github.event.workflow_run.event == 'pull_request'\u003c/span\u003e\n      \u003cspan class=\"s\"\u003e\u0026amp;\u0026amp; github.event.workflow_run.conclusion == 'success'\u003c/span\u003e\n      \u003cspan class=\"s\"\u003e\u0026amp;\u0026amp; startsWith(github.event.workflow_run.head_branch, 'dependabot/')\u003c/span\u003e\n      \u003cspan class=\"s\"\u003e\u0026amp;\u0026amp; contains(github.event.workflow_run.head_branch, '/minor-and-patch-') }}\u003c/span\u003e\n    \u003cspan class=\"na\"\u003esteps\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eCI が通った Dependabot PR をマージ\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"c1\"\u003e# GITHUB_TOKEN でマージすると main への push で CI が起動しない → PAT を使う\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eGH_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.AUTOMERGE_TOKEN }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eREPO\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.repository }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eBRANCH\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.event.workflow_run.head_branch }}\u003c/span\u003e\n        \u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e|\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eset -euo pipefail\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eif [ -z \"${GH_TOKEN:-}\" ]; then\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eecho \"::error::AUTOMERGE_TOKEN が未設定です。\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eexit 1\u003c/span\u003e\n          \u003cspan class=\"s\"\u003efi\u003c/span\u003e\n          \u003cspan class=\"s\"\u003e# 作成者が Dependabot であることも確認（ブランチ名の偽装対策）\u003c/span\u003e\n          \u003cspan class=\"s\"\u003enum=\"$(gh pr list --repo \"$REPO\" --head \"$BRANCH\" --state open \\\u003c/span\u003e\n                   \u003cspan class=\"s\"\u003e--json number,author \\\u003c/span\u003e\n                   \u003cspan class=\"s\"\u003e--jq '[.[] | select(.author.login == \"app/dependabot\" or .author.login == \"dependabot[bot]\") | .number] | first // empty')\"\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eif [ -z \"$num\" ]; then\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eecho \"対象の open な Dependabot PR がありません。\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eexit 0\u003c/span\u003e\n          \u003cspan class=\"s\"\u003efi\u003c/span\u003e\n          \u003cspan class=\"s\"\u003egh pr merge \"$num\" --repo \"$REPO\" --squash --delete-branch\u003c/span\u003e\n\n  \u003cspan class=\"na\"\u003etoken-check\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eif\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.event_name == 'workflow_dispatch' }}\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eubuntu-latest\u003c/span\u003e\n    \u003cspan class=\"na\"\u003esteps\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eAUTOMERGE_TOKEN の疎通確認（マージはしない）\u003c/span\u003e\n        \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eGH_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ secrets.AUTOMERGE_TOKEN }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eREPO\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.repository }}\u003c/span\u003e\n        \u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e|\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eset -euo pipefail\u003c/span\u003e\n          \u003cspan class=\"s\"\u003e[ -n \"${GH_TOKEN:-}\" ] || { echo \"::error::AUTOMERGE_TOKEN が空です（--body \\\"\\$(...)\\\" の失敗など）\"; exit 1; }\u003c/span\u003e\n          \u003cspan class=\"s\"\u003egh api \"repos/$REPO\" --jq '.full_name' \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \\\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e|| { echo \"::error::リポジトリを読めません（期限切れ・Repository access 外）\"; exit 1; }\u003c/span\u003e\n          \u003cspan class=\"s\"\u003egh pr list --repo \"$REPO\" --state open --limit 1 \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \\\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e|| { echo \"::error::Pull requests を読めません\"; exit 1; }\u003c/span\u003e\n          \u003cspan class=\"s\"\u003epush_perm=\"$(gh api \"repos/$REPO\" --jq '.permissions.push // \"unknown\"')\"\u003c/span\u003e\n          \u003cspan class=\"s\"\u003e[ \"$push_perm\" = \"true\" ] \u0026amp;\u0026amp; echo \"OK: 書き込み権限あり\" \\\u003c/span\u003e\n            \u003cspan class=\"s\"\u003e|| echo \"::warning::書き込み権限を確認できません (permissions.push=$push_perm)\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"200:1-200:76\"\u003eこのファイルには、ハマりどころが3つ詰まっています。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"202:1-202:101\"\u003e\n\u003cspan id=\"罠github_token-でマージするとその後の-ci-もデプロイも起動しない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%BD%A0github_token-%E3%81%A7%E3%83%9E%E3%83%BC%E3%82%B8%E3%81%99%E3%82%8B%E3%81%A8%E3%81%9D%E3%81%AE%E5%BE%8C%E3%81%AE-ci-%E3%82%82%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%82%82%E8%B5%B7%E5%8B%95%E3%81%97%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e罠①：\u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e でマージすると、その後の CI もデプロイも起動しない\u003c/h3\u003e\n\u003cp data-sourcepos=\"204:1-204:30\"\u003eこれが一番の罠です。\u003c/p\u003e\n\u003cp data-sourcepos=\"206:1-206:279\"\u003eGitHub には、\u003cstrong\u003e\u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e で起こしたイベントからは新しいワークフローを起動しない\u003c/strong\u003eという仕様があります（\u003ccode\u003eworkflow_dispatch\u003c/code\u003e と \u003ccode\u003erepository_dispatch\u003c/code\u003e を除く）。ワークフローが無限に連鎖するのを防ぐためです。\u003c/p\u003e\n\u003cp data-sourcepos=\"208:1-208:73\"\u003eそのため \u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e でマージすると、こうなります。\u003c/p\u003e\n\u003col data-sourcepos=\"210:1-213:0\"\u003e\n\u003cli data-sourcepos=\"210:1-210:33\"\u003e自動マージは成功する\u003c/li\u003e\n\u003cli data-sourcepos=\"211:1-211:26\"\u003emain に push される\u003c/li\u003e\n\u003cli data-sourcepos=\"212:1-213:0\"\u003e\n\u003cstrong\u003eでも CI が起動しない\u003c/strong\u003e → E2E もデプロイも走らない\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"214:1-214:184\"\u003e\u003cstrong\u003e「マージされたのに、検証もデプロイもされていない」状態になり、エラーも出ません。\u003c/strong\u003e Actions の画面を見に行くまで気づけません。\u003c/p\u003e\n\u003cp data-sourcepos=\"216:1-216:227\"\u003e対策は、fine-grained PAT（Personal Access Token）を作って Secrets に登録し、マージに使うことです。PAT でのマージは通常の push として扱われるので、CI がちゃんと起動します。\u003c/p\u003e\n\u003cp data-sourcepos=\"218:1-218:40\"\u003ePAT の権限は最小限にします。\u003c/p\u003e\n\u003ctable data-sourcepos=\"220:1-224:34\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"220:1-220:19\"\u003e\n\u003cth data-sourcepos=\"220:2-220:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"220:11-220:18\"\u003e設定\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"222:1-222:51\"\u003e\n\u003ctd data-sourcepos=\"222:2-222:20\"\u003eRepository access\u003c/td\u003e\n\u003ctd data-sourcepos=\"222:22-222:50\"\u003e対象リポジトリのみ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"223:1-223:29\"\u003e\n\u003ctd data-sourcepos=\"223:2-223:11\"\u003eContents\u003c/td\u003e\n\u003ctd data-sourcepos=\"223:13-223:28\"\u003eRead and write\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"224:1-224:34\"\u003e\n\u003ctd data-sourcepos=\"224:2-224:16\"\u003ePull requests\u003c/td\u003e\n\u003ctd data-sourcepos=\"224:18-224:33\"\u003eRead and write\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 data-sourcepos=\"226:1-226:88\"\u003e\n\u003cspan id=\"罠トリガーを-pull_request-にするとマージ-api-を呼べない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%BD%A0%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%82%92-pull_request-%E3%81%AB%E3%81%99%E3%82%8B%E3%81%A8%E3%83%9E%E3%83%BC%E3%82%B8-api-%E3%82%92%E5%91%BC%E3%81%B9%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e罠②：トリガーを \u003ccode\u003epull_request\u003c/code\u003e にすると、マージ API を呼べない\u003c/h3\u003e\n\u003cp data-sourcepos=\"228:1-228:291\"\u003e素直に書くと \u003ccode\u003eon: pull_request\u003c/code\u003e で Dependabot の PR を拾いたくなります。しかし、\u003cstrong\u003eDependabot が起動したワークフローには読み取り専用の \u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e しか渡らず、Actions の Secrets も渡りません\u003c/strong\u003e。これではマージできません。\u003c/p\u003e\n\u003cp data-sourcepos=\"230:1-230:260\"\u003e一方、CI の完了イベント（\u003ccode\u003eworkflow_run\u003c/code\u003e）から起動したワークフローは、元の実行が Dependabot 由来でも通常の権限と Secrets で動きます。なので「CI が完了した」タイミングでマージ判定をします。\u003c/p\u003e\n\u003cp data-sourcepos=\"232:1-232:84\"\u003e\u003ccode\u003eworkflow_run.branches\u003c/code\u003e の絞り込みも入れておくのがおすすめです。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"234:1-237:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e    \u003cspan class=\"na\"\u003ebranches\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003edependabot/*/minor-and-patch-*\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cul data-sourcepos=\"239:1-242:0\"\u003e\n\u003cli data-sourcepos=\"239:1-239:120\"\u003e\n\u003ccode\u003e*\u003c/code\u003e は \u003ccode\u003e/\u003c/code\u003e に一致しないので、\u003ccode\u003edependabot/npm_and_yarn/minor-and-patch-xxx\u003c/code\u003e の形だけにマッチします\u003c/li\u003e\n\u003cli data-sourcepos=\"240:1-240:187\"\u003e絞らないと、main への push を含む\u003cstrong\u003eすべての CI 完了で起動\u003c/strong\u003eし、\u003ccode\u003eif\u003c/code\u003e で弾かれた \u003ccode\u003eskipped\u003c/code\u003e の実行が Actions 一覧に毎回並んでうるさくなります\u003c/li\u003e\n\u003cli data-sourcepos=\"241:1-242:0\"\u003e\n\u003ccode\u003eif\u003c/code\u003e 条件は多層防御としてそのまま残しておきます\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 data-sourcepos=\"243:1-243:93\"\u003e\n\u003cspan id=\"罠pat-を持つワークフローで-pr-のコードを動かしてはいけない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%BD%A0pat-%E3%82%92%E6%8C%81%E3%81%A4%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%81%A7-pr-%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%8B%95%E3%81%8B%E3%81%97%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e罠③：PAT を持つワークフローで PR のコードを動かしてはいけない\u003c/h3\u003e\n\u003cp data-sourcepos=\"245:1-245:89\"\u003eこのワークフローは \u003ccode\u003eactions/checkout\u003c/code\u003e をしていません。意図的です。\u003c/p\u003e\n\u003cp data-sourcepos=\"247:1-247:242\"\u003e更新後の依存をインストールすると、\u003ccode\u003epostinstall\u003c/code\u003e などのスクリプトが動きます。そこに\u003cstrong\u003e書き込み権限付きの PAT が見える状態だと、サプライチェーン攻撃の格好の的\u003c/strong\u003eになります。\u003c/p\u003e\n\u003cp data-sourcepos=\"249:1-249:45\"\u003eそこで役割をきっちり分けます。\u003c/p\u003e\n\u003ctable data-sourcepos=\"251:1-254:89\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"251:1-251:35\"\u003e\n\u003cth data-sourcepos=\"251:2-251:9\"\u003e役割\u003c/th\u003e\n\u003cth data-sourcepos=\"251:11-251:19\"\u003eSecrets\u003c/th\u003e\n\u003cth data-sourcepos=\"251:21-251:34\"\u003eやること\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"253:1-253:78\"\u003e\n\u003ctd data-sourcepos=\"253:2-253:5\"\u003eCI\u003c/td\u003e\n\u003ctd data-sourcepos=\"253:7-253:20\"\u003e持たない\u003c/td\u003e\n\u003ctd data-sourcepos=\"253:22-253:77\"\u003e依存を実際にインストールして検証する\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"254:1-254:89\"\u003e\n\u003ctd data-sourcepos=\"254:2-254:13\"\u003eauto-merge\u003c/td\u003e\n\u003ctd data-sourcepos=\"254:15-254:29\"\u003ePAT を持つ\u003c/td\u003e\n\u003ctd data-sourcepos=\"254:31-254:88\"\u003eGitHub API を叩くだけ（コードは触らない）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"256:1-256:133\"\u003e「CI の最後にマージのジョブを足せば1ファイルで済むのでは？」とも考えましたが、やめました。\u003c/p\u003e\n\u003cul data-sourcepos=\"258:1-260:0\"\u003e\n\u003cli data-sourcepos=\"258:1-258:166\"\u003eDependabot 起動の CI に PAT を渡すには、PAT を \u003cstrong\u003eDependabot secrets にも二重登録\u003c/strong\u003eする必要がある → 期限更新の管理箇所が増える\u003c/li\u003e\n\u003cli data-sourcepos=\"259:1-260:0\"\u003ePAT と依存コードの実行が同じワークフローに来て、\u003cstrong\u003e隔離の境界が曖昧になる\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"261:1-261:78\"\u003e得られるのは見た目の整理だけなので、割に合いません。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"263:1-265:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"264:1-264:302\"\u003eGitHub 標準の auto-merge（\u003ccode\u003egh pr merge --auto\u003c/code\u003e）は、ブランチ保護で必須チェックを設定していることが前提です。無料プランのプライベートリポジトリなど、ブランチ保護を使えない環境では、このように自作する意味があります。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch2 data-sourcepos=\"267:1-267:51\"\u003e\n\u003cspan id=\"3-dependabot-の-pr-と-secretse2e-の扱い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-dependabot-%E3%81%AE-pr-%E3%81%A8-secretse2e-%E3%81%AE%E6%89%B1%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. Dependabot の PR と Secrets・E2E の扱い\u003c/h2\u003e\n\u003ch3 data-sourcepos=\"269:1-269:60\"\u003e\n\u003cspan id=\"罠dependabot-の-pr-では-e2e-が必ず落ちる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%BD%A0dependabot-%E3%81%AE-pr-%E3%81%A7%E3%81%AF-e2e-%E3%81%8C%E5%BF%85%E3%81%9A%E8%90%BD%E3%81%A1%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e罠④：Dependabot の PR では E2E が必ず落ちる\u003c/h3\u003e\n\u003cp data-sourcepos=\"271:1-272:131\"\u003e罠②と同じ理由で、Dependabot が起動した実行には Actions の Secrets が渡りません（渡るのは別枠の「Dependabot secrets」だけです）。\u003cbr\u003e\nE2E が外部サービスの接続情報を Secrets から読んでいる場合、値が空になって\u003cstrong\u003e必ず失敗します\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"274:1-275:98\"\u003eここでは、\u003cstrong\u003eDependabot の PR では E2E をスキップする\u003c/strong\u003eと判断しました。\u003cbr\u003e\n毎週必ず赤い CI が出ると、\u003cstrong\u003eCI そのものが信用されなくなる\u003c/strong\u003eからです。\u003c/p\u003e\n\u003cp data-sourcepos=\"277:1-277:60\"\u003eただし、デプロイ前のゲートは緩めません。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"279:1-300:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.github/workflows/ci.yml（関係部分のみ）\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eCI\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003eon\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003epush\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003ebranches\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003emain\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n  \u003cspan class=\"na\"\u003epull_request\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eworkflow_dispatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003ejobs\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003elint-build\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# lint → 型チェック → テスト → ビルド（Secrets 不要）\u003c/span\u003e\n    \u003cspan class=\"s\"\u003e...\u003c/span\u003e\n  \u003cspan class=\"na\"\u003ee2e\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# Dependabot 発の PR とフォークの PR は除外（Secrets が渡らず必ず落ちるため）。\u003c/span\u003e\n    \u003cspan class=\"c1\"\u003e# push イベントでは実行者に関わらず常に実行＝デプロイ前のゲートは維持。\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eif\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository \u0026amp;\u0026amp; github.actor != 'dependabot[bot]') }}\u003c/span\u003e\n    \u003cspan class=\"s\"\u003e...\u003c/span\u003e\n  \u003cspan class=\"na\"\u003edeploy\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eneeds\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e[\u003c/span\u003e\u003cspan class=\"nv\"\u003elint-build\u003c/span\u003e\u003cspan class=\"pi\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003ee2e\u003c/span\u003e\u003cspan class=\"pi\"\u003e]\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eif\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.event_name == 'push' \u0026amp;\u0026amp; github.ref == 'refs/heads/main' }}\u003c/span\u003e\n    \u003cspan class=\"s\"\u003e...\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"302:1-302:39\"\u003eポイントは次のとおりです。\u003c/p\u003e\n\u003cul data-sourcepos=\"304:1-307:0\"\u003e\n\u003cli data-sourcepos=\"304:1-304:128\"\u003e\n\u003cstrong\u003epush イベントでは実行者に関係なく E2E を必ず回す\u003c/strong\u003e（PAT でのマージ後の push はここに来る）\u003c/li\u003e\n\u003cli data-sourcepos=\"305:1-305:58\"\u003e\n\u003ccode\u003edeploy\u003c/code\u003e は \u003ccode\u003eneeds\u003c/code\u003e で E2E の通過を条件にする\u003c/li\u003e\n\u003cli data-sourcepos=\"306:1-307:0\"\u003eE2E が落ちればデプロイはスキップされ、\u003cstrong\u003e本番は前回の正常版のまま残る\u003c/strong\u003e。main を revert するだけで復旧できる\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"308:1-308:113\"\u003eつまり「PR 段階の E2E」は省略しても、「本番に出る前の E2E」は省略していません。\u003c/p\u003e\n\u003cdiv data-sourcepos=\"310:1-312:3\" class=\"note info\"\u003e\n\u003cspan class=\"fa fa-fw fa-check-circle\"\u003e\u003c/span\u003e\u003cdiv\u003e\n\u003cp data-sourcepos=\"311:1-311:198\"\u003e代替案として、Dependabot secrets に同じ値を登録して除外条件を外す方法もあります。二重管理のコストと引き換えに、PR の段階で E2E まで回せます。\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 data-sourcepos=\"314:1-314:84\"\u003e\n\u003cspan id=\"罠0x-系パッケージの-minor-更新は破壊的変更を含みうる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%BD%A00x-%E7%B3%BB%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE-minor-%E6%9B%B4%E6%96%B0%E3%81%AF%E7%A0%B4%E5%A3%8A%E7%9A%84%E5%A4%89%E6%9B%B4%E3%82%92%E5%90%AB%E3%81%BF%E3%81%86%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e罠⑤：0.x 系パッケージの minor 更新は破壊的変更を含みうる\u003c/h3\u003e\n\u003cp data-sourcepos=\"316:1-316:234\"\u003esemver 上、\u003ccode\u003e0.12 → 0.13\u003c/code\u003e は minor 扱いです。しかし 0.x は安定性の保証がないため、実質的な破壊的変更が入ることがあります。そして\u003cstrong\u003e自動マージの対象に入ってしまいます\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"318:1-318:209\"\u003e防波堤になるのは、CI（型チェック・テスト・ビルド）とマージ後の E2E です。上の構成なら落ちてもデプロイされないので、\u003cstrong\u003e失敗は安全側に倒れます\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"320:1-320:133\"\u003e心配なパッケージがあれば、そのパッケージだけグループから外して個別 PR にする手もあります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"322:1-322:42\"\u003e\n\u003cspan id=\"4-静かに壊れる箇所への対策\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E9%9D%99%E3%81%8B%E3%81%AB%E5%A3%8A%E3%82%8C%E3%82%8B%E7%AE%87%E6%89%80%E3%81%B8%E3%81%AE%E5%AF%BE%E7%AD%96\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 静かに壊れる箇所への対策\u003c/h2\u003e\n\u003cp data-sourcepos=\"324:1-324:200\"\u003e自動化は「動いているうちは見なくなる」ので、\u003cstrong\u003e止まったことに気づけない\u003c/strong\u003eのが一番怖いところです。ここでは3つの「静かな故障」に備えます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"326:1-326:57\"\u003e\n\u003cspan id=\"pat-の期限切れ--token-check-で疎通確認\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#pat-%E3%81%AE%E6%9C%9F%E9%99%90%E5%88%87%E3%82%8C--token-check-%E3%81%A7%E7%96%8E%E9%80%9A%E7%A2%BA%E8%AA%8D\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003ePAT の期限切れ → \u003ccode\u003etoken-check\u003c/code\u003e で疎通確認\u003c/h3\u003e\n\u003cp data-sourcepos=\"328:1-328:144\"\u003efine-grained PAT には有効期限があります。切れても、\u003cstrong\u003ePR が open のまま残るだけで表面上は何も起きません\u003c/strong\u003e。\u003c/p\u003e\n\u003cp data-sourcepos=\"330:1-330:244\"\u003eそこで、auto-merge ワークフローに手動実行（\u003ccode\u003eworkflow_dispatch\u003c/code\u003e）用の \u003ccode\u003etoken-check\u003c/code\u003e ジョブを入れています（前掲のコード参照）。確認する順番は次のとおりで、\u003cstrong\u003eマージは一切しません\u003c/strong\u003e。\u003c/p\u003e\n\u003col data-sourcepos=\"332:1-336:0\"\u003e\n\u003cli data-sourcepos=\"332:1-332:33\"\u003eトークンが空でないか\u003c/li\u003e\n\u003cli data-sourcepos=\"333:1-333:99\"\u003eリポジトリを読めるか（期限切れ・Repository access 外ならここで落ちる）\u003c/li\u003e\n\u003cli data-sourcepos=\"334:1-334:21\"\u003ePR を読めるか\u003c/li\u003e\n\u003cli data-sourcepos=\"335:1-336:0\"\u003e書き込み権限があるか\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"337:1-337:120\"\u003ePAT を更新したら、Actions 画面からこのジョブを手動実行して緑になることを確認します。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"339:1-339:90\"\u003e\n\u003cspan id=\"シークレットが空文字で登録される--コマンド置換を使わない\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B7%E3%83%BC%E3%82%AF%E3%83%AC%E3%83%83%E3%83%88%E3%81%8C%E7%A9%BA%E6%96%87%E5%AD%97%E3%81%A7%E7%99%BB%E9%8C%B2%E3%81%95%E3%82%8C%E3%82%8B--%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E7%BD%AE%E6%8F%9B%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eシークレットが空文字で登録される → コマンド置換を使わない\u003c/h3\u003e\n\u003cp data-sourcepos=\"341:1-341:49\"\u003e\u003ccode\u003egh secret set\u003c/code\u003e でこう書くと危険です。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"bash\" data-sourcepos=\"343:1-345:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003egh secret \u003cspan class=\"nb\"\u003eset \u003c/span\u003eAUTOMERGE_TOKEN \u003cspan class=\"nt\"\u003e--body\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"si\"\u003e$(\u003c/span\u003esome-command\u003cspan class=\"si\"\u003e)\u003c/span\u003e\u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"347:1-347:151\"\u003eコマンド置換が失敗しても、\u003cstrong\u003e空文字のまま登録に成功してしまいます\u003c/strong\u003e。エラーにならないので気づけません。\u003c/p\u003e\n\u003cp data-sourcepos=\"349:1-349:36\"\u003e対策は次のいずれかです。\u003c/p\u003e\n\u003cul data-sourcepos=\"351:1-353:0\"\u003e\n\u003cli data-sourcepos=\"351:1-351:83\"\u003eブラウザの Settings → Secrets and variables → Actions から登録する\u003c/li\u003e\n\u003cli data-sourcepos=\"352:1-353:0\"\u003eクリップボード経由にする（macOS なら \u003ccode\u003e--body \"$(pbpaste)\"\u003c/code\u003e）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"354:1-354:166\"\u003eいずれにしても、\u003cstrong\u003eトークン文字列をシェルのコマンドラインに直接置かない\u003c/strong\u003eようにします（シェルの履歴に残るため）。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"356:1-356:77\"\u003e\n\u003cspan id=\"放置-pr--スキャン前日に-github-自身の通知で知らせる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%94%BE%E7%BD%AE-pr--%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E5%89%8D%E6%97%A5%E3%81%AB-github-%E8%87%AA%E8%BA%AB%E3%81%AE%E9%80%9A%E7%9F%A5%E3%81%A7%E7%9F%A5%E3%82%89%E3%81%9B%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e放置 PR → スキャン前日に GitHub 自身の通知で知らせる\u003c/h3\u003e\n\u003cp data-sourcepos=\"358:1-358:82\"\u003e次の2種類の PR を、Dependabot のスキャン前日に洗い出します。\u003c/p\u003e\n\u003cul data-sourcepos=\"360:1-362:0\"\u003e\n\u003cli data-sourcepos=\"360:1-360:72\"\u003e\n\u003cstrong\u003eメジャー更新の PR\u003c/strong\u003e（そもそも自動マージしない）\u003c/li\u003e\n\u003cli data-sourcepos=\"361:1-362:0\"\u003e\n\u003cstrong\u003e72時間以上 open のままの minor-and-patch PR\u003c/strong\u003e（自動マージが機能していない疑い）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"363:1-409:3\"\u003e\n\u003cdiv class=\"code-lang\"\u003e\u003cspan class=\"bold\"\u003e.github/workflows/dependabot-review-notify.yml\u003c/span\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003ename\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eDependabot 要レビューPR通知\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003eon\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eschedule\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003ecron\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003e0\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e1\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e*\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e*\u003c/span\u003e\u003cspan class=\"nv\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e0\"\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# 毎週日曜 10:00 JST（Dependabot のスキャン前日）\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eworkflow_dispatch\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003epermissions\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003econtents\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eread\u003c/span\u003e\n  \u003cspan class=\"na\"\u003epull-requests\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ewrite\u003c/span\u003e\n\n\u003cspan class=\"na\"\u003ejobs\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003enotify\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eruns-on\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eubuntu-latest\u003c/span\u003e\n    \u003cspan class=\"na\"\u003esteps\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n      \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003eenv\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eGH_TOKEN\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.token }}\u003c/span\u003e   \u003cspan class=\"c1\"\u003e# マージしないので GITHUB_TOKEN で足りる\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eREPO\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e${{ github.repository }}\u003c/span\u003e\n          \u003cspan class=\"na\"\u003eOWNER\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003eyour-github-id\u003c/span\u003e\n        \u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"pi\"\u003e|\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eset -euo pipefail\u003c/span\u003e\n          \u003cspan class=\"s\"\u003eprs=\"$(gh pr list --repo \"$REPO\" --state open \\\u003c/span\u003e\n                   \u003cspan class=\"s\"\u003e--json number,title,headRefName,createdAt,author \\\u003c/span\u003e\n                   \u003cspan class=\"s\"\u003e--jq '[.[] | select(.author.login == \"app/dependabot\" or .author.login == \"dependabot[bot]\")]')\"\u003c/span\u003e\n          \u003cspan class=\"s\"\u003ecount=\"$(echo \"$prs\" | jq 'length')\"\u003c/span\u003e\n          \u003cspan class=\"s\"\u003e[ \"$count\" -eq 0 ] \u0026amp;\u0026amp; exit 0\u003c/span\u003e\n          \u003cspan class=\"s\"\u003enow_epoch=\"$(date -u +%s)\"\u003c/span\u003e\n          \u003cspan class=\"s\"\u003efor i in $(seq 0 $((count - 1))); do\u003c/span\u003e\n            \u003cspan class=\"s\"\u003enumber=\"$(echo \"$prs\" | jq -r \".[$i].number\")\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003ebranch=\"$(echo \"$prs\" | jq -r \".[$i].headRefName\")\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003ecreated_epoch=\"$(date -u -d \"$(echo \"$prs\" | jq -r \".[$i].createdAt\")\" +%s)\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eage_hours=$(( (now_epoch - created_epoch) / 3600 ))\u003c/span\u003e\n            \u003cspan class=\"s\"\u003ereason=\"\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eif [[ \"$branch\" != dependabot/*/minor-and-patch-* ]]; then\u003c/span\u003e\n              \u003cspan class=\"s\"\u003ereason=\"自動マージ対象外（メジャー更新など）\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eelif [ \"$age_hours\" -ge 72 ]; then\u003c/span\u003e\n              \u003cspan class=\"s\"\u003ereason=\"自動マージ対象のはずが ${age_hours} 時間 open のまま\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003efi\u003c/span\u003e\n            \u003cspan class=\"s\"\u003eif [ -n \"$reason\" ]; then\u003c/span\u003e\n              \u003cspan class=\"s\"\u003egh pr edit \"$number\" --repo \"$REPO\" --add-assignee \"$OWNER\" || true\u003c/span\u003e\n              \u003cspan class=\"s\"\u003egh pr edit \"$number\" --repo \"$REPO\" --add-reviewer \"$OWNER\" || true\u003c/span\u003e\n              \u003cspan class=\"s\"\u003egh pr comment \"$number\" --repo \"$REPO\" \\\u003c/span\u003e\n                \u003cspan class=\"s\"\u003e--body \"🔔 @$OWNER このPRは自動マージされません（理由: ${reason}）。\"\u003c/span\u003e\n            \u003cspan class=\"s\"\u003efi\u003c/span\u003e\n          \u003cspan class=\"s\"\u003edone\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp data-sourcepos=\"411:1-411:40\"\u003e設計上のこだわりは3つです。\u003c/p\u003e\n\u003cul data-sourcepos=\"413:1-416:0\"\u003e\n\u003cli data-sourcepos=\"413:1-413:145\"\u003e\n\u003cstrong\u003e外部サービス（Slack など）は使わない\u003c/strong\u003e。assign・レビュー依頼・メンションで、GitHub 自身の通知に載せる\u003c/li\u003e\n\u003cli data-sourcepos=\"414:1-414:123\"\u003e\n\u003cstrong\u003e該当が無ければ何もしない\u003c/strong\u003e。空振りの通知を送るとノイズになり、そのうち見なくなる\u003c/li\u003e\n\u003cli data-sourcepos=\"415:1-416:0\"\u003eこちらは\u003cstrong\u003eマージも push もしないので \u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e で足りる\u003c/strong\u003e。\u003ccode\u003eschedule\u003c/code\u003e 起動は Dependabot 由来ではないので、書き込み権限も普通に使える\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 data-sourcepos=\"417:1-417:18\"\u003e\n\u003cspan id=\"5-導入手順\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-%E5%B0%8E%E5%85%A5%E6%89%8B%E9%A0%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. 導入手順\u003c/h2\u003e\n\u003col data-sourcepos=\"419:1-425:0\"\u003e\n\u003cli data-sourcepos=\"419:1-419:37\"\u003e\n\u003ccode\u003e.github/dependabot.yml\u003c/code\u003e を置く\u003c/li\u003e\n\u003cli data-sourcepos=\"420:1-420:101\"\u003efine-grained PAT を発行する（対象リポジトリのみ・Contents RW / Pull requests RW）\u003c/li\u003e\n\u003cli data-sourcepos=\"421:1-421:113\"\u003eブラウザの Settings → Secrets and variables → \u003cstrong\u003eActions\u003c/strong\u003e に \u003ccode\u003eAUTOMERGE_TOKEN\u003c/code\u003e として登録する\u003c/li\u003e\n\u003cli data-sourcepos=\"422:1-422:84\"\u003e2つのワークフローを置き、CI の E2E / deploy の条件を調整する\u003c/li\u003e\n\u003cli data-sourcepos=\"423:1-423:121\"\u003eActions 画面から「Dependabot auto-merge」を手動実行し、\u003ccode\u003etoken-check\u003c/code\u003e が緑になることを確認する\u003c/li\u003e\n\u003cli data-sourcepos=\"424:1-425:0\"\u003e急ぐときは Insights → Dependency graph → Dependabot の「Check for updates」で即時スキャンする（設定の不備もここに表示される）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos=\"426:1-426:43\"\u003eプレースホルダは次の3つです。\u003c/p\u003e\n\u003ctable data-sourcepos=\"428:1-432:42\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"428:1-428:34\"\u003e\n\u003cth data-sourcepos=\"428:2-428:24\"\u003eプレースホルダ\u003c/th\u003e\n\u003cth data-sourcepos=\"428:26-428:33\"\u003e中身\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"430:1-430:45\"\u003e\n\u003ctd data-sourcepos=\"430:2-430:19\"\u003e\u003ccode\u003eyour-github-id\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"430:21-430:44\"\u003e通知先の GitHub ID\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"431:1-431:54\"\u003e\n\u003ctd data-sourcepos=\"431:2-431:20\"\u003e\u003ccode\u003eAUTOMERGE_TOKEN\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"431:22-431:53\"\u003ePAT を登録する Secret 名\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"432:1-432:42\"\u003e\n\u003ctd data-sourcepos=\"432:2-432:7\"\u003e\u003ccode\u003eCI\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-sourcepos=\"432:9-432:41\"\u003eCI ワークフローの \u003ccode\u003ename\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"434:1-434:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"436:1-442:0\"\u003e\n\u003cli data-sourcepos=\"436:1-436:121\"\u003e自動マージ自体は簡単。\u003cstrong\u003e難しいのは「マージ後に CI とデプロイがちゃんと動くこと」\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"437:1-437:114\"\u003e\n\u003ccode\u003eGITHUB_TOKEN\u003c/code\u003e でマージすると後続のワークフローが起動しない → \u003cstrong\u003ePAT でマージする\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"438:1-438:114\"\u003eDependabot 起動の実行は読み取り専用・Secrets なし → \u003cstrong\u003e\u003ccode\u003eworkflow_run\u003c/code\u003e でマージ判定する\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"439:1-439:138\"\u003ePAT を持つワークフローでは\u003cstrong\u003eコードを checkout も実行もしない\u003c/strong\u003e。検証は Secrets を持たない CI に任せる\u003c/li\u003e\n\u003cli data-sourcepos=\"440:1-440:141\"\u003eDependabot の PR では E2E をスキップしても、\u003cstrong\u003emain への push では必ず E2E を回してデプロイのゲートにする\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli data-sourcepos=\"441:1-442:0\"\u003ePAT の期限切れ・空のシークレット・放置 PR は静かに壊れる → \u003cstrong\u003e疎通確認ジョブと放置 PR 通知で検知する\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"443:1-443:196\"\u003e「自動化したから安心」ではなく、「\u003cstrong\u003e自動化が止まったときに気づける\u003c/strong\u003e」ところまで作って、ようやく放置しない仕組みになると感じました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"445:1-445:9\"\u003e\n\u003cspan id=\"参考\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考\u003c/h2\u003e\n\u003cul data-sourcepos=\"447:1-452:171\"\u003e\n\u003cli data-sourcepos=\"447:1-447:142\"\u003e\u003ca href=\"https://docs.github.com/en/actions/concepts/security/github_token\" rel=\"nofollow noopener\" target=\"_blank\"\u003eGITHUB_TOKEN（When GITHUB_TOKEN triggers workflow runs）- GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"448:1-448:175\"\u003e\u003ca href=\"https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_run\" rel=\"nofollow noopener\" target=\"_blank\"\u003eEvents that trigger workflows（workflow_run）- GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"449:1-449:175\"\u003e\u003ca href=\"https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet\" rel=\"nofollow noopener\" target=\"_blank\"\u003eWorkflow syntax（Filter pattern cheat sheet）- GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"450:1-450:169\"\u003e\u003ca href=\"https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/automate-dependabot-with-actions\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAutomating Dependabot with GitHub Actions - GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"451:1-451:169\"\u003e\u003ca href=\"https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference\" rel=\"nofollow noopener\" target=\"_blank\"\u003eDependabot options reference（groups / ignore）- GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003cli data-sourcepos=\"452:1-452:171\"\u003e\u003ca href=\"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\" rel=\"nofollow noopener\" target=\"_blank\"\u003eManaging your personal access tokens - GitHub Docs\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"## はじめに\n\n依存パッケージの更新を放置しないために、Dependabot の PR を自動マージするようにしました。\n自動マージそのものは数行で書けます。ただ、**「マージ → main で CI → 本番デプロイ」までつなげると、エラーも出さずに静かに壊れる箇所がいくつもありました**。\n\nこの記事は、GitHub Actions と Dependabot を使っている個人開発者・小規模チーム向けです。\nそのまま持ち込めるコードに加えて、「なぜそう書くのか」を1つずつ説明します。\n\n## TL;DR\n\n- **`GITHUB_TOKEN` でマージすると、main への push で CI が起動しない**。E2E もデプロイも走らず、エラーも出ない。マージには fine-grained PAT を使う\n- **トリガーは `pull_request` ではなく `workflow_run`（CI 完了）にする**。Dependabot が起動したワークフローには読み取り専用トークンしか渡らず、Secrets も渡らない\n- **PAT を持つワークフローでは、PR のコードを checkout も実行もしない**。依存のインストールスクリプトに書き込み権限付きのトークンを晒さないため\n- **Dependabot の PR では Actions Secrets が渡らず、E2E が必ず落ちる**。PR 段階では E2E をスキップし、main への push では必ず回してデプロイのゲートにする\n- **PAT の期限切れ・空のシークレット・放置 PR は「静かに」壊れる**。疎通確認ジョブと放置 PR 通知で検知する\n\n## 前提\n\n- GitHub Actions で CI（lint / 型チェック / テスト / ビルド / E2E）を回している\n- main への push で CI が全部通ったときだけ、CI の `deploy` ジョブが本番デプロイする\n  - ホスティング側の Git 連携による自動デプロイは止めておく（CI を通らずにデプロイされてしまうため）\n- 例は npm ですが、言語やフレームワークは問いません\n\n## 全体像\n\n```mermaid\nflowchart TD\n    A[週1回: Dependabot が npm と GitHub Actions をチェック] --\u003e B{更新の種類}\n    B --\u003e|minor / patch| C[1本のまとめPR\u003cbr/\u003edependabot/\u0026lt;eco\u0026gt;/minor-and-patch-\u0026lt;hash\u0026gt;]\n    B --\u003e|major| D[個別PR\u003cbr/\u003e自動マージしない・人間がレビュー]\n    C --\u003e E[CI 緑]\n    E --\u003e|workflow_run| F[auto-merge\u003cbr/\u003ePAT で squash マージ]\n    F --\u003e G[main への push → CI（E2E 含む）]\n    G --\u003e|全部緑| H[本番デプロイ]\n    G --\u003e|どれか赤| I[デプロイしない\u003cbr/\u003e本番は前回の正常版のまま]\n    J[スキャン前日: 放置PR通知] -.-\u003e D\n    J -.-\u003e C\n    K[手動: token-check\u003cbr/\u003ePAT の疎通確認] -.-\u003e F\n```\n\n目的は「常に最新であること」ではありません。\n**まとめて上げる羽目にならないよう小さく継続的に追従し、1回あたりの移行コストとリスクを下げる**ことです。\n\n登場するファイルは3つ + CI です。\n\n| ファイル | 役割 |\n|---|---|\n| `.github/dependabot.yml` | 更新のグループ化と ignore |\n| `.github/workflows/dependabot-auto-merge.yml` | CI 完了後の自動マージ + PAT の疎通確認 |\n| `.github/workflows/dependabot-review-notify.yml` | 人間の対応が必要な PR の通知 |\n| `.github/workflows/ci.yml` | 既存の CI（E2E の条件とデプロイのゲートだけ触る） |\n\n## 1. Dependabot の設定：グループ化と ignore の基準\n\n```yaml:.github/dependabot.yml\nversion: 2\nupdates:\n  - package-ecosystem: npm\n    directory: \"/\"\n    schedule:\n      interval: weekly\n      day: monday\n      time: \"09:00\"\n      timezone: Asia/Tokyo\n    open-pull-requests-limit: 5\n    groups:\n      # minor / patch は1本のPRにまとめる。メジャーだけ個別PR。\n      # グループ名はブランチ名 dependabot/npm_and_yarn/minor-and-patch-\u003chash\u003e に現れ、\n      # 自動マージ判定に使われる。名前を変えるときはワークフロー側も直すこと。\n      minor-and-patch:\n        patterns: [\"*\"]\n        update-types: [minor, patch]\n    ignore:\n      # 型定義は実行環境の Node メジャーと揃える。Node 本体を上げるときに手動で。\n      - dependency-name: \"@types/node\"\n        update-types: [\"version-update:semver-major\"]\n      # 例: 上流が未対応で CI が落ちるもの。対応版が出たら外す（外す条件を必ず書く）。\n      # - dependency-name: \"eslint\"\n      #   update-types: [\"version-update:semver-major\"]\n\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: weekly\n      day: monday\n      time: \"09:00\"\n      timezone: Asia/Tokyo\n    open-pull-requests-limit: 3\n    groups:\n      # npm 側と同じグループ名にする（ワークフローがブランチ名で判定するため）\n      minor-and-patch:\n        patterns: [\"*\"]\n        update-types: [minor, patch]\n```\n\n### ブランチ名を「自動マージしてよい」ことの証明にする\n\nグループの `update-types` を minor / patch に限定すると、グループ名がブランチ名に入ります。\n\n```\ndependabot/npm_and_yarn/minor-and-patch-\u003chash\u003e\n```\n\nつまり、**ブランチ名がそのまま「メジャー更新を含まない」ことの証明になる**ので、ワークフロー側はブランチ名だけで判定できます。\n\n注意点は2つです。\n\n- エコシステムが複数あるときは**グループ名を揃える**\n- グループ名を変えるときはワークフローの判定条件も直す。忘れないよう、**両方のファイルにコメントで相互参照を書いておく**\n\nなお、ブランチ名は手で真似できるので、ワークフロー側では「作成者が Dependabot であること」も確認します（後述）。\n\n### メジャー更新を ignore する基準\n\nignore するのは次の2種類に絞っています。\n\n**① 「新しいほど良い」とは限らない依存**\n\n代表例が `@types/node` のような実行環境の型定義です。本番の Node より新しいメジャーを入れると、実行環境に存在しない API が型の上では OK になります。\n結果、**型チェックは緑なのに本番で落ちる**という最悪のパターンになります。実行環境を上げるときに手動で揃えます。\n\n**② 上流が未対応で、上げると CI が必ず落ちるもの**\n\nリンターや TypeScript のメジャー更新で、プラグインや周辺ツールが追いついていないケースです。\n\nここで学んだのは、**peer 宣言や `npm install --dry-run` が通っても互換性の証明にはならない**ということです。実際に上げて CI で確かめるしかありません。\n\nそして ignore には**「いつ外すか」の条件を必ずコメントで書きます**。書かないと永久に放置されます。\n\n## 2. 自動マージのワークフロー\n\n```yaml:.github/workflows/dependabot-auto-merge.yml\nname: Dependabot auto-merge\n\n# pull_request ではなく workflow_run: Dependabot 起動の実行は読み取り専用トークンのため。\n# PR のコードは checkout しない: PAT を依存のインストールスクリプトに晒さないため。\non:\n  workflow_run:\n    workflows: [\"CI\"]\n    types: [completed]\n    branches:\n      - \"dependabot/*/minor-and-patch-*\"\n  workflow_dispatch:   # token-check 用\n\npermissions:\n  contents: read   # マージは PAT で行うので既定トークンは読み取りのみ\n\njobs:\n  auto-merge:\n    runs-on: ubuntu-latest\n    if: \u003e-\n      ${{ github.event_name == 'workflow_run'\n      \u0026\u0026 github.event.workflow_run.event == 'pull_request'\n      \u0026\u0026 github.event.workflow_run.conclusion == 'success'\n      \u0026\u0026 startsWith(github.event.workflow_run.head_branch, 'dependabot/')\n      \u0026\u0026 contains(github.event.workflow_run.head_branch, '/minor-and-patch-') }}\n    steps:\n      - name: CI が通った Dependabot PR をマージ\n        env:\n          # GITHUB_TOKEN でマージすると main への push で CI が起動しない → PAT を使う\n          GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}\n          REPO: ${{ github.repository }}\n          BRANCH: ${{ github.event.workflow_run.head_branch }}\n        run: |\n          set -euo pipefail\n          if [ -z \"${GH_TOKEN:-}\" ]; then\n            echo \"::error::AUTOMERGE_TOKEN が未設定です。\"\n            exit 1\n          fi\n          # 作成者が Dependabot であることも確認（ブランチ名の偽装対策）\n          num=\"$(gh pr list --repo \"$REPO\" --head \"$BRANCH\" --state open \\\n                   --json number,author \\\n                   --jq '[.[] | select(.author.login == \"app/dependabot\" or .author.login == \"dependabot[bot]\") | .number] | first // empty')\"\n          if [ -z \"$num\" ]; then\n            echo \"対象の open な Dependabot PR がありません。\"\n            exit 0\n          fi\n          gh pr merge \"$num\" --repo \"$REPO\" --squash --delete-branch\n\n  token-check:\n    if: ${{ github.event_name == 'workflow_dispatch' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: AUTOMERGE_TOKEN の疎通確認（マージはしない）\n        env:\n          GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}\n          REPO: ${{ github.repository }}\n        run: |\n          set -euo pipefail\n          [ -n \"${GH_TOKEN:-}\" ] || { echo \"::error::AUTOMERGE_TOKEN が空です（--body \\\"\\$(...)\\\" の失敗など）\"; exit 1; }\n          gh api \"repos/$REPO\" --jq '.full_name' \u003e /dev/null 2\u003e\u00261 \\\n            || { echo \"::error::リポジトリを読めません（期限切れ・Repository access 外）\"; exit 1; }\n          gh pr list --repo \"$REPO\" --state open --limit 1 \u003e /dev/null 2\u003e\u00261 \\\n            || { echo \"::error::Pull requests を読めません\"; exit 1; }\n          push_perm=\"$(gh api \"repos/$REPO\" --jq '.permissions.push // \"unknown\"')\"\n          [ \"$push_perm\" = \"true\" ] \u0026\u0026 echo \"OK: 書き込み権限あり\" \\\n            || echo \"::warning::書き込み権限を確認できません (permissions.push=$push_perm)\"\n```\n\nこのファイルには、ハマりどころが3つ詰まっています。\n\n### 罠①：`GITHUB_TOKEN` でマージすると、その後の CI もデプロイも起動しない\n\nこれが一番の罠です。\n\nGitHub には、**`GITHUB_TOKEN` で起こしたイベントからは新しいワークフローを起動しない**という仕様があります（`workflow_dispatch` と `repository_dispatch` を除く）。ワークフローが無限に連鎖するのを防ぐためです。\n\nそのため `GITHUB_TOKEN` でマージすると、こうなります。\n\n1. 自動マージは成功する\n2. main に push される\n3. **でも CI が起動しない** → E2E もデプロイも走らない\n\n**「マージされたのに、検証もデプロイもされていない」状態になり、エラーも出ません。** Actions の画面を見に行くまで気づけません。\n\n対策は、fine-grained PAT（Personal Access Token）を作って Secrets に登録し、マージに使うことです。PAT でのマージは通常の push として扱われるので、CI がちゃんと起動します。\n\nPAT の権限は最小限にします。\n\n| 項目 | 設定 |\n|---|---|\n| Repository access | 対象リポジトリのみ |\n| Contents | Read and write |\n| Pull requests | Read and write |\n\n### 罠②：トリガーを `pull_request` にすると、マージ API を呼べない\n\n素直に書くと `on: pull_request` で Dependabot の PR を拾いたくなります。しかし、**Dependabot が起動したワークフローには読み取り専用の `GITHUB_TOKEN` しか渡らず、Actions の Secrets も渡りません**。これではマージできません。\n\n一方、CI の完了イベント（`workflow_run`）から起動したワークフローは、元の実行が Dependabot 由来でも通常の権限と Secrets で動きます。なので「CI が完了した」タイミングでマージ判定をします。\n\n`workflow_run.branches` の絞り込みも入れておくのがおすすめです。\n\n```yaml\n    branches:\n      - \"dependabot/*/minor-and-patch-*\"\n```\n\n- `*` は `/` に一致しないので、`dependabot/npm_and_yarn/minor-and-patch-xxx` の形だけにマッチします\n- 絞らないと、main への push を含む**すべての CI 完了で起動**し、`if` で弾かれた `skipped` の実行が Actions 一覧に毎回並んでうるさくなります\n- `if` 条件は多層防御としてそのまま残しておきます\n\n### 罠③：PAT を持つワークフローで PR のコードを動かしてはいけない\n\nこのワークフローは `actions/checkout` をしていません。意図的です。\n\n更新後の依存をインストールすると、`postinstall` などのスクリプトが動きます。そこに**書き込み権限付きの PAT が見える状態だと、サプライチェーン攻撃の格好の的**になります。\n\nそこで役割をきっちり分けます。\n\n| 役割 | Secrets | やること |\n|---|---|---|\n| CI | 持たない | 依存を実際にインストールして検証する |\n| auto-merge | PAT を持つ | GitHub API を叩くだけ（コードは触らない） |\n\n「CI の最後にマージのジョブを足せば1ファイルで済むのでは？」とも考えましたが、やめました。\n\n- Dependabot 起動の CI に PAT を渡すには、PAT を **Dependabot secrets にも二重登録**する必要がある → 期限更新の管理箇所が増える\n- PAT と依存コードの実行が同じワークフローに来て、**隔離の境界が曖昧になる**\n\n得られるのは見た目の整理だけなので、割に合いません。\n\n:::note info\nGitHub 標準の auto-merge（`gh pr merge --auto`）は、ブランチ保護で必須チェックを設定していることが前提です。無料プランのプライベートリポジトリなど、ブランチ保護を使えない環境では、このように自作する意味があります。\n:::\n\n## 3. Dependabot の PR と Secrets・E2E の扱い\n\n### 罠④：Dependabot の PR では E2E が必ず落ちる\n\n罠②と同じ理由で、Dependabot が起動した実行には Actions の Secrets が渡りません（渡るのは別枠の「Dependabot secrets」だけです）。\nE2E が外部サービスの接続情報を Secrets から読んでいる場合、値が空になって**必ず失敗します**。\n\nここでは、**Dependabot の PR では E2E をスキップする**と判断しました。\n毎週必ず赤い CI が出ると、**CI そのものが信用されなくなる**からです。\n\nただし、デプロイ前のゲートは緩めません。\n\n```yaml:.github/workflows/ci.yml（関係部分のみ）\nname: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  lint-build:   # lint → 型チェック → テスト → ビルド（Secrets 不要）\n    ...\n  e2e:\n    # Dependabot 発の PR とフォークの PR は除外（Secrets が渡らず必ず落ちるため）。\n    # push イベントでは実行者に関わらず常に実行＝デプロイ前のゲートは維持。\n    if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository \u0026\u0026 github.actor != 'dependabot[bot]') }}\n    ...\n  deploy:\n    needs: [lint-build, e2e]\n    if: ${{ github.event_name == 'push' \u0026\u0026 github.ref == 'refs/heads/main' }}\n    ...\n```\n\nポイントは次のとおりです。\n\n- **push イベントでは実行者に関係なく E2E を必ず回す**（PAT でのマージ後の push はここに来る）\n- `deploy` は `needs` で E2E の通過を条件にする\n- E2E が落ちればデプロイはスキップされ、**本番は前回の正常版のまま残る**。main を revert するだけで復旧できる\n\nつまり「PR 段階の E2E」は省略しても、「本番に出る前の E2E」は省略していません。\n\n:::note\n代替案として、Dependabot secrets に同じ値を登録して除外条件を外す方法もあります。二重管理のコストと引き換えに、PR の段階で E2E まで回せます。\n:::\n\n### 罠⑤：0.x 系パッケージの minor 更新は破壊的変更を含みうる\n\nsemver 上、`0.12 → 0.13` は minor 扱いです。しかし 0.x は安定性の保証がないため、実質的な破壊的変更が入ることがあります。そして**自動マージの対象に入ってしまいます**。\n\n防波堤になるのは、CI（型チェック・テスト・ビルド）とマージ後の E2E です。上の構成なら落ちてもデプロイされないので、**失敗は安全側に倒れます**。\n\n心配なパッケージがあれば、そのパッケージだけグループから外して個別 PR にする手もあります。\n\n## 4. 静かに壊れる箇所への対策\n\n自動化は「動いているうちは見なくなる」ので、**止まったことに気づけない**のが一番怖いところです。ここでは3つの「静かな故障」に備えます。\n\n### PAT の期限切れ → `token-check` で疎通確認\n\nfine-grained PAT には有効期限があります。切れても、**PR が open のまま残るだけで表面上は何も起きません**。\n\nそこで、auto-merge ワークフローに手動実行（`workflow_dispatch`）用の `token-check` ジョブを入れています（前掲のコード参照）。確認する順番は次のとおりで、**マージは一切しません**。\n\n1. トークンが空でないか\n2. リポジトリを読めるか（期限切れ・Repository access 外ならここで落ちる）\n3. PR を読めるか\n4. 書き込み権限があるか\n\nPAT を更新したら、Actions 画面からこのジョブを手動実行して緑になることを確認します。\n\n### シークレットが空文字で登録される → コマンド置換を使わない\n\n`gh secret set` でこう書くと危険です。\n\n```bash\ngh secret set AUTOMERGE_TOKEN --body \"$(some-command)\"\n```\n\nコマンド置換が失敗しても、**空文字のまま登録に成功してしまいます**。エラーにならないので気づけません。\n\n対策は次のいずれかです。\n\n- ブラウザの Settings → Secrets and variables → Actions から登録する\n- クリップボード経由にする（macOS なら `--body \"$(pbpaste)\"`）\n\nいずれにしても、**トークン文字列をシェルのコマンドラインに直接置かない**ようにします（シェルの履歴に残るため）。\n\n### 放置 PR → スキャン前日に GitHub 自身の通知で知らせる\n\n次の2種類の PR を、Dependabot のスキャン前日に洗い出します。\n\n- **メジャー更新の PR**（そもそも自動マージしない）\n- **72時間以上 open のままの minor-and-patch PR**（自動マージが機能していない疑い）\n\n```yaml:.github/workflows/dependabot-review-notify.yml\nname: Dependabot 要レビューPR通知\n\non:\n  schedule:\n    - cron: \"0 1 * * 0\"   # 毎週日曜 10:00 JST（Dependabot のスキャン前日）\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  notify:\n    runs-on: ubuntu-latest\n    steps:\n      - env:\n          GH_TOKEN: ${{ github.token }}   # マージしないので GITHUB_TOKEN で足りる\n          REPO: ${{ github.repository }}\n          OWNER: your-github-id\n        run: |\n          set -euo pipefail\n          prs=\"$(gh pr list --repo \"$REPO\" --state open \\\n                   --json number,title,headRefName,createdAt,author \\\n                   --jq '[.[] | select(.author.login == \"app/dependabot\" or .author.login == \"dependabot[bot]\")]')\"\n          count=\"$(echo \"$prs\" | jq 'length')\"\n          [ \"$count\" -eq 0 ] \u0026\u0026 exit 0\n          now_epoch=\"$(date -u +%s)\"\n          for i in $(seq 0 $((count - 1))); do\n            number=\"$(echo \"$prs\" | jq -r \".[$i].number\")\"\n            branch=\"$(echo \"$prs\" | jq -r \".[$i].headRefName\")\"\n            created_epoch=\"$(date -u -d \"$(echo \"$prs\" | jq -r \".[$i].createdAt\")\" +%s)\"\n            age_hours=$(( (now_epoch - created_epoch) / 3600 ))\n            reason=\"\"\n            if [[ \"$branch\" != dependabot/*/minor-and-patch-* ]]; then\n              reason=\"自動マージ対象外（メジャー更新など）\"\n            elif [ \"$age_hours\" -ge 72 ]; then\n              reason=\"自動マージ対象のはずが ${age_hours} 時間 open のまま\"\n            fi\n            if [ -n \"$reason\" ]; then\n              gh pr edit \"$number\" --repo \"$REPO\" --add-assignee \"$OWNER\" || true\n              gh pr edit \"$number\" --repo \"$REPO\" --add-reviewer \"$OWNER\" || true\n              gh pr comment \"$number\" --repo \"$REPO\" \\\n                --body \"🔔 @$OWNER このPRは自動マージされません（理由: ${reason}）。\"\n            fi\n          done\n```\n\n設計上のこだわりは3つです。\n\n- **外部サービス（Slack など）は使わない**。assign・レビュー依頼・メンションで、GitHub 自身の通知に載せる\n- **該当が無ければ何もしない**。空振りの通知を送るとノイズになり、そのうち見なくなる\n- こちらは**マージも push もしないので `GITHUB_TOKEN` で足りる**。`schedule` 起動は Dependabot 由来ではないので、書き込み権限も普通に使える\n\n## 5. 導入手順\n\n1. `.github/dependabot.yml` を置く\n2. fine-grained PAT を発行する（対象リポジトリのみ・Contents RW / Pull requests RW）\n3. ブラウザの Settings → Secrets and variables → **Actions** に `AUTOMERGE_TOKEN` として登録する\n4. 2つのワークフローを置き、CI の E2E / deploy の条件を調整する\n5. Actions 画面から「Dependabot auto-merge」を手動実行し、`token-check` が緑になることを確認する\n6. 急ぐときは Insights → Dependency graph → Dependabot の「Check for updates」で即時スキャンする（設定の不備もここに表示される）\n\nプレースホルダは次の3つです。\n\n| プレースホルダ | 中身 |\n|---|---|\n| `your-github-id` | 通知先の GitHub ID |\n| `AUTOMERGE_TOKEN` | PAT を登録する Secret 名 |\n| `CI` | CI ワークフローの `name` |\n\n## まとめ\n\n- 自動マージ自体は簡単。**難しいのは「マージ後に CI とデプロイがちゃんと動くこと」**\n- `GITHUB_TOKEN` でマージすると後続のワークフローが起動しない → **PAT でマージする**\n- Dependabot 起動の実行は読み取り専用・Secrets なし → **`workflow_run` でマージ判定する**\n- PAT を持つワークフローでは**コードを checkout も実行もしない**。検証は Secrets を持たない CI に任せる\n- Dependabot の PR では E2E をスキップしても、**main への push では必ず E2E を回してデプロイのゲートにする**\n- PAT の期限切れ・空のシークレット・放置 PR は静かに壊れる → **疎通確認ジョブと放置 PR 通知で検知する**\n\n「自動化したから安心」ではなく、「**自動化が止まったときに気づける**」ところまで作って、ようやく放置しない仕組みになると感じました。\n\n## 参考\n\n- [GITHUB_TOKEN（When GITHUB_TOKEN triggers workflow runs）- GitHub Docs](https://docs.github.com/en/actions/concepts/security/github_token)\n- [Events that trigger workflows（workflow_run）- GitHub Docs](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_run)\n- [Workflow syntax（Filter pattern cheat sheet）- GitHub Docs](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet)\n- [Automating Dependabot with GitHub Actions - GitHub Docs](https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/automate-dependabot-with-actions)\n- [Dependabot options reference（groups / ignore）- GitHub Docs](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference)\n- [Managing your personal access tokens - GitHub Docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T11:05:19+09:00","group":null,"id":"323ff90fa7558dd7bd59","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"GitHubActions","versions":[]},{"name":"CICD","versions":[]},{"name":"dependabot","versions":[]},{"name":"Security","versions":[]},{"name":"個人開発","versions":[]}],"title":"Dependabot 自動マージで本番デプロイまで回したら、GITHUB_TOKEN の罠で CI もデプロイも起動しなかった話","updated_at":"2026-09-27T11:07:51+09:00","url":"https://qiita.com/satoshi_061/items/323ff90fa7558dd7bd59","user":{"description":null,"facebook_id":null,"followees_count":4,"followers_count":1,"github_login_name":null,"id":"satoshi_061","items_count":17,"linkedin_id":null,"location":null,"name":"","organization":null,"permanent_id":590349,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590349/profile-images/1606889993","team_only":false,"twitter_screen_name":"nskwsatoshi_061","website_url":null},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:25\"\u003e\u003cstrong\u003eこの記事の要点\u003c/strong\u003e\u003c/p\u003e\n\u003cul data-sourcepos=\"3:1-6:0\"\u003e\n\u003cli data-sourcepos=\"3:1-3:169\"\u003eAI生成スライドそのまま登壇で違和感を感じる主因は、スライドの情報密度と話者の話す速度が噛み合っていないことです。\u003c/li\u003e\n\u003cli data-sourcepos=\"4:1-4:166\"\u003e生成AIツールは体裁の整った資料を素早く作れますが、具体的な数値や固有の経験は書き込まないと空白のまま残ります。\u003c/li\u003e\n\u003cli data-sourcepos=\"5:1-6:0\"\u003e対策は「生成AIに下書きさせて人間が削る」順番を守ること、スライド1枚に主張を1つだけ残すことです。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"7:1-7:543\"\u003e最近の勉強会やカンファレンスで、AI生成スライドをそのまま使った登壇を見かける機会が増えました。見た目は整っているのに、なぜか頭に残らない。そんな感覚を持ったエンジニアも多いのではないでしょうか。この記事では、AI生成スライドそのまま登壇に違和感を覚える理由を分解し、実際に生成AIでスライド作成を試した所感を交えながら、聞き手に刺さる資料にするための工夫を紹介します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"9:1-9:71\"\u003e\n\u003cspan id=\"そもそもai生成スライドそのまま登壇とは何か\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82ai%E7%94%9F%E6%88%90%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E7%99%BB%E5%A3%87%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eそもそも「AI生成スライドそのまま登壇」とは何か\u003c/h2\u003e\n\u003cp data-sourcepos=\"11:1-11:245\"\u003eここでいう「AI生成スライドそのまま登壇」とは、ChatGPT や Claude、Gamma のようなツールにテーマを渡し、出力されたスライドをほぼ手を加えずに発表資料として使う状態を指します。\u003c/p\u003e\n\u003cp data-sourcepos=\"13:1-13:342\"\u003e見出しと箇条書きが整い、配色も統一されているため、一見すると完成度は高く見えます。問題は中身です。テンプレート的な構成と、どこかで見たような言い回しが並びやすく、話者の一次情報が薄いまま登壇本番を迎えてしまうケースが目立ちます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"15:1-15:39\"\u003e\n\u003cspan id=\"なぜ違和感を持たれるのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E9%81%95%E5%92%8C%E6%84%9F%E3%82%92%E6%8C%81%E3%81%9F%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜ違和感を持たれるのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"17:1-17:70\"\u003e違和感の正体は、大きく2つの要因に分けられます。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"19:1-19:43\"\u003e\n\u003cspan id=\"情報密度と話者の呼吸のズレ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%83%85%E5%A0%B1%E5%AF%86%E5%BA%A6%E3%81%A8%E8%A9%B1%E8%80%85%E3%81%AE%E5%91%BC%E5%90%B8%E3%81%AE%E3%82%BA%E3%83%AC\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e情報密度と話者の呼吸のズレ\u003c/h3\u003e\n\u003cp data-sourcepos=\"21:1-21:195\"\u003e生成AIは文章として成立する量の情報を1枚に詰め込みがちです。人が話しながらめくるスライドとしては、密度が高すぎることがよくあります。\u003c/p\u003e\n\u003cp data-sourcepos=\"23:1-23:243\"\u003e話者がスライドを読み上げる形になると、聴衆はスライドの文字を先に読み終えてしまいます。結果として、話の内容とスライドの表示タイミングがずれ、退屈さにつながります。\u003c/p\u003e\n\u003ch3 data-sourcepos=\"25:1-25:58\"\u003e\n\u003cspan id=\"どのツールで作っても似た見た目になる\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%A9%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB%E3%81%A7%E4%BD%9C%E3%81%A3%E3%81%A6%E3%82%82%E4%BC%BC%E3%81%9F%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%81%AB%E3%81%AA%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eどのツールで作っても似た見た目になる\u003c/h3\u003e\n\u003cp data-sourcepos=\"27:1-27:272\"\u003e生成AIのスライド機能は、学習データに含まれる典型的な構成パターンを再現する性質を持っています。そのため、ツールが違っても「概要→背景→手法→まとめ」のような似た骨格になりやすいです。\u003c/p\u003e\n\u003cp data-sourcepos=\"29:1-29:270\"\u003e聞き手側は複数のカンファレンスをはしごすることが多く、似た構成のスライドを連続で見ると既視感が強まります。内容自体は別物でも、パターンの反復が「またこの形か」という印象を生みます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"31:1-31:59\"\u003e\n\u003cspan id=\"実際に生成aiでスライド作成を試した所感\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%AE%9F%E9%9A%9B%E3%81%AB%E7%94%9F%E6%88%90ai%E3%81%A7%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89%E4%BD%9C%E6%88%90%E3%82%92%E8%A9%A6%E3%81%97%E3%81%9F%E6%89%80%E6%84%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e実際に生成AIでスライド作成を試した所感\u003c/h2\u003e\n\u003cp data-sourcepos=\"33:1-33:277\"\u003e筆者も社内の技術共有会に向けて、Claude にアウトラインを作らせてから資料化したことがあります。所要時間は体感でかなり短縮できましたが、出てきた文章はどれも一般論の範囲にとどまっていました。\u003c/p\u003e\n\u003cp data-sourcepos=\"35:1-35:403\"\u003e具体的な失敗談や、自分のチームだけで起きた数値変化のようなものは、当然ながら生成AI側からは出てきません。後から自分で書き足す作業が必要で、この書き足しの量が資料の説得力をほぼ決めていると感じました。生成AIは骨組みを作る道具であり、肉付けは人間の仕事だと実感した経験です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"37:1-37:51\"\u003e\n\u003cspan id=\"聞き手に刺さる登壇資料にする工夫\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%81%9E%E3%81%8D%E6%89%8B%E3%81%AB%E5%88%BA%E3%81%95%E3%82%8B%E7%99%BB%E5%A3%87%E8%B3%87%E6%96%99%E3%81%AB%E3%81%99%E3%82%8B%E5%B7%A5%E5%A4%AB\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e聞き手に刺さる登壇資料にする工夫\u003c/h2\u003e\n\u003cp data-sourcepos=\"39:1-39:131\"\u003e生成AIを使うこと自体は否定しません。使い方の順番と、手を加える箇所を絞ることが重要です。\u003c/p\u003e\n\u003col data-sourcepos=\"41:1-46:0\"\u003e\n\u003cli data-sourcepos=\"41:1-41:91\"\u003eまず生成AIにアウトラインだけを作らせる(本文は書かせすぎない)\u003c/li\u003e\n\u003cli data-sourcepos=\"42:1-42:80\"\u003eスライド1枚につき主張を1つに絞り、装飾的な文章を削る\u003c/li\u003e\n\u003cli data-sourcepos=\"43:1-43:106\"\u003e自分の失敗談・数値・スクリーンショットなど一次情報を最低1枚は差し込む\u003c/li\u003e\n\u003cli data-sourcepos=\"44:1-44:90\"\u003e声に出して通し読みし、読み上げないと成立しない枚数を減らす\u003c/li\u003e\n\u003cli data-sourcepos=\"45:1-46:0\"\u003e質疑で聞かれそうな細部は、スライドではなく口頭説明に回す\u003c/li\u003e\n\u003c/ol\u003e\n\u003ctable data-sourcepos=\"47:1-52:79\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"47:1-47:60\"\u003e\n\u003cth data-sourcepos=\"47:2-47:9\"\u003e観点\u003c/th\u003e\n\u003cth data-sourcepos=\"47:11-47:32\"\u003eAI生成そのまま\u003c/th\u003e\n\u003cth data-sourcepos=\"47:34-47:59\"\u003e人の手を加えた版\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"49:1-49:46\"\u003e\n\u003ctd data-sourcepos=\"49:2-49:15\"\u003e作成速度\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:17-49:24\"\u003e速い\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:26-49:45\"\u003eやや遅くなる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:64\"\u003e\n\u003ctd data-sourcepos=\"50:2-50:24\"\u003e一次情報の有無\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:26-50:36\"\u003e乏しい\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:38-50:63\"\u003e追記で確保できる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:52\"\u003e\n\u003ctd data-sourcepos=\"51:2-51:21\"\u003e聴衆の既視感\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:23-51:36\"\u003e出やすい\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:38-51:51\"\u003e減らせる\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"52:1-52:79\"\u003e\n\u003ctd data-sourcepos=\"52:2-52:36\"\u003e話者本人の理解度の担保\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:38-52:45\"\u003e薄い\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:47-52:78\"\u003e話しながら鍛えられる\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 data-sourcepos=\"54:1-54:75\"\u003e\n\u003cspan id=\"デメリット向いていない人うまくいかないケース\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E5%90%91%E3%81%84%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E4%BA%BA%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%82%B1%E3%83%BC%E3%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eデメリット・向いていない人・うまくいかないケース\u003c/h2\u003e\n\u003cp data-sourcepos=\"56:1-56:107\"\u003e生成AIによるスライド作成は、すべての登壇に向いているわけではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"58:1-58:224\"\u003e社内の定例報告や進捗共有のように、情報伝達だけが目的の場では、体裁が整ったAI生成スライドはむしろ効率的です。無理に一次情報を盛り込む必要はありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-60:615\"\u003e一方、登壇者自身の専門性や実績を示したい発表、初めて会う聴衆に信頼してもらう必要がある場では、AI生成そのままは不向きです。具体的な失敗や試行錯誤の過程が抜け落ちると、聞き手は「本当にこの人がやったのか」という疑いを持ちやすくなります。また、生成AIの出力を検証せずに専門用語を使うと、誤った説明をそのまま話してしまうリスクもあります。ファクトチェックの時間が取れない場合は、生成AIへの依存度を下げたほうが安全です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"62:1-62:24\"\u003e\n\u003cspan id=\"まとめに代えて\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%AB%E4%BB%A3%E3%81%88%E3%81%A6\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめに代えて\u003c/h2\u003e\n\u003cp data-sourcepos=\"64:1-64:248\"\u003eAI生成スライドそのまま登壇の問題は、ツールの性能ではなく使う順番にあります。骨組みを任せ、肉付けを自分でやる。この分担さえ守れば、資料作成の速度は落とさずに済みます。\u003c/p\u003e\n\u003cp data-sourcepos=\"66:1-66:279\"\u003e次に登壇の機会があれば、スライドを作り終えた後に「このスライドから自分の名前を消しても成立するか」を自問してみてください。成立してしまうなら、まだ一次情報が足りていないというサインです。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"68:1-68:21\"\u003e\n\u003cspan id=\"よくある質問\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E8%B3%AA%E5%95%8F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eよくある質問\u003c/h2\u003e\n\u003cp data-sourcepos=\"70:1-71:192\"\u003e\u003cstrong\u003eAI生成スライドをそのまま使うのは悪いことですか?\u003c/strong\u003e\u003cbr\u003e\n目的次第です。社内の情報共有など効率重視の場では問題ありませんが、専門性や信頼を示したい登壇では一次情報の追記をおすすめします。\u003c/p\u003e\n\u003cp data-sourcepos=\"73:1-74:135\"\u003e\u003cstrong\u003eAI生成スライドかどうかは聞き手にバレますか?\u003c/strong\u003e\u003cbr\u003e\n構成の既視感や具体性の薄さから、複数の登壇を見慣れた聴衆には気づかれやすい傾向があります。\u003c/p\u003e\n\u003cp data-sourcepos=\"76:1-77:138\"\u003e\u003cstrong\u003e生成AIにスライドを作らせる際のコツはありますか?\u003c/strong\u003e\u003cbr\u003e\n本文まで書かせず、見出しと構成だけを作らせて、中身は自分の経験で埋めるとバランスが取れます。\u003c/p\u003e\n\u003cp data-sourcepos=\"79:1-80:152\"\u003e\u003cstrong\u003eスライド1枚に情報を詰め込みすぎないための目安はありますか?\u003c/strong\u003e\u003cbr\u003e\n主張を1枚1つに絞り、読み上げないと理解できない文章量になっていないか声に出して確認する方法が有効です。\u003c/p\u003e\n\u003cp data-sourcepos=\"82:1-83:114\"\u003e\u003cstrong\u003eAI生成スライドと相性がいい発表形式はありますか?\u003c/strong\u003e\u003cbr\u003e\n進捗報告や定例会など、事実の伝達が主目的でスピード重視の場と相性がよいです。\u003c/p\u003e\n\u003cp data-sourcepos=\"85:1-86:129\"\u003e\u003cstrong\u003e一次情報とは具体的に何を指しますか?\u003c/strong\u003e\u003cbr\u003e\n自分やチームが実際に経験した失敗談、試行錯誤の過程、自分で確認した挙動などを指します。\u003c/p\u003e\n\u003cp data-sourcepos=\"88:1-89:146\"\u003e\u003cstrong\u003e登壇資料の既視感を減らす一番手軽な方法は何ですか?\u003c/strong\u003e\u003cbr\u003e\n生成AIが出しがちなテンプレート構成の見出しを、自分の言葉や具体的なエピソードに書き換えることです。\u003c/p\u003e\n","body":"**この記事の要点**\n\n- AI生成スライドそのまま登壇で違和感を感じる主因は、スライドの情報密度と話者の話す速度が噛み合っていないことです。\n- 生成AIツールは体裁の整った資料を素早く作れますが、具体的な数値や固有の経験は書き込まないと空白のまま残ります。\n- 対策は「生成AIに下書きさせて人間が削る」順番を守ること、スライド1枚に主張を1つだけ残すことです。\n\n最近の勉強会やカンファレンスで、AI生成スライドをそのまま使った登壇を見かける機会が増えました。見た目は整っているのに、なぜか頭に残らない。そんな感覚を持ったエンジニアも多いのではないでしょうか。この記事では、AI生成スライドそのまま登壇に違和感を覚える理由を分解し、実際に生成AIでスライド作成を試した所感を交えながら、聞き手に刺さる資料にするための工夫を紹介します。\n\n## そもそも「AI生成スライドそのまま登壇」とは何か\n\nここでいう「AI生成スライドそのまま登壇」とは、ChatGPT や Claude、Gamma のようなツールにテーマを渡し、出力されたスライドをほぼ手を加えずに発表資料として使う状態を指します。\n\n見出しと箇条書きが整い、配色も統一されているため、一見すると完成度は高く見えます。問題は中身です。テンプレート的な構成と、どこかで見たような言い回しが並びやすく、話者の一次情報が薄いまま登壇本番を迎えてしまうケースが目立ちます。\n\n## なぜ違和感を持たれるのか\n\n違和感の正体は、大きく2つの要因に分けられます。\n\n### 情報密度と話者の呼吸のズレ\n\n生成AIは文章として成立する量の情報を1枚に詰め込みがちです。人が話しながらめくるスライドとしては、密度が高すぎることがよくあります。\n\n話者がスライドを読み上げる形になると、聴衆はスライドの文字を先に読み終えてしまいます。結果として、話の内容とスライドの表示タイミングがずれ、退屈さにつながります。\n\n### どのツールで作っても似た見た目になる\n\n生成AIのスライド機能は、学習データに含まれる典型的な構成パターンを再現する性質を持っています。そのため、ツールが違っても「概要→背景→手法→まとめ」のような似た骨格になりやすいです。\n\n聞き手側は複数のカンファレンスをはしごすることが多く、似た構成のスライドを連続で見ると既視感が強まります。内容自体は別物でも、パターンの反復が「またこの形か」という印象を生みます。\n\n## 実際に生成AIでスライド作成を試した所感\n\n筆者も社内の技術共有会に向けて、Claude にアウトラインを作らせてから資料化したことがあります。所要時間は体感でかなり短縮できましたが、出てきた文章はどれも一般論の範囲にとどまっていました。\n\n具体的な失敗談や、自分のチームだけで起きた数値変化のようなものは、当然ながら生成AI側からは出てきません。後から自分で書き足す作業が必要で、この書き足しの量が資料の説得力をほぼ決めていると感じました。生成AIは骨組みを作る道具であり、肉付けは人間の仕事だと実感した経験です。\n\n## 聞き手に刺さる登壇資料にする工夫\n\n生成AIを使うこと自体は否定しません。使い方の順番と、手を加える箇所を絞ることが重要です。\n\n1. まず生成AIにアウトラインだけを作らせる(本文は書かせすぎない)\n2. スライド1枚につき主張を1つに絞り、装飾的な文章を削る\n3. 自分の失敗談・数値・スクリーンショットなど一次情報を最低1枚は差し込む\n4. 声に出して通し読みし、読み上げないと成立しない枚数を減らす\n5. 質疑で聞かれそうな細部は、スライドではなく口頭説明に回す\n\n| 観点 | AI生成そのまま | 人の手を加えた版 |\n|---|---|---|\n| 作成速度 | 速い | やや遅くなる |\n| 一次情報の有無 | 乏しい | 追記で確保できる |\n| 聴衆の既視感 | 出やすい | 減らせる |\n| 話者本人の理解度の担保 | 薄い | 話しながら鍛えられる |\n\n## デメリット・向いていない人・うまくいかないケース\n\n生成AIによるスライド作成は、すべての登壇に向いているわけではありません。\n\n社内の定例報告や進捗共有のように、情報伝達だけが目的の場では、体裁が整ったAI生成スライドはむしろ効率的です。無理に一次情報を盛り込む必要はありません。\n\n一方、登壇者自身の専門性や実績を示したい発表、初めて会う聴衆に信頼してもらう必要がある場では、AI生成そのままは不向きです。具体的な失敗や試行錯誤の過程が抜け落ちると、聞き手は「本当にこの人がやったのか」という疑いを持ちやすくなります。また、生成AIの出力を検証せずに専門用語を使うと、誤った説明をそのまま話してしまうリスクもあります。ファクトチェックの時間が取れない場合は、生成AIへの依存度を下げたほうが安全です。\n\n## まとめに代えて\n\nAI生成スライドそのまま登壇の問題は、ツールの性能ではなく使う順番にあります。骨組みを任せ、肉付けを自分でやる。この分担さえ守れば、資料作成の速度は落とさずに済みます。\n\n次に登壇の機会があれば、スライドを作り終えた後に「このスライドから自分の名前を消しても成立するか」を自問してみてください。成立してしまうなら、まだ一次情報が足りていないというサインです。\n\n## よくある質問\n\n**AI生成スライドをそのまま使うのは悪いことですか?**\n目的次第です。社内の情報共有など効率重視の場では問題ありませんが、専門性や信頼を示したい登壇では一次情報の追記をおすすめします。\n\n**AI生成スライドかどうかは聞き手にバレますか?**\n構成の既視感や具体性の薄さから、複数の登壇を見慣れた聴衆には気づかれやすい傾向があります。\n\n**生成AIにスライドを作らせる際のコツはありますか?**\n本文まで書かせず、見出しと構成だけを作らせて、中身は自分の経験で埋めるとバランスが取れます。\n\n**スライド1枚に情報を詰め込みすぎないための目安はありますか?**\n主張を1枚1つに絞り、読み上げないと理解できない文章量になっていないか声に出して確認する方法が有効です。\n\n**AI生成スライドと相性がいい発表形式はありますか?**\n進捗報告や定例会など、事実の伝達が主目的でスピード重視の場と相性がよいです。\n\n**一次情報とは具体的に何を指しますか?**\n自分やチームが実際に経験した失敗談、試行錯誤の過程、自分で確認した挙動などを指します。\n\n**登壇資料の既視感を減らす一番手軽な方法は何ですか?**\n生成AIが出しがちなテンプレート構成の見出しを、自分の言葉や具体的なエピソードに書き換えることです。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T10:59:07+09:00","group":null,"id":"228305caab24ef22aa33","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"AI生成コンテンツ","versions":[]},{"name":"カンファレンス","versions":[]},{"name":"AI","versions":[]},{"name":"生成AI","versions":[]}],"title":"AI生成スライドそのまま登壇はなぜ聞き手に響かないのか","updated_at":"2026-09-27T10:59:07+09:00","url":"https://qiita.com/syun136_616/items/228305caab24ef22aa33","user":{"description":"","facebook_id":"","followees_count":0,"followers_count":0,"github_login_name":null,"id":"syun136_616","items_count":54,"linkedin_id":"","location":"","name":"駿 西嶋","organization":"","permanent_id":1207668,"profile_image_url":"https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1207668/profile-images/1785723071","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch1 data-sourcepos=\"1:1-1:2\"\u003e\u003c/h1\u003e\n\u003ch2 data-sourcepos=\"3:1-3:30\"\u003e\n\u003cspan id=\"前回までのあらすじ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%89%8D%E5%9B%9E%E3%81%BE%E3%81%A7%E3%81%AE%E3%81%82%E3%82%89%E3%81%99%E3%81%98\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e前回までのあらすじ\u003c/h2\u003e\n\u003cp data-sourcepos=\"5:1-5:414\"\u003e前回、無負荷でモーター単体を測定したところ、歯車を付けた無負荷状態とほとんど同じ電流・騒音になり、「歯車由来の差は、モーターという支配的な要因の陰に隠れて見えなくなっている」という結論に達した。歯形をどれだけ磨いても、電流・騒音というベンチ指標には、もう現れてこないだろうと。\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:220\"\u003e今回、その「モーターの壁」だと思っていたものの正体が、実はモーターそのものではなく、\u003cstrong\u003eモーターマウントの作り方\u003c/strong\u003eだったことが分かった経緯を記録する。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"9:1-9:15\"\u003e\n\u003cspan id=\"きっかけ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eきっかけ\u003c/h2\u003e\n\u003cp data-sourcepos=\"11:1-11:292\"\u003eモーターの保持構造を確認したところ、モーター缶の受け部分（前面・側面・底面）が、PETG製マウントに直接、硬く接触している作りだった。軸受け周りは別として、缶の広い面がそのままマウントに密着している。\u003c/p\u003e\n\u003cp data-sourcepos=\"13:1-13:71\"\u003e試しに、この接触面に0.5mm厚のゴム板を挟んでみた。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"15:1-15:36\"\u003e\n\u003cspan id=\"結果騒音が4db下がった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E7%B5%90%E6%9E%9C%E9%A8%92%E9%9F%B3%E3%81%8C4db%E4%B8%8B%E3%81%8C%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e結果：騒音が4dB下がった\u003c/h2\u003e\n\u003ctable data-sourcepos=\"17:1-20:77\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"17:1-17:28\"\u003e\n\u003cth data-sourcepos=\"17:2-17:9\"\u003e条件\u003c/th\u003e\n\u003cth data-sourcepos=\"17:11-17:18\"\u003e電流\u003c/th\u003e\n\u003cth data-sourcepos=\"17:20-17:27\"\u003e騒音\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"19:1-19:71\"\u003e\n\u003ctd data-sourcepos=\"19:2-19:51\"\u003e硬いマウント（歯車あり・無負荷）\u003c/td\u003e\n\u003ctd data-sourcepos=\"19:53-19:61\"\u003e252.6mA\u003c/td\u003e\n\u003ctd data-sourcepos=\"19:63-19:70\"\u003e70.7dB\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"20:1-20:77\"\u003e\n\u003ctd data-sourcepos=\"20:2-20:57\"\u003eゴム入りマウント（歯車あり・無負荷）\u003c/td\u003e\n\u003ctd data-sourcepos=\"20:59-20:67\"\u003e249.1mA\u003c/td\u003e\n\u003ctd data-sourcepos=\"20:69-20:76\"\u003e66.9dB\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"22:1-22:306\"\u003e電流はほとんど変わらず、騒音だけが約4dB下がった。これまで19個体を測ってきて、材質・Pressure Advance・軸間距離・圧力角補正のどれを変えても69.8〜70.9dBという1.1dBの帯から出てこなかったことを思えば、桁違いの変化だった。\u003c/p\u003e\n\u003cp data-sourcepos=\"24:1-24:299\"\u003e厚みを1mmに増やしても変化はなく、0.5mmで既に効果は飽和していた。軸受け周りにもゴムを追加してみたが、これも騒音はほとんど変わらなかった。振動の伝達経路は、前面・側面・底面に集中していたということになる。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"26:1-26:63\"\u003e\n\u003cspan id=\"決定打モーター単体でも同じだけ下がった\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E6%B1%BA%E5%AE%9A%E6%89%93%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%8D%98%E4%BD%93%E3%81%A7%E3%82%82%E5%90%8C%E3%81%98%E3%81%A0%E3%81%91%E4%B8%8B%E3%81%8C%E3%81%A3%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e決定打：モーター単体でも同じだけ下がった\u003c/h2\u003e\n\u003cp data-sourcepos=\"28:1-28:81\"\u003e歯車を完全に外し、モーター単体で同じ比較をしたところ、\u003c/p\u003e\n\u003ctable data-sourcepos=\"30:1-33:62\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"30:1-30:28\"\u003e\n\u003cth data-sourcepos=\"30:2-30:9\"\u003e条件\u003c/th\u003e\n\u003cth data-sourcepos=\"30:11-30:18\"\u003e電流\u003c/th\u003e\n\u003cth data-sourcepos=\"30:20-30:27\"\u003e騒音\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"32:1-32:62\"\u003e\n\u003ctd data-sourcepos=\"32:2-32:42\"\u003eモーター単体・硬いマウント\u003c/td\u003e\n\u003ctd data-sourcepos=\"32:44-32:52\"\u003e249.7mA\u003c/td\u003e\n\u003ctd data-sourcepos=\"32:54-32:61\"\u003e70.5dB\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"33:1-33:62\"\u003e\n\u003ctd data-sourcepos=\"33:2-33:42\"\u003eモーター単体・ゴムマウント\u003c/td\u003e\n\u003ctd data-sourcepos=\"33:44-33:52\"\u003e247.0mA\u003c/td\u003e\n\u003ctd data-sourcepos=\"33:54-33:61\"\u003e66.8dB\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"35:1-35:324\"\u003e歯車の有無に関係なく、ほぼ同じ約3.7dBの改善が出た。これで、4dBの改善は歯車まわりの何かではなく、\u003cstrong\u003eモーター缶の鉄板の振動と、それを無減衰のまま伝えていた硬いマウントの組み合わせ\u003c/strong\u003eそのものが原因だったことが、ほぼ確定した。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"37:1-37:36\"\u003e\n\u003cspan id=\"なぜここまで効いたのか\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%AA%E3%81%9C%E3%81%93%E3%81%93%E3%81%BE%E3%81%A7%E5%8A%B9%E3%81%84%E3%81%9F%E3%81%AE%E3%81%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eなぜここまで効いたのか\u003c/h2\u003e\n\u003cp data-sourcepos=\"39:1-39:387\"\u003e車のダンパーに例えると分かりやすい。ダンパーのないバネだけの車体は、一度揺れるといつまでも揺れ続ける。ゴムを挟んだことは、単に「柔らかくした」のではなく、ゴムの内部摩擦（ヒステリシス）で振動エネルギーを熱として消費する、ダンパーの役割を持ち込んだことに近い。\u003c/p\u003e\n\u003cp data-sourcepos=\"41:1-41:867\"\u003eさらに、今回の構造（モーター缶という硬いもの、ゴム、PETGマウントという硬いもの、という3層）は、制振の分野で「拘束層減衰」と呼ばれる構成に近かった可能性がある。金属板が振動で微小に伸び縮みするとき、ゴムの片面だけが自由なら単純な伸縮で終わるが、両側を硬いもので挟まれていると、ゴムは伸縮ではなく\u003cstrong\u003eせん断変形\u003c/strong\u003eを強いられる。粘弾性材料はせん断変形の方がはるかに多くのエネルギーを熱に変換できるため、同じ厚みのゴムでも、拘束層になっているかどうかで効き方が大きく変わる。意図したわけではなかったが、モーター缶とマウントに挟まれたことで、この効率の良い減衰モードが働いていたと考えられる。\u003c/p\u003e\n\u003cp data-sourcepos=\"43:1-43:455\"\u003e回転数の反復ばらつきも、硬いマウントのstd57〜66から、ゴムマウントでstd11.3まで縮んだ。振動が減衰されずに伝わり続けている状態では、缶自体がミクロン単位で暴れ続け、それが回転子の運動にも影響していたが、ダンパーが効いたことで缶が静止に近い状態を保てるようになり、回転もろとも安定した、という説明で一貫している。\u003c/p\u003e\n\u003cp data-sourcepos=\"45:1-45:321\"\u003e耳で聞いても「音がまったく違う」というほどの変化だった。4dBという音圧レベルの差だけでは説明しきれない体感差は、単純な音量変化ではなく、硬い共鳴音・耳障りな高調波そのものが、ダンパーによって消えたためだと考えている。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"47:1-47:36\"\u003e\n\u003cspan id=\"この発見が意味すること\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%93%E3%81%AE%E7%99%BA%E8%A6%8B%E3%81%8C%E6%84%8F%E5%91%B3%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eこの発見が意味すること\u003c/h2\u003e\n\u003cp data-sourcepos=\"49:1-49:252\"\u003e前回「モーターの壁」と呼んだものは、正確には「モーターの壁」ではなく「無減衰マウントの壁」だった。歯車の測定・改善に、まだ意味がないと結論づけるのは早すぎたことになる。\u003c/p\u003e\n\u003cp data-sourcepos=\"51:1-51:413\"\u003e床が4dB下がったことで、これまで埋もれていた歯形由来の差が、初めて測定にかかる可能性が出てきた。奇偶差やA/B面の非対称を潰し込んだ「補正2」「補正3」と、20°/20°のまま作った素のPETG-aを、この新しいゴムマウントの上で測り比べる——これが、次に答えを出すべき、最後の宿題として残っている。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"53:1-53:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cul data-sourcepos=\"55:1-60:0\"\u003e\n\u003cli data-sourcepos=\"55:1-55:119\"\u003eモーター缶の広い面をマウントに直接固定する構造は、振動伝達の太い経路になり得る\u003c/li\u003e\n\u003cli data-sourcepos=\"56:1-56:175\"\u003e0.5mm厚のゴムを挟むだけで、騒音が約4dB、しかも安定して下がった。厚みを増やしても頭打りで、軸受け周りへの追加も効果なし\u003c/li\u003e\n\u003cli data-sourcepos=\"57:1-57:134\"\u003eモーター単体でも同じだけ下がったことから、原因は歯車ではなくマウント側にあると確定できた\u003c/li\u003e\n\u003cli data-sourcepos=\"58:1-58:203\"\u003e「歯形は電流・騒音に効かない」という前回の結論は、実は「無減衰マウントという、もっと大きな雑音源に埋もれていただけ」だった可能性がある\u003c/li\u003e\n\u003cli data-sourcepos=\"59:1-60:0\"\u003e次の一手は、下がった床の上で、歯形の効果を再検証すること\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp data-sourcepos=\"61:1-61:168\"\u003e測定器を磨き続けていたら、測定器ではなく測定環境そのものに、一番大きな伸びしろが隠れていた、という結末になった。\u003c/p\u003e\n","body":"# \n\n## 前回までのあらすじ\n\n前回、無負荷でモーター単体を測定したところ、歯車を付けた無負荷状態とほとんど同じ電流・騒音になり、「歯車由来の差は、モーターという支配的な要因の陰に隠れて見えなくなっている」という結論に達した。歯形をどれだけ磨いても、電流・騒音というベンチ指標には、もう現れてこないだろうと。\n\n今回、その「モーターの壁」だと思っていたものの正体が、実はモーターそのものではなく、**モーターマウントの作り方**だったことが分かった経緯を記録する。\n\n## きっかけ\n\nモーターの保持構造を確認したところ、モーター缶の受け部分（前面・側面・底面）が、PETG製マウントに直接、硬く接触している作りだった。軸受け周りは別として、缶の広い面がそのままマウントに密着している。\n\n試しに、この接触面に0.5mm厚のゴム板を挟んでみた。\n\n## 結果：騒音が4dB下がった\n\n| 条件 | 電流 | 騒音 |\n|---|---|---|\n| 硬いマウント（歯車あり・無負荷） | 252.6mA | 70.7dB |\n| ゴム入りマウント（歯車あり・無負荷） | 249.1mA | 66.9dB |\n\n電流はほとんど変わらず、騒音だけが約4dB下がった。これまで19個体を測ってきて、材質・Pressure Advance・軸間距離・圧力角補正のどれを変えても69.8〜70.9dBという1.1dBの帯から出てこなかったことを思えば、桁違いの変化だった。\n\n厚みを1mmに増やしても変化はなく、0.5mmで既に効果は飽和していた。軸受け周りにもゴムを追加してみたが、これも騒音はほとんど変わらなかった。振動の伝達経路は、前面・側面・底面に集中していたということになる。\n\n## 決定打：モーター単体でも同じだけ下がった\n\n歯車を完全に外し、モーター単体で同じ比較をしたところ、\n\n| 条件 | 電流 | 騒音 |\n|---|---|---|\n| モーター単体・硬いマウント | 249.7mA | 70.5dB |\n| モーター単体・ゴムマウント | 247.0mA | 66.8dB |\n\n歯車の有無に関係なく、ほぼ同じ約3.7dBの改善が出た。これで、4dBの改善は歯車まわりの何かではなく、**モーター缶の鉄板の振動と、それを無減衰のまま伝えていた硬いマウントの組み合わせ**そのものが原因だったことが、ほぼ確定した。\n\n## なぜここまで効いたのか\n\n車のダンパーに例えると分かりやすい。ダンパーのないバネだけの車体は、一度揺れるといつまでも揺れ続ける。ゴムを挟んだことは、単に「柔らかくした」のではなく、ゴムの内部摩擦（ヒステリシス）で振動エネルギーを熱として消費する、ダンパーの役割を持ち込んだことに近い。\n\nさらに、今回の構造（モーター缶という硬いもの、ゴム、PETGマウントという硬いもの、という3層）は、制振の分野で「拘束層減衰」と呼ばれる構成に近かった可能性がある。金属板が振動で微小に伸び縮みするとき、ゴムの片面だけが自由なら単純な伸縮で終わるが、両側を硬いもので挟まれていると、ゴムは伸縮ではなく**せん断変形**を強いられる。粘弾性材料はせん断変形の方がはるかに多くのエネルギーを熱に変換できるため、同じ厚みのゴムでも、拘束層になっているかどうかで効き方が大きく変わる。意図したわけではなかったが、モーター缶とマウントに挟まれたことで、この効率の良い減衰モードが働いていたと考えられる。\n\n回転数の反復ばらつきも、硬いマウントのstd57〜66から、ゴムマウントでstd11.3まで縮んだ。振動が減衰されずに伝わり続けている状態では、缶自体がミクロン単位で暴れ続け、それが回転子の運動にも影響していたが、ダンパーが効いたことで缶が静止に近い状態を保てるようになり、回転もろとも安定した、という説明で一貫している。\n\n耳で聞いても「音がまったく違う」というほどの変化だった。4dBという音圧レベルの差だけでは説明しきれない体感差は、単純な音量変化ではなく、硬い共鳴音・耳障りな高調波そのものが、ダンパーによって消えたためだと考えている。\n\n## この発見が意味すること\n\n前回「モーターの壁」と呼んだものは、正確には「モーターの壁」ではなく「無減衰マウントの壁」だった。歯車の測定・改善に、まだ意味がないと結論づけるのは早すぎたことになる。\n\n床が4dB下がったことで、これまで埋もれていた歯形由来の差が、初めて測定にかかる可能性が出てきた。奇偶差やA/B面の非対称を潰し込んだ「補正2」「補正3」と、20°/20°のまま作った素のPETG-aを、この新しいゴムマウントの上で測り比べる——これが、次に答えを出すべき、最後の宿題として残っている。\n\n## まとめ\n\n- モーター缶の広い面をマウントに直接固定する構造は、振動伝達の太い経路になり得る\n- 0.5mm厚のゴムを挟むだけで、騒音が約4dB、しかも安定して下がった。厚みを増やしても頭打りで、軸受け周りへの追加も効果なし\n- モーター単体でも同じだけ下がったことから、原因は歯車ではなくマウント側にあると確定できた\n- 「歯形は電流・騒音に効かない」という前回の結論は、実は「無減衰マウントという、もっと大きな雑音源に埋もれていただけ」だった可能性がある\n- 次の一手は、下がった床の上で、歯形の効果を再検証すること\n\n測定器を磨き続けていたら、測定器ではなく測定環境そのものに、一番大きな伸びしろが隠れていた、という結末になった。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T10:55:38+09:00","group":null,"id":"d7e64b7e825b1a06b54c","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"ラジコン","versions":[]},{"name":"制振","versions":[]},{"name":"モーター","versions":[]},{"name":"3Dプリンタ","versions":[]},{"name":"騒音","versions":[]}],"title":"モーターの壁だと思っていたら、マウントの壁だった話","updated_at":"2026-09-27T10:55:38+09:00","url":"https://qiita.com/himajisan/items/d7e64b7e825b1a06b54c","user":{"description":"AIを使ってラジコンを科学する爺さん","facebook_id":"","followees_count":1,"followers_count":3,"github_login_name":null,"id":"himajisan","items_count":84,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":4377818,"profile_image_url":"https://lh3.googleusercontent.com/a/ACg8ocJXDt7y_LrI4Of5hIoAkS8JKK12uPD1lQyXseqkhxWmOg_5Fw=s96-c","team_only":false,"twitter_screen_name":null,"website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-1:630\"\u003eAgentic Trading（LLM Tradingとも呼ばれたりしてるね）におけるLLMエージェントの判断を研究するため、\u003ca href=\"https://github.com/virattt/ai-hedge-fund\" rel=\"nofollow noopener\" target=\"_blank\"\u003evirattt/ai-hedge-fund\u003c/a\u003e を日本株で動かしてみました。これは投資家エージェントを含む教育・研究用の仮想ファンドのようで、OSSなので誰でもできるはず。ただ、米国株を対象に作られているので、米国株向けのデータ層をJ-Quants API V2の無料プランに差し替え、日本株の100株単位で動く\u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp\" rel=\"nofollow noopener\" target=\"_blank\"\u003eフォーク\u003c/a\u003eを作りました。\u003c/p\u003e\n\u003cp data-sourcepos=\"3:1-3:333\"\u003eこの記事では、実装で必要だった時点管理・TTM換算・分割対応を説明し、同じ財務データについて「企業を識別できる入力」と「匿名化した入力」を比べた予備実験を示します。実売買はしていません。投資助言や運用成績の検証ではありません。\u003c/p\u003e\n\u003cp data-sourcepos=\"5:1-5:40\"\u003e\u003ciframe id=\"qiita-embed-content__3af977f256add4a71c01137c23fa1593\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__3af977f256add4a71c01137c23fa1593\" data-content=\"https%3A%2F%2Fgithub.com%2Fvirattt%2Fai-hedge-fund\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003cp data-sourcepos=\"7:1-7:42\"\u003e\u003ciframe id=\"qiita-embed-content__248c9f0b7a0d68ad570161cac7b2ffc5\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__248c9f0b7a0d68ad570161cac7b2ffc5\" data-content=\"https%3A%2F%2Fgithub.com%2FTIshow%2Fai-hedge-fund-jp\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n\u003ctable data-sourcepos=\"10:1-16:107\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"10:1-10:43\"\u003e\n\u003cth data-sourcepos=\"10:2-10:9\"\u003e項目\u003c/th\u003e\n\u003cth data-sourcepos=\"10:11-10:18\"\u003e上流\u003c/th\u003e\n\u003cth data-sourcepos=\"10:20-10:42\"\u003e日本株フォーク\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"12:1-12:108\"\u003e\n\u003ctd data-sourcepos=\"12:2-12:18\"\u003e価格・財務\u003c/td\u003e\n\u003ctd data-sourcepos=\"12:20-12:39\"\u003eFinancial Datasets\u003c/td\u003e\n\u003ctd data-sourcepos=\"12:41-12:107\"\u003eJ-Quants V2の日足、決算短信サマリー、銘柄マスタ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"13:1-13:59\"\u003e\n\u003ctd data-sourcepos=\"13:2-13:15\"\u003e発注単位\u003c/td\u003e\n\u003ctd data-sourcepos=\"13:17-13:28\"\u003e原則1株\u003c/td\u003e\n\u003ctd data-sourcepos=\"13:30-13:58\"\u003e100株単位に切り捨て\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"14:1-14:58\"\u003e\n\u003ctd data-sourcepos=\"14:2-14:6\"\u003eTTM\u003c/td\u003e\n\u003ctd data-sourcepos=\"14:8-14:21\"\u003eAPIの指標\u003c/td\u003e\n\u003ctd data-sourcepos=\"14:23-14:57\"\u003e決算短信の累計から作成\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"15:1-15:107\"\u003e\n\u003ctd data-sourcepos=\"15:2-15:24\"\u003e開示情報の採用\u003c/td\u003e\n\u003ctd data-sourcepos=\"15:26-15:57\"\u003e判断日以前の提出情報\u003c/td\u003e\n\u003ctd data-sourcepos=\"15:59-15:106\"\u003e判断日より\u003cstrong\u003e前\u003c/strong\u003eに開示された短信\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"16:1-16:107\"\u003e\n\u003ctd data-sourcepos=\"16:2-16:15\"\u003e比較対象\u003c/td\u003e\n\u003ctd data-sourcepos=\"16:17-16:48\"\u003e指定したベンチマーク\u003c/td\u003e\n\u003ctd data-sourcepos=\"16:50-16:106\"\u003e1306の調整後価格（売買しない場合のみ）\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"18:1-18:241\"\u003eJ-Quants無料プランには過去2年、直近12週間の遅延、1分5回のAPI制限があります。財務諸表の詳細項目は含まれないため、粗利率・負債資本比率・流動比率は欠損値のまま扱います。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"20:1-20:45\"\u003e\n\u003cspan id=\"1-決算短信の累計からttmを作る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-%E6%B1%BA%E7%AE%97%E7%9F%AD%E4%BF%A1%E3%81%AE%E7%B4%AF%E8%A8%88%E3%81%8B%E3%82%89ttm%E3%82%92%E4%BD%9C%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. 決算短信の累計からTTMを作る\u003c/h2\u003e\n\u003cp data-sourcepos=\"21:1-21:155\"\u003e日本の四半期決算は期初からの累計です。前年同期と前期通期を使い、直近12か月のフローを次の式で作りました。\u003c/p\u003e\n\u003cp data-sourcepos=\"23:1-23:58\"\u003e\u003ccode\u003eTTM = 今期累計 + 前期通期 − 前年同期累計\u003c/code\u003e\u003c/p\u003e\n\u003cp data-sourcepos=\"25:1-26:383\"\u003e前期通期または前年同期がなければその期は省き、年換算しません。実装は \u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp/blob/main/hedge_fund/data/jquants_fins.py\" rel=\"nofollow noopener\" target=\"_blank\"\u003ejquants_fins.py\u003c/a\u003e の _ttm です。四半期の前年同期比は同じ累計期間同士を比較します。\u003cbr\u003e\nEPSだけは分割前後の値を単純に加減できません。株式分割の前後で「1株」の基準が異なるためです。TTMの純利益を、最新の短信に載る期中平均株式数 AvgSh で割る計算を優先します。AvgSh が取れない場合、実装はEPSの累計からの換算値に戻るので、分割がある銘柄では注意が必要です。\u003c/p\u003e\n\u003cp data-sourcepos=\"28:1-28:378\"\u003e時価総額、PER、PBRはその短信の株式数と開示日までの未調整終値から算出します。取得できる株価の範囲より古い短信では空欄です。事業分離などで前年を組み替えた会社は、当初開示の旧基準値と今期の新基準値が混ざる場合があります。無料サマリーだけでは補正できません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"30:1-30:54\"\u003e\n\u003cspan id=\"2-時点管理を日付単位で安全側に倒す\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-%E6%99%82%E7%82%B9%E7%AE%A1%E7%90%86%E3%82%92%E6%97%A5%E4%BB%98%E5%8D%98%E4%BD%8D%E3%81%A7%E5%AE%89%E5%85%A8%E5%81%B4%E3%81%AB%E5%80%92%E3%81%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. 時点管理を日付単位で安全側に倒す\u003c/h2\u003e\n\u003cp data-sourcepos=\"31:1-31:274\"\u003e終値での仮想約定に対して当日引け後の開示を使わないよう、短信の DiscDate が判断日より前のものだけを採用します。訂正短信は、その訂正の開示後に置き換えます。業績予想の修正は取り込みません。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"33:1-37:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# jquants_fins.py の判定を簡略化\n\u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003erow\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDiscDate\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003eas_of\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"39:1-39:210\"\u003eこれは同日午前中の開示も除外する保守的な規則です。また、資料の開示日を守っていても、LLMの学習済み知識まで過去時点へ戻せるわけではありません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"41:1-41:36\"\u003e\n\u003cspan id=\"3-分割と100株単位を扱う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-%E5%88%86%E5%89%B2%E3%81%A8100%E6%A0%AA%E5%8D%98%E4%BD%8D%E3%82%92%E6%89%B1%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. 分割と100株単位を扱う\u003c/h2\u003e\n\u003cp data-sourcepos=\"42:1-43:289\"\u003e仮想注文は100株単位に切り捨てます。売買銘柄の評価には未調整終値を使い、対応済みの株数が増える分割では権利落ち日に仮想持株数を増やします。AdjFactor=0.2 の1株→5株なら持株数を 1 / 0.2 = 5 倍します。調整係数の意味はJ-Quants公式の説明にもあります。\u003cbr\u003e\n一方、売買しない比較対象1306は分割で価格が飛ばないよう AdjC を使います。株式併合、100株単位が崩れる分割、momentum戦略での売買銘柄の分割にはまだ対応していません。1306の価格比較には分配金が含まれません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"45:1-45:48\"\u003e\n\u003cspan id=\"4-実データで判明した失敗を扱う\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E5%AE%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%A7%E5%88%A4%E6%98%8E%E3%81%97%E3%81%9F%E5%A4%B1%E6%95%97%E3%82%92%E6%89%B1%E3%81%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. 実データで判明した失敗を扱う\u003c/h2\u003e\n\u003ctable data-sourcepos=\"46:1-53:129\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"46:1-46:19\"\u003e\n\u003cth data-sourcepos=\"46:2-46:9\"\u003e現象\u003c/th\u003e\n\u003cth data-sourcepos=\"46:11-46:18\"\u003e対応\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"48:1-48:100\"\u003e\n\u003ctd data-sourcepos=\"48:2-48:45\"\u003e1306の2026年3月の分割で検証停止\u003c/td\u003e\n\u003ctd data-sourcepos=\"48:47-48:99\"\u003e売買しない比較対象だけ調整後価格へ\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"49:1-49:75\"\u003e\n\u003ctd data-sourcepos=\"49:2-49:39\"\u003e分割をまたいだEPSの桁違い\u003c/td\u003e\n\u003ctd data-sourcepos=\"49:41-49:74\"\u003eTTM純利益 ÷ \u003ccode\u003eAvgSh\u003c/code\u003eを優先\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"50:1-50:92\"\u003e\n\u003ctd data-sourcepos=\"50:2-50:38\"\u003e12秒おきの呼び出しでも429\u003c/td\u003e\n\u003ctd data-sourcepos=\"50:40-50:91\"\u003e13.2秒間隔に広げ、\u003ccode\u003eRetry-After\u003c/code\u003eで再試行\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"51:1-51:134\"\u003e\n\u003ctd data-sourcepos=\"51:2-51:52\"\u003e2年の契約範囲より前の価格取得で400\u003c/td\u003e\n\u003ctd data-sourcepos=\"51:54-51:133\"\u003eエラー内の利用可能範囲を読み取り、開始日を直して取得\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"52:1-52:155\"\u003e\n\u003ctd data-sourcepos=\"52:2-52:52\"\u003eAIが短いJSONの後に理由付きJSONを返す\u003c/td\u003e\n\u003ctd data-sourcepos=\"52:54-52:154\"\u003e判断と確信度が一致すれば理由付きの方を採用。不一致なら判断を控える\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"53:1-53:129\"\u003e\n\u003ctd data-sourcepos=\"53:2-53:72\"\u003e銘柄選定の基準日が早く、期が足りない会社が出る\u003c/td\u003e\n\u003ctd data-sourcepos=\"53:74-53:128\"\u003e結果を見る前に2月9日から2月16日に変更\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"55:1-55:213\"\u003e最後の変更はAPIの問題ではありません。2月9日時点では、期限内の2月10〜13日に発表した14社まで除外され、発表日の早い会社に寄った標本になっていました。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"57:1-57:42\"\u003e\n\u003cspan id=\"企業の識別情報を伏せる比較\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E4%BC%81%E6%A5%AD%E3%81%AE%E8%AD%98%E5%88%A5%E6%83%85%E5%A0%B1%E3%82%92%E4%BC%8F%E3%81%9B%E3%82%8B%E6%AF%94%E8%BC%83\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e企業の識別情報を伏せる比較\u003c/h2\u003e\n\u003cp data-sourcepos=\"58:1-58:269\"\u003e通常の入力は、会社名の文字列をAIに渡していません。ただし証券コード・業種・時価総額・決算期末日・開示日を含み、会社を特定できてしまいます。以下では便宜上「識別情報あり」と呼びます。\u003c/p\u003e\n\u003cp data-sourcepos=\"60:1-61:500\"\u003eLLMの学習の性質上、バックテストで期間を指定しても、すでにそれより先の未来を学習していると、その情報を踏まえた上でバックテストを行ってしまう恐れがあります。そのため企業を匿名化し、日付は latest / -3m のような相対表記に変更して行ってみました。PER、ROE、利益率、EPS、BPSなどの数字は残します。2つの設定の差は anonymize: true の有無です（\u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan-30.yaml\" rel=\"nofollow noopener\" target=\"_blank\"\u003ejapan-30.yaml\u003c/a\u003e、\u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan-30-anon.yaml\" rel=\"nofollow noopener\" target=\"_blank\"\u003ejapan-30-anon.yaml\u003c/a\u003e）。同じスナップショットのハッシュで判断を組にする\u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan_compare.py\" rel=\"nofollow noopener\" target=\"_blank\"\u003e集計コード\u003c/a\u003eを使いました。\u003cbr\u003e\n2026年2月16日時点のプライム市場から金融業を除き、TOPIX Core30・Large70・Mid400から各10社を種固定で選びました。選定に必要なTTMの期を4つ以上確保しています。週次判断は2月20日〜6月26日の19回。未変化のスナップショットは重複計上せず、Buffett役61組、Graham役61組、合計122組を比較しました。事前仮説と基準日の変更は手順書に記録しています。AIモデルはClaude Opus 5.5です。\u003c/p\u003e\n\u003ctable data-sourcepos=\"63:1-66:44\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"63:1-63:126\"\u003e\n\u003cth data-sourcepos=\"63:2-63:12\"\u003e判断役\u003c/th\u003e\n\u003cth style=\"text-align: right\" data-sourcepos=\"63:14-63:56\"\u003e中立：識別情報あり → 匿名化\u003c/th\u003e\n\u003cth style=\"text-align: right\" data-sourcepos=\"63:58-63:71\"\u003e判断一致\u003c/th\u003e\n\u003cth style=\"text-align: right\" data-sourcepos=\"63:73-63:125\"\u003e中立が「ありだけ」／「匿名化だけ」\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"65:1-65:46\"\u003e\n\u003ctd data-sourcepos=\"65:2-65:13\"\u003eBuffett役\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"65:15-65:27\"\u003e79% → 59%\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"65:29-65:33\"\u003e67%\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"65:35-65:45\"\u003e16／4組\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"66:1-66:44\"\u003e\n\u003ctd data-sourcepos=\"66:2-66:12\"\u003eGraham役\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"66:14-66:26\"\u003e36% → 41%\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"66:28-66:32\"\u003e92%\u003c/td\u003e\n\u003ctd style=\"text-align: right\" data-sourcepos=\"66:34-66:43\"\u003e1／4組\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp data-sourcepos=\"68:1-70:282\"\u003eBuffett役では、13組が「識別情報ありで中立、匿名化で弱気」。Graham役ではほぼ同じ判断でした。Core30、Large70、Mid400におけるBuffett役の中立差は24、20、15ポイントで、会社への認知度の自己評価と会社ごとの差の順位相関は−0.05です。自己評価はMid400でも平均7.2点で、幅が狭い指標でした。\u003cbr\u003e\n同じ会社が何度も登場するので、122組を独立した試行として有意差を主張できません。会社単位ではBuffett役で識別情報ありの中立が多い会社11社、少ない会社4社。符号検定の両側p値は約0.12です。会社が分かると判断が変わる可能性は示しましたが、記憶が原因だと特定したわけではありません。1株あたりの値からの再識別や、匿名化によって業種を伏せた影響も残ります。\u003cbr\u003e\n同期間の仮想収益率は識別情報あり−6.8%、匿名化−3.7%、1306の価格が+5.1%。約4か月半で、空売りの費用、手数料、税、配当、実際の約定可能性などを含めていません。LLMの優劣や戦略の収益性は評価しません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"72:1-72:15\"\u003e\n\u003cspan id=\"再現手順\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%86%8D%E7%8F%BE%E6%89%8B%E9%A0%86\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e再現手順\u003c/h2\u003e\n\u003cp data-sourcepos=\"73:1-73:290\"\u003ePython 3.11以降、uv、J-Quants V2のAPIキーを用意します。AIを使う比較にはAnthropicのAPIキーも必要です。キーはリポジトリ直下の .env に JQUANTS_API_KEY=... と ANTHROPIC_API_KEY=... を各1行で設定し、Gitの管理対象外にしてください。\u003c/p\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"terminal\" data-sourcepos=\"75:1-81:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"go\"\u003egit clone https://github.com/TIshow/ai-hedge-fund-jp.git\ncd ai-hedge-fund-jp\nuv venv .venv\n\u003c/span\u003e\u003cspan class=\"gp\"\u003euv pip install --python .venv/bin/python -e . 'pytest\u0026gt;\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e7.4,\u0026lt;8\u003cspan class=\"s1\"\u003e'\n\u003c/span\u003e\u003cspan class=\"gp\"\u003e.venv/bin/python japan_pilot_smoke.py \u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e/dev/null\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"83:1-83:650\"\u003e最後のコマンドは架空データの動作確認で、APIキー不要です。実データの30社比較は、\u003ca href=\"https://github.com/TIshow/ai-hedge-fund-jp/blob/main/JAPAN_PILOT.md#30%E9%8A%98%E6%9F%84%E3%81%AE%E6%AF%94%E8%BC%83%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86\" rel=\"nofollow noopener\" target=\"_blank\"\u003eJAPAN_PILOT.md\u003c/a\u003eの「30銘柄の比較の実行手順」にある japan_universe.py → japan_knowledge_probe.py → 2つのバックテスト → japan_compare.py の順に実行します。J-Quantsの呼び出しがある作業は無料プランの制限上、順番に実行します。保存済み結果から画像を作る japan_media.py はAPIを呼びません。\u003c/p\u003e\n\u003cp data-sourcepos=\"85:1-85:303\"\u003eコードと手順を再現できても、無料プランのデータ欠損、当初開示値の組み替え、LLMの学習済み知識、標本の小ささという制約は残ります。次は小型株への拡張と、モデルがまだ知りえない期間での前向きな比較を試します。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"87:1-87:27\"\u003e\n\u003cspan id=\"お疲れ様でした\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%8A%E7%96%B2%E3%82%8C%E6%A7%98%E3%81%A7%E3%81%97%E3%81%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eお疲れ様でした。\u003c/h2\u003e\n\u003cp data-sourcepos=\"88:1-88:149\"\u003eわからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。\u003c/p\u003e\n","body":"Agentic Trading（LLM Tradingとも呼ばれたりしてるね）におけるLLMエージェントの判断を研究するため、[virattt/ai-hedge-fund](https://github.com/virattt/ai-hedge-fund) を日本株で動かしてみました。これは投資家エージェントを含む教育・研究用の仮想ファンドのようで、OSSなので誰でもできるはず。ただ、米国株を対象に作られているので、米国株向けのデータ層をJ-Quants API V2の無料プランに差し替え、日本株の100株単位で動く[フォーク](https://github.com/TIshow/ai-hedge-fund-jp)を作りました。\n\nこの記事では、実装で必要だった時点管理・TTM換算・分割対応を説明し、同じ財務データについて「企業を識別できる入力」と「匿名化した入力」を比べた予備実験を示します。実売買はしていません。投資助言や運用成績の検証ではありません。\n\nhttps://github.com/virattt/ai-hedge-fund\n\nhttps://github.com/TIshow/ai-hedge-fund-jp\n\n\n| 項目 | 上流 | 日本株フォーク |\n|---|---|---|\n| 価格・財務 | Financial Datasets | J-Quants V2の日足、決算短信サマリー、銘柄マスタ |\n| 発注単位 | 原則1株 | 100株単位に切り捨て |\n| TTM | APIの指標 | 決算短信の累計から作成 |\n| 開示情報の採用 | 判断日以前の提出情報 | 判断日より**前**に開示された短信 |\n| 比較対象 | 指定したベンチマーク | 1306の調整後価格（売買しない場合のみ） |\n\nJ-Quants無料プランには過去2年、直近12週間の遅延、1分5回のAPI制限があります。財務諸表の詳細項目は含まれないため、粗利率・負債資本比率・流動比率は欠損値のまま扱います。\n\n## 1. 決算短信の累計からTTMを作る\n日本の四半期決算は期初からの累計です。前年同期と前期通期を使い、直近12か月のフローを次の式で作りました。\n\n`TTM = 今期累計 + 前期通期 − 前年同期累計`\n\n前期通期または前年同期がなければその期は省き、年換算しません。実装は [jquants_fins.py](https://github.com/TIshow/ai-hedge-fund-jp/blob/main/hedge_fund/data/jquants_fins.py) の _ttm です。四半期の前年同期比は同じ累計期間同士を比較します。\nEPSだけは分割前後の値を単純に加減できません。株式分割の前後で「1株」の基準が異なるためです。TTMの純利益を、最新の短信に載る期中平均株式数 AvgSh で割る計算を優先します。AvgSh が取れない場合、実装はEPSの累計からの換算値に戻るので、分割がある銘柄では注意が必要です。\n\n時価総額、PER、PBRはその短信の株式数と開示日までの未調整終値から算出します。取得できる株価の範囲より古い短信では空欄です。事業分離などで前年を組み替えた会社は、当初開示の旧基準値と今期の新基準値が混ざる場合があります。無料サマリーだけでは補正できません。\n\n## 2. 時点管理を日付単位で安全側に倒す\n終値での仮想約定に対して当日引け後の開示を使わないよう、短信の DiscDate が判断日より前のものだけを採用します。訂正短信は、その訂正の開示後に置き換えます。業績予想の修正は取り込みません。\n\n```python\n# jquants_fins.py の判定を簡略化\nif row[\"DiscDate\"] \u003e= as_of:\n    continue\n```\n\nこれは同日午前中の開示も除外する保守的な規則です。また、資料の開示日を守っていても、LLMの学習済み知識まで過去時点へ戻せるわけではありません。\n\n## 3. 分割と100株単位を扱う\n仮想注文は100株単位に切り捨てます。売買銘柄の評価には未調整終値を使い、対応済みの株数が増える分割では権利落ち日に仮想持株数を増やします。AdjFactor=0.2 の1株→5株なら持株数を 1 / 0.2 = 5 倍します。調整係数の意味はJ-Quants公式の説明にもあります。\n一方、売買しない比較対象1306は分割で価格が飛ばないよう AdjC を使います。株式併合、100株単位が崩れる分割、momentum戦略での売買銘柄の分割にはまだ対応していません。1306の価格比較には分配金が含まれません。\n\n## 4. 実データで判明した失敗を扱う\n| 現象 | 対応 |\n|---|---|\n| 1306の2026年3月の分割で検証停止 | 売買しない比較対象だけ調整後価格へ |\n| 分割をまたいだEPSの桁違い | TTM純利益 ÷ `AvgSh`を優先 |\n| 12秒おきの呼び出しでも429 | 13.2秒間隔に広げ、`Retry-After`で再試行 |\n| 2年の契約範囲より前の価格取得で400 | エラー内の利用可能範囲を読み取り、開始日を直して取得 |\n| AIが短いJSONの後に理由付きJSONを返す | 判断と確信度が一致すれば理由付きの方を採用。不一致なら判断を控える |\n| 銘柄選定の基準日が早く、期が足りない会社が出る | 結果を見る前に2月9日から2月16日に変更 |\n\n最後の変更はAPIの問題ではありません。2月9日時点では、期限内の2月10〜13日に発表した14社まで除外され、発表日の早い会社に寄った標本になっていました。\n\n## 企業の識別情報を伏せる比較\n通常の入力は、会社名の文字列をAIに渡していません。ただし証券コード・業種・時価総額・決算期末日・開示日を含み、会社を特定できてしまいます。以下では便宜上「識別情報あり」と呼びます。\n\nLLMの学習の性質上、バックテストで期間を指定しても、すでにそれより先の未来を学習していると、その情報を踏まえた上でバックテストを行ってしまう恐れがあります。そのため企業を匿名化し、日付は latest / -3m のような相対表記に変更して行ってみました。PER、ROE、利益率、EPS、BPSなどの数字は残します。2つの設定の差は anonymize: true の有無です（[japan-30.yaml](https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan-30.yaml)、[japan-30-anon.yaml](https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan-30-anon.yaml)）。同じスナップショットのハッシュで判断を組にする[集計コード](https://github.com/TIshow/ai-hedge-fund-jp/blob/main/japan_compare.py)を使いました。\n2026年2月16日時点のプライム市場から金融業を除き、TOPIX Core30・Large70・Mid400から各10社を種固定で選びました。選定に必要なTTMの期を4つ以上確保しています。週次判断は2月20日〜6月26日の19回。未変化のスナップショットは重複計上せず、Buffett役61組、Graham役61組、合計122組を比較しました。事前仮説と基準日の変更は手順書に記録しています。AIモデルはClaude Opus 5.5です。\n\n| 判断役 | 中立：識別情報あり → 匿名化 | 判断一致 | 中立が「ありだけ」／「匿名化だけ」 |\n|---|---:|---:|---:|\n| Buffett役 | 79% → 59% | 67% | 16／4組 |\n| Graham役 | 36% → 41% | 92% | 1／4組 |\n\nBuffett役では、13組が「識別情報ありで中立、匿名化で弱気」。Graham役ではほぼ同じ判断でした。Core30、Large70、Mid400におけるBuffett役の中立差は24、20、15ポイントで、会社への認知度の自己評価と会社ごとの差の順位相関は−0.05です。自己評価はMid400でも平均7.2点で、幅が狭い指標でした。\n同じ会社が何度も登場するので、122組を独立した試行として有意差を主張できません。会社単位ではBuffett役で識別情報ありの中立が多い会社11社、少ない会社4社。符号検定の両側p値は約0.12です。会社が分かると判断が変わる可能性は示しましたが、記憶が原因だと特定したわけではありません。1株あたりの値からの再識別や、匿名化によって業種を伏せた影響も残ります。\n同期間の仮想収益率は識別情報あり−6.8%、匿名化−3.7%、1306の価格が+5.1%。約4か月半で、空売りの費用、手数料、税、配当、実際の約定可能性などを含めていません。LLMの優劣や戦略の収益性は評価しません。\n\n## 再現手順\nPython 3.11以降、uv、J-Quants V2のAPIキーを用意します。AIを使う比較にはAnthropicのAPIキーも必要です。キーはリポジトリ直下の .env に JQUANTS_API_KEY=... と ANTHROPIC_API_KEY=... を各1行で設定し、Gitの管理対象外にしてください。\n\n```terminal\ngit clone https://github.com/TIshow/ai-hedge-fund-jp.git\ncd ai-hedge-fund-jp\nuv venv .venv\nuv pip install --python .venv/bin/python -e . 'pytest\u003e=7.4,\u003c8'\n.venv/bin/python japan_pilot_smoke.py \u003e /dev/null\n```\n\n最後のコマンドは架空データの動作確認で、APIキー不要です。実データの30社比較は、[JAPAN_PILOT.md](https://github.com/TIshow/ai-hedge-fund-jp/blob/main/JAPAN_PILOT.md#30%E9%8A%98%E6%9F%84%E3%81%AE%E6%AF%94%E8%BC%83%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86)の「30銘柄の比較の実行手順」にある japan_universe.py → japan_knowledge_probe.py → 2つのバックテスト → japan_compare.py の順に実行します。J-Quantsの呼び出しがある作業は無料プランの制限上、順番に実行します。保存済み結果から画像を作る japan_media.py はAPIを呼びません。\n\nコードと手順を再現できても、無料プランのデータ欠損、当初開示値の組み替え、LLMの学習済み知識、標本の小ささという制約は残ります。次は小型株への拡張と、モデルがまだ知りえない期間での前向きな比較を試します。\n\n## お疲れ様でした。\nわからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T10:55:36+09:00","group":null,"id":"00fc1106fd2d27b1f395","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"Python","versions":[]},{"name":"jquants","versions":[]},{"name":"LLM","versions":[]},{"name":"株価","versions":[]},{"name":"ファイナンス","versions":[]}],"title":"【Agentic Trading】日本株AIヘッジファンドの実装と匿名化実験","updated_at":"2026-09-27T10:55:36+09:00","url":"https://qiita.com/TIshow/items/00fc1106fd2d27b1f395","user":{"description":"I'm a scinentist failure.","facebook_id":"hiromasa.saito.754/","followees_count":1,"followers_count":4,"github_login_name":"TIshow","id":"TIshow","items_count":25,"linkedin_id":"hiromasa-saito-3877a5145/","location":"","name":"大将","organization":"","permanent_id":256394,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/256394/e3c160e5841f2cb80d5203a1557e9659b97715e0/large.png?1648826026","team_only":false,"twitter_screen_name":"T_I_SHOW","website_url":"https://tishow.github.io/mysite/"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003cp data-sourcepos=\"1:1-3:90\"\u003eトークン化預金とは、\u003cbr\u003e\nブロックチェーン技術を活用して銀行口座の預金のトークン化をおこなうこと。\u003cbr\u003e\nより詳細にどういったものか、どういう論点があるかを整理する。\u003c/p\u003e\n\u003ch1 data-sourcepos=\"5:1-5:38\"\u003e\n\u003cspan id=\"ステーブルコインとの違い\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%82%B9%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%B3%E3%82%A4%E3%83%B3%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eステーブルコインとの違い\u003c/h1\u003e\n\u003ctable data-sourcepos=\"7:1-19:4\"\u003e\n\u003cthead\u003e\n\u003ctr data-sourcepos=\"7:1-7:59\"\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"7:2-7:7\"\u003e項目\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"7:9-7:34\"\u003eステーブルコイン\u003c/th\u003e\n\u003cth style=\"text-align: center\" data-sourcepos=\"7:36-7:58\"\u003eトークン化預金\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr data-sourcepos=\"9:1-9:36\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"9:2-9:10\"\u003e発行体\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"9:12-9:22\"\u003e事業者\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"9:24-9:35\"\u003e商業銀行\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"10:1-10:28\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"10:2-10:7\"\u003e金利\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"10:9-10:20\"\u003eつかない\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"10:22-10:27\"\u003eつく\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"11:1-11:100\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"11:2-11:13\"\u003e銀行口座\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"11:15-11:56\"\u003e不要（海外送金対応しやすい）\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"11:58-11:99\"\u003e必要（送金先にも口座が必要）\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"12:1-12:34\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"12:2-12:7\"\u003e移転\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"12:9-12:20\"\u003e即時可能\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"12:22-12:33\"\u003e即時可能\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"13:1-13:79\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"13:2-13:28\"\u003eプログラマビリティ\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"13:30-13:53\"\u003eデータ書込み可能\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"13:55-13:78\"\u003eデータ書込み可能\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"14:1-14:94\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"14:2-14:25\"\u003eトレーサビリティ\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"14:27-14:59\"\u003e追跡可能・対改ざん性高\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"14:61-14:93\"\u003e追跡可能・対改ざん性高\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"15:1-15:4\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"15:2-15:1\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"15:3-15:2\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"15:4-15:3\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"16:1-16:4\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"16:2-16:1\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"16:3-16:2\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"16:4-16:3\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"17:1-17:4\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"17:2-17:1\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"17:3-17:2\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"17:4-17:3\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"18:1-18:4\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"18:2-18:1\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"18:3-18:2\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"18:4-18:3\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr data-sourcepos=\"19:1-19:4\"\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"19:2-19:1\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"19:3-19:2\"\u003e\u003c/td\u003e\n\u003ctd style=\"text-align: center\" data-sourcepos=\"19:4-19:3\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch1 data-sourcepos=\"21:1-21:8\"\u003e\n\u003cspan id=\"論点\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%AB%96%E7%82%B9\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e論点\u003c/h1\u003e\n\u003cp data-sourcepos=\"22:1-24:70\"\u003e・既存の銀行システムとの親和性があるか\u003cbr\u003e\n・取引コストを下げられる要因の人件費削減に業務手順が合わせられるか\u003cbr\u003e\n・日本円のデジタル化（CBDC）の普及に対応できるか\u003c/p\u003e\n\u003ch1 data-sourcepos=\"26:1-26:14\"\u003e\n\u003cspan id=\"参考資料\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e参考資料\u003c/h1\u003e\n\u003cp data-sourcepos=\"28:1-28:51\"\u003e\u003ciframe id=\"qiita-embed-content__e648529fdc6677a4574e90171c6b318a\" src=\"https://qiita.com/embed-contents/link-card#qiita-embed-content__e648529fdc6677a4574e90171c6b318a\" data-content=\"https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D9FrxTAqOQ0A%26t%3D1219s\" frameborder=\"0\" scrolling=\"no\" loading=\"lazy\" style=\"width:100%;\" height=\"29\"\u003e\n\u003c/iframe\u003e\n\u003c/p\u003e\n","body":"トークン化預金とは、\nブロックチェーン技術を活用して銀行口座の預金のトークン化をおこなうこと。\nより詳細にどういったものか、どういう論点があるかを整理する。\n\n# ステーブルコインとの違い\n\n|項目| ステーブルコイン | トークン化預金 |\n|:-:|:-:|:-:|\n|発行体| 事業者 |商業銀行|\n|金利|つかない|つく|\n|銀行口座|不要（海外送金対応しやすい）|必要（送金先にも口座が必要）|\n|移転|即時可能|即時可能|\n|プログラマビリティ|データ書込み可能|データ書込み可能|\n|トレーサビリティ|追跡可能・対改ざん性高|追跡可能・対改ざん性高|\n||||\n||||\n||||\n||||\n||||\n\n# 論点\n・既存の銀行システムとの親和性があるか\n・取引コストを下げられる要因の人件費削減に業務手順が合わせられるか\n・日本円のデジタル化（CBDC）の普及に対応できるか\n\n# 参考資料\n\nhttps://www.youtube.com/watch?v=9FrxTAqOQ0A\u0026t=1219s\n\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T10:53:48+09:00","group":null,"id":"7d28515b19ebde296747","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"ブロックチェーン","versions":[]},{"name":"トークン","versions":[]},{"name":"トークン化","versions":[]},{"name":"トークンエコノミー","versions":[]}],"title":"トークン化預金","updated_at":"2026-09-27T10:53:48+09:00","url":"https://qiita.com/masaSystem/items/7d28515b19ebde296747","user":{"description":"~エンジニアとして一人前になることが目標~\r\n社会人4年目/都内在住/SIer勤務/\r\n大学時代にプログラミングのゼミに入り、\r\n教授に「これからやる2年間は社会人の3週間で抜かれます」と言われ、\r\n2週間で抜かれました。","facebook_id":"","followees_count":6,"followers_count":6,"github_login_name":null,"id":"masaSystem","items_count":7,"linkedin_id":"","location":"","name":"Masashi .T","organization":"","permanent_id":3835526,"profile_image_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/3835526/180a9b665a1aa6260b30500422a80b6dffb4cdf8/x_large.png?1735798912","team_only":false,"twitter_screen_name":"masaSys_tem","website_url":""},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null},{"rendered_body":"\u003ch2 data-sourcepos=\"1:1-1:9\"\u003e\n\u003cspan id=\"背景\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E8%83%8C%E6%99%AF\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e背景\u003c/h2\u003e\n\u003cp data-sourcepos=\"3:1-3:267\"\u003eCDK PipelinesにCodeDeployを組み合わせて、Spring Boot WARのホットデプロイまで自動化するハンズオンをやりました。インフラのCI/CDだけだった前段のハンズオンから、アプリのCI/CDまで踏み込んだ内容です。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"5:1-5:83\"\u003e\n\u003cspan id=\"1-appspecymlが3つのライフサイクルフックを順番に実行する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#1-appspecyml%E3%81%8C3%E3%81%A4%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF%E3%82%92%E9%A0%86%E7%95%AA%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e1. \u003ccode\u003eappspec.yml\u003c/code\u003eが3つのライフサイクルフックを順番に実行する\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"yaml\" data-sourcepos=\"7:1-21:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"na\"\u003eversion\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0.0\u003c/span\u003e\n\u003cspan class=\"na\"\u003eos\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003elinux\u003c/span\u003e\n\u003cspan class=\"na\"\u003efiles\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003esource\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003ewebapp.war\u003c/span\u003e\n    \u003cspan class=\"na\"\u003edestination\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e/opt/tomcat/webapps/\u003c/span\u003e\n    \u003cspan class=\"na\"\u003eoverwrite\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003cspan class=\"na\"\u003ehooks\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eApplicationStop\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003elocation\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003escripts/stop_tomcat.sh\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# Tomcat停止\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eBeforeInstall\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003elocation\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003escripts/cleanup.sh\u003c/span\u003e         \u003cspan class=\"c1\"\u003e# 古いWAR・展開ディレクトリを削除\u003c/span\u003e\n  \u003cspan class=\"na\"\u003eAfterInstall\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e\n    \u003cspan class=\"pi\"\u003e-\u003c/span\u003e \u003cspan class=\"na\"\u003elocation\u003c/span\u003e\u003cspan class=\"pi\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003escripts/start_tomcat.sh\u003c/span\u003e    \u003cspan class=\"c1\"\u003e# Tomcat起動\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"23:1-23:322\"\u003eCodeDeployは\u003ccode\u003eappspec.yml\u003c/code\u003eに定義された順序でフックスクリプトを実行し、Tomcatを完全に再起動することなくWARファイルを差し替えます。「停止→クリーンアップ→配置→起動」という定型的なホットデプロイの手順がYAML1枚に集約されています。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"25:1-25:106\"\u003e\n\u003cspan id=\"2-env_from_cfn_outputsでスタック出力をパイプラインの環境変数として受け取る\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#2-env_from_cfn_outputs%E3%81%A7%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E5%87%BA%E5%8A%9B%E3%82%92%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%A8%E3%81%97%E3%81%A6%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e2. \u003ccode\u003eenv_from_cfn_outputs\u003c/code\u003eでスタック出力をパイプラインの環境変数として受け取る\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"27:1-42:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"c1\"\u003e# webapp_stack.py側: CfnOutputでリソース名を公開\n\u003c/span\u003e\u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edeploy_app_name_output\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eCfnOutput\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eself\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDeployApplicationName\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003eweb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edeploy_application\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eapplication_name\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\n\u003cspan class=\"c1\"\u003e# pipeline_stack.py側: 環境変数として受け取る\n\u003c/span\u003e\u003cspan class=\"n\"\u003ebuild_and_deploy\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nc\"\u003eCodeBuildStep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n    \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eBuildAndDeployApp\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"n\"\u003eenv_from_cfn_outputs\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n        \u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eDEPLOY_APP_NAME\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edeploy_app_name_output\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n    \u003cspan class=\"n\"\u003ecommands\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"s\"\u003eaws deploy create-deployment --application-name $DEPLOY_APP_NAME ...\u003c/span\u003e\u003cspan class=\"sh\"\u003e\"\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\n\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"44:1-44:362\"\u003eCodeDeployのアプリケーション名をパイプラインのコードにハードコードせず、CloudFormationが実際に作成したリソース名を動的に環境変数として受け取ります。これにより、リソース名の変更がCloudFormation側で起きても、パイプラインのコードを修正する必要がありません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"46:1-46:108\"\u003e\n\u003cspan id=\"3-postステップでインフラ確定後にアプリをデプロイする順序を保証する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#3-post%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%E3%81%A7%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E7%A2%BA%E5%AE%9A%E5%BE%8C%E3%81%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B%E9%A0%86%E5%BA%8F%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e3. \u003ccode\u003epost\u003c/code\u003eステップで「インフラ確定後にアプリをデプロイする」順序を保証する\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"python\" data-sourcepos=\"48:1-50:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003e\u003cspan class=\"n\"\u003epipeline\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eadd_stage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epost\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ebuild_and_deploy\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"52:1-52:292\"\u003e\u003ccode\u003epost\u003c/code\u003eにステップを渡すことで、ステージ（InfraDeploy）の完了後に続けて\u003ccode\u003eBuildAndDeployApp\u003c/code\u003eが実行される順序が保証されます。EC2やCodeDeployのリソースがまだ存在しない状態でデプロイを試みる、という順序ミスを防げます。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"54:1-54:92\"\u003e\n\u003cspan id=\"4-パイプラインは全ファイルjavaソース含むのpushを前提にする\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#4-%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%AF%E5%85%A8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%ABjava%E3%82%BD%E3%83%BC%E3%82%B9%E5%90%AB%E3%82%80%E3%81%AEpush%E3%82%92%E5%89%8D%E6%8F%90%E3%81%AB%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e4. パイプラインは全ファイル（Javaソース含む）のpushを前提にする\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"56:1-59:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ecdk deploy の前に必ずプロジェクト全体を push する必要がある\n理由: パイプラインがGitHubから取得したコードで cdk synth と mvn package を両方実行するため\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"61:1-61:378\"\u003eインフラのみをCI/CD化していた前段のハンズオンとは異なり、\u003ccode\u003eapp/\u003c/code\u003e配下のJavaソース・\u003ccode\u003eappspec.yml\u003c/code\u003e・\u003ccode\u003escripts/\u003c/code\u003eがリポジトリに存在しないと、パイプラインの\u003ccode\u003eBuildAndDeployApp\u003c/code\u003eステップ自体が失敗します。ローカルの作業ディレクトリだけに置いたファイルはパイプラインからは見えません。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"63:1-63:88\"\u003e\n\u003cspan id=\"5-urlパスにwebappが必要な理由もwarのデプロイ形式に起因する\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#5-url%E3%83%91%E3%82%B9%E3%81%ABwebapp%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1%E3%82%82war%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E5%BD%A2%E5%BC%8F%E3%81%AB%E8%B5%B7%E5%9B%A0%E3%81%99%E3%82%8B\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e5. URLパスに\u003ccode\u003e/webapp/\u003c/code\u003eが必要な理由もWARのデプロイ形式に起因する\u003c/h2\u003e\n\u003cdiv class=\"code-frame\" data-lang=\"text\" data-sourcepos=\"65:1-68:3\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003ccode\u003ehttp://\u0026lt;ALBEndpoint\u0026gt;/            ← Tomcatのデフォルト画面\nhttp://\u0026lt;ALBEndpoint\u0026gt;/webapp/     ← デプロイしたSpring Bootアプリ\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\u003cp data-sourcepos=\"70:1-70:313\"\u003e\u003ccode\u003ewebapp.war\u003c/code\u003eという名前でデプロイされるため、アプリは\u003ccode\u003e/webapp/\u003c/code\u003eというコンテキストパス以下に配置されます。ルートパスのままではTomcatの初期画面が表示され続けるため、アプリ本体を見るには明示的にパスを付ける必要があります。\u003c/p\u003e\n\u003ch2 data-sourcepos=\"72:1-72:12\"\u003e\n\u003cspan id=\"まとめ\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E3%81%BE%E3%81%A8%E3%82%81\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eまとめ\u003c/h2\u003e\n\u003cp data-sourcepos=\"74:1-74:391\"\u003e\u003ccode\u003eappspec.yml\u003c/code\u003eによるホットデプロイの定型化と、\u003ccode\u003eenv_from_cfn_outputs\u003c/code\u003eによるリソース名の動的受け渡しが、このハンズオンでのCodeDeploy連携のポイントでした。インフラとアプリの両方が\u003ccode\u003egit push\u003c/code\u003eだけで反映される状態を実現することで、AWSコンソールでの手動変更が入り込む余地をなくす設計です。\u003c/p\u003e\n\u003cp data-sourcepos=\"76:1-76:140\"\u003e→ \u003ca href=\"https://caymezon.com/aws-handson-cdk-webapp-pipeline/\" rel=\"nofollow noopener\" target=\"_blank\"\u003eCDK Pipelines で Spring Boot を CI/CD 自動デプロイする（ブログ）\u003c/a\u003e\u003c/p\u003e\n","body":"## 背景\n\nCDK PipelinesにCodeDeployを組み合わせて、Spring Boot WARのホットデプロイまで自動化するハンズオンをやりました。インフラのCI/CDだけだった前段のハンズオンから、アプリのCI/CDまで踏み込んだ内容です。\n\n## 1. `appspec.yml`が3つのライフサイクルフックを順番に実行する\n\n```yaml\nversion: 0.0\nos: linux\nfiles:\n  - source: webapp.war\n    destination: /opt/tomcat/webapps/\n    overwrite: true\nhooks:\n  ApplicationStop:\n    - location: scripts/stop_tomcat.sh    # Tomcat停止\n  BeforeInstall:\n    - location: scripts/cleanup.sh         # 古いWAR・展開ディレクトリを削除\n  AfterInstall:\n    - location: scripts/start_tomcat.sh    # Tomcat起動\n```\n\nCodeDeployは`appspec.yml`に定義された順序でフックスクリプトを実行し、Tomcatを完全に再起動することなくWARファイルを差し替えます。「停止→クリーンアップ→配置→起動」という定型的なホットデプロイの手順がYAML1枚に集約されています。\n\n## 2. `env_from_cfn_outputs`でスタック出力をパイプラインの環境変数として受け取る\n\n```python\n# webapp_stack.py側: CfnOutputでリソース名を公開\nself.deploy_app_name_output = CfnOutput(\n    self, \"DeployApplicationName\",\n    value=web.deploy_application.application_name,\n)\n\n# pipeline_stack.py側: 環境変数として受け取る\nbuild_and_deploy = CodeBuildStep(\n    \"BuildAndDeployApp\",\n    env_from_cfn_outputs={\n        \"DEPLOY_APP_NAME\": stage.deploy_app_name_output,\n    },\n    commands=[\"aws deploy create-deployment --application-name $DEPLOY_APP_NAME ...\"],\n)\n```\n\nCodeDeployのアプリケーション名をパイプラインのコードにハードコードせず、CloudFormationが実際に作成したリソース名を動的に環境変数として受け取ります。これにより、リソース名の変更がCloudFormation側で起きても、パイプラインのコードを修正する必要がありません。\n\n## 3. `post`ステップで「インフラ確定後にアプリをデプロイする」順序を保証する\n\n```python\npipeline.add_stage(stage, post=[build_and_deploy])\n```\n\n`post`にステップを渡すことで、ステージ（InfraDeploy）の完了後に続けて`BuildAndDeployApp`が実行される順序が保証されます。EC2やCodeDeployのリソースがまだ存在しない状態でデプロイを試みる、という順序ミスを防げます。\n\n## 4. パイプラインは全ファイル（Javaソース含む）のpushを前提にする\n\n```\ncdk deploy の前に必ずプロジェクト全体を push する必要がある\n理由: パイプラインがGitHubから取得したコードで cdk synth と mvn package を両方実行するため\n```\n\nインフラのみをCI/CD化していた前段のハンズオンとは異なり、`app/`配下のJavaソース・`appspec.yml`・`scripts/`がリポジトリに存在しないと、パイプラインの`BuildAndDeployApp`ステップ自体が失敗します。ローカルの作業ディレクトリだけに置いたファイルはパイプラインからは見えません。\n\n## 5. URLパスに`/webapp/`が必要な理由もWARのデプロイ形式に起因する\n\n```\nhttp://\u003cALBEndpoint\u003e/            ← Tomcatのデフォルト画面\nhttp://\u003cALBEndpoint\u003e/webapp/     ← デプロイしたSpring Bootアプリ\n```\n\n`webapp.war`という名前でデプロイされるため、アプリは`/webapp/`というコンテキストパス以下に配置されます。ルートパスのままではTomcatの初期画面が表示され続けるため、アプリ本体を見るには明示的にパスを付ける必要があります。\n\n## まとめ\n\n`appspec.yml`によるホットデプロイの定型化と、`env_from_cfn_outputs`によるリソース名の動的受け渡しが、このハンズオンでのCodeDeploy連携のポイントでした。インフラとアプリの両方が`git push`だけで反映される状態を実現することで、AWSコンソールでの手動変更が入り込む余地をなくす設計です。\n\n→ [CDK Pipelines で Spring Boot を CI/CD 自動デプロイする（ブログ）](https://caymezon.com/aws-handson-cdk-webapp-pipeline/)\n","coediting":false,"comments_count":0,"created_at":"2026-09-27T10:48:37+09:00","group":null,"id":"394efd27ce5a966c9aef","likes_count":0,"private":false,"reactions_count":0,"stocks_count":0,"tags":[{"name":"AWS","versions":[]},{"name":"CDK","versions":[]},{"name":"CodeDeploy","versions":[]},{"name":"CICD","versions":[]},{"name":"SpringBoot","versions":[]}],"title":"`env_from_cfn_outputs`でCloudFormationの出力値をパイプラインの環境変数に渡す","updated_at":"2026-09-27T10:48:37+09:00","url":"https://qiita.com/caymezon/items/394efd27ce5a966c9aef","user":{"description":"CayTech-Lab（caymezon.com）運営。\r\nAWS CDK / CloudFormationを使ったハンズオン形式の学習コンテンツと、\r\nClaude Code・GitHub Copilot等のAI開発ツールの実務活用パターンを発信しています。","facebook_id":"","followees_count":1,"followers_count":3,"github_login_name":"caymezon","id":"caymezon","items_count":80,"linkedin_id":"","location":"","name":"","organization":"","permanent_id":4481097,"profile_image_url":"https://avatars.githubusercontent.com/u/277156739?v=4","team_only":false,"twitter_screen_name":null,"website_url":"https://caymezon.com"},"page_views_count":null,"team_membership":null,"organization_url_name":null,"slide":false,"posting_campaign_uuid":null}]