AIエージェントに本番を触らせない分担:コードはAI、マージと本番と秘密は人
Claude Codeと一緒に、データベースの変更から本番のデプロイまでを進めた記録。AIはコードと手元の検証、人はマージと本番の実行と秘密の入力、という分担にして、実際に起きた失敗と、それでも事故にならなかった理由をまとめる。
AIには、コードを書き、手元で確かめ、公開されたページを外から確かめるところまでを任せます。マージ、本番での実行、秘密の入力は人がやります。この線を引いておくと、AIの書いたスクリプトに不具合があっても、本番で壊れる前に止まりました。
この記事の前提
あるサービスに、新しい種類の商品(約7万件)を足す作業を、Claude Codeと一緒に進めました。データベースの表を足し、毎日の取り込みを直し、サイトの検索に混ぜ、7万件分のベクトル(意味で探すための数値)を作る、という一連の作業です。リポジトリは3つ、本番はVMのPostgreSQLと、Cloudflare Workersのサイトです。
この記事は、そのときの分担と、実際に起きたことの記録です。設定の細かい書き方は、権限の記事と、.envの実験の記事にあります。
分担:どこまでをAIに任せるか
| 作業 | 誰が | 理由 |
|---|---|---|
| コードを書く、テストを足す | AI | 速く、書いた理由も説明できる |
| 手元のDB(コンテナ)で動かす、測る | AI | 壊しても、作り直せばよい |
| コミット | AI | 手元だけの変更で、いつでも戻せる |
| push、マージリクエストを作る | AI(人が「はい」と言ってから) | 外に出る。CIが動き、ほかの人に見える |
| マージ | 人 | mainに入ると、CIが本番にデプロイする |
| 本番のサーバーでの実行(デプロイのスクリプトなど) | 人 | 戻しにくい。本番のデータに触れる |
| パスワードやトークンの入力 | 人 | AIの会話に、秘密を一度も入れない |
| 公開されたページを外から確かめる | AI | 読むだけで、誰でもできる操作 |
線の引き方は単純です。手元で完結して、戻せるものはAI。外に出るもの、戻しにくいもの、秘密は人。
本番のサーバーは、人が実行する
デプロイのスクリプトは、VMにSSHで入り、ファイルを置き、DBの変更を当て、一度動かしてから、タイマーを有効にします。このスクリプトを、AIは実行しません。
実際に、AIが本番のDBの中身を「読むだけ」のつもりでSSHのコマンドを出したときも、auto モードの判定(操作を別のモデルが確認する仕組み)が止めました。本番のサーバーに入る操作は、読むだけでも、人が実行する扱いです。
その代わり、AIは人が貼った出力を読んで次に進みます。
- 人:デプロイのスクリプトを実行し、出力をそのまま貼る
- AI:出力を読み、「新しい表は今回作られた(既にあるという通知が出ていない)」「3つの分類の取り込みが完了した」と確かめる
- AI:確かめられないことは、人に頼むコマンドとして渡す(どのコマンドを、何が出れば成功か、まで書く)
スクリプトは、何度実行しても同じ結果になるように書きます(CREATE TABLE IF NOT EXISTS、二度目は何も変えない UPDATE など)。人が途中で止めても、もう一度実行すればよい形にしておくと、人に頼みやすくなります。
秘密は、会話に入れない
本番のDBに7万件分のベクトルを書き込むには、DBのパスワードが要りました。AIには、パスワードを渡しません。人が、自分のターミナルで、画面に出さずに入力します。
read -s "PGPASSWORD?DB password: " && export PGPASSWORD && echo
PG_URL='postgres://app@127.0.0.1:55440/app' node scripts/embed-products.mjs --other
unset PGPASSWORDread -sは、入力を画面に出さない(zshの書き方。bashではread -rs -p "..." PGPASSWORD)- 接続文字列にはパスワードを書かず、
PGPASSWORDで渡す。Nodeのpgも、psqlも、この環境変数を読む。記号を含むパスワードでも、URLのエスケープが要らない - 終わったら
unsetする。シェルの履歴にも、AIの会話にも残らない
AIの側は、値ではなく、あるかどうかだけを確かめます。デプロイのスクリプトは、設定ファイルに必要な変数が2つとも入っているかを、値を表示せずに調べてから始まります。
[ "$(grep -c -E '^(API_ID|AFFILIATE_ID)=.' "$ENV_FILE")" = 2 ] || { echo "API_ID and AFFILIATE_ID not both in $ENV_FILE"; exit 1; }トークンも同じです。人が手元で入力し、パイプでSSHの先の、root だけが読めるファイルに流し込みます。保管場所は、パスワード管理ツール(1Password)です。
実際に起きた失敗
1. 許可の前にpushした
一度、AIが、CIの設定を足したコミットを、許可を待たずにpushしました。人が「何をしていますか?」と止め、AIが経緯を説明して謝りました。害はありませんでしたが、pushは外に出る操作です。
それ以来、「pushとデプロイは、はっきり指示があるまでしない」を、AIの記憶(プロジェクトのメモ)に書いて、毎回の会話で読ませています。一度言われたことを、次の会話でも守らせる仕組みが要ります。
2. AIの書いたスクリプトが、本番で最初に落ちた
ベクトルを作るスクリプトに、新しい表を対象にする --other という指定を、AIが足しました。人が本番に向けて実行すると、すぐに落ちました。
70568 titles to embed
error: syntax error at or near "0.0059967041015625"原因は、表の名前を入れた変数 vectors を、ループの中で、計算したベクトルを入れる同じ名前の変数が隠していたことです。INSERT INTO ${vectors} に、表の名前ではなく、数値の列が入っていました。
事故にならなかったのは、次の理由です。
- 最初の書き込みで落ちたので、何も書き込まれていなかった
- スクリプトは「まだベクトルのない行だけ」を対象にするので、直してから、そのまま再実行できた(7万件を、約10分で完了)
- 実行したのは人で、出力をそのまま貼ったので、AIがすぐに原因を特定できた
反省点は、新しい指定を足したら、手元のDBで少しだけ(--limit 100 など)流してから、本番に渡すことです。手元のテストは、検索のSQLにはありましたが、このスクリプトにはありませんでした。
外から確かめるのは、AIに任せてよい
デプロイの後の確認は、公開されているページを読むだけなので、AIに任せました。
- サイトマップに、新しい商品が入ったか(2026年分で36,407件)
- 新しい商品のページが200で開き、「似た作品」の欄が出るか
- 既存のページの表示時間が、変わっていないか
robots.txtと、ページのnoindexの有無
ただし、外部への問い合わせが、そのまま費用や数字に影響する操作(広告のリンクを踏む、課金のあるAIを呼ぶ)は、確かめる側でも避けます。チャットの検索は、呼ぶたびに費用がかかるので、試さずに「試していない」と報告させました。
環境の制約も、AIに伝えておく
AIは、言われていない制約を知りません。このときは、次のことを、プロジェクトのメモに書いていました。
- 内蔵ディスクがいっぱいなので、一時ファイルは外付けのSSDに置く(
TMPDIR=/Volumes/.../.tmpを付けて、開発サーバーやテストを動かす) - 自分が起動していないプロセスは止めない(ほかの作業の開発サーバーが、同じマシンで動いている)
- 本番のサーバーへの変更は、人が実行する
- 広告のリンクに、自分からアクセスしない(クリックの数字が汚れる)
一つずつは小さな決まりですが、書いておかないと、AIは毎回、同じところでつまずきます。
チェックリスト
- push、マージ、本番での実行を、人の明示の指示まで待たせているか
- 秘密を、AIの会話にも、シェルの履歴にも入れていないか(
read -s、環境変数、パスワード管理ツール) - AIには、秘密の値ではなく、「あるかどうか」だけを確かめさせているか
- 本番で動かすスクリプトは、何度実行しても同じ結果になるか。途中で止めても再開できるか
- 新しい指定やスクリプトを、手元で少量流してから、本番に渡しているか
- 一度決めたルールを、次の会話でも読まれる場所(メモ、CLAUDE.md)に書いたか
- 確かめていないことを、AIが「確かめていない」と報告しているか