実装もユニットテストもエージェントが回してくれる。なのに Postgres を使う Playwright の E2E だけは、「DB を用意できないので手元で実行してください」のような一言とともに人の手へ戻ってくる。
筆者の場合、理由は自分の指示書にあった。「E2E(Playwright + DB コンテナ)は人間か CI が回す」。安全のための一行が、エージェントには一番筋の通った断り文句になっていた。
では、一行を消せば済むのか? 実際は、sandbox の中で DB を立てる道が 2 本ともふさがっている。
sandbox の中で DB が立たない理由
筆者の macOS の Claude Code(Pro $20/月、Max $100/月〜)の sandbox で試した。
- docker:
permission denied while trying to connect to the docker API。daemon へのソケットを塞がれる - initdb:
could not create shared memory segment: Operation not permitted。共有メモリの確保を拒否される
initdb は止まる前に、max_connections を 25、shared_buffers を 400kB まで自分で値切っていた。値切ってなお断られる。Linux は未確認だ。
sandbox の公式ドキュメントは docker を excludedCommands で外に出す例を載せる。ただし compose のように作業ツリーのファイルを読むコマンドは、Claude がそのファイルを書いて外で動かせる、とも注意している。そして daemon は rootless でなければ root 権限で動き、Linux ならホストの / もコンテナにマウントできる。macOS でも、既定で共有されるホームなどはコンテナから読み書きできる。
外に出せば DB は立つ。エージェントが書いた compose ファイルも、root 相当で立つ。比べるべきは、外の DB を「どの口から受け取るか」だ。
4 つの方法を比べる
| 方法 | エージェントのコマンドを sandbox の外で動かすか | 開ける穴 | 後始末 | 結果まで |
|---|---|---|---|---|
| docker compose を外で動かす | 動かす | compose ファイルを root 相当で動かす | down を忘れると残る | 速い |
| Testcontainers | テストごと外に出すか、docker のソケットを開ける | 同じ docker のソケット | Ryuk が回収 | 速い |
| CI に回す | 動かさない | 無し | ランナーごと捨てる | 遅い |
| 使い捨て DB を払い出してもらう | 動かさない | 払い出し役への口 1 本 | 削除と期限切れの二段 | 速い |
Testcontainers はポートを空き番号から選ぶので並列で動かしてもぶつからず、テストが落ちても Ryuk が片付ける。ただ Docker API が使えるランタイムが前提で、片付け上手でも入る鍵は docker と同じ一本だ。
払い出し型は、sandbox の外の払い出し役に頼んで接続 URL だけを受け取る。筆者の払い出し役は別の VM に digest 固定の postgres コンテナを立て、期限は 90 分、同時に 4 つまで。superuser でも暴れられるのは VM の中だけだ。pnpm + Prisma + Playwright の repo で、smoke のテスト 10 件が 42 秒ですべて通った(筆者の観察)。
払い出し型でエージェントに確かめさせること
| 確かめさせること | 外すと起きること |
|---|---|
DB を自分で立てず、docker を excludedCommands に足さない | 失敗後に、sandbox の外での実行を人に頼んでくる |
| 外部 DB の URL を読む場所(環境変数など)を、テスト手順書で先に確かめる | 自前で compose を立てる作りなら、渡した URL は無視される |
| 既存の開発サーバーに繋がっていないか | 設定例の reuseExistingServer: !process.env.CI は手元で既存のサーバーを使い回す。そのサーバーは普段の DB に繋がったままなので、渡した URL を使わずに E2E が通る |
| 後始末は終了時の削除と期限切れの二段 | 削除が走らない終わり方がある |
| DB が取れなければ CI に回し、理由を残す | E2E を回したか飛ばしたか分からない |
postinstall・prepare で落ちたら HUSKY=0 やキャッシュ先の変更で入れ直す | DB の手前で止まる。npm install が落ちる原因も参照 |
見落としやすいのは 2 行目だ。「DATABASE_URL を渡して」と頼めば、エージェントは渡す。受け取る側が聞いていなくても、渡したことに変わりはない。
後始末を trap に任せきらない
削除は trap '...' EXIT に書きたくなる。ファイルに 1 行書く trap を仕込み、終わらせ方ごとに試した。
| 終わり方 | bash 3.2 / 5.3 | zsh 5.9 |
|---|---|---|
正常終了・exit 1 | 走る | 走る |
| TERM | 走る | 走らない |
| KILL | 走らない | 走らない |
タスクを止めたとき、前景で動かしたサブエージェントが終わったとき、無人実行の時間制限に達したときにコマンドは止められる。そのときどのシグナルが届くかは、こちらでは選べない。
trap に書いたから安心、のはずが、zsh は TERM で黙って帰り、KILL ではどれも動かない。だから払い出し役の側に期限を持たせ、消し忘れても 90 分で消えるようにする。
ホスト型なら、Neon のブランチが --expires-at で最長 30 日の期限を付けられる。$0 の Free プランでも 1 プロジェクト 10 ブランチまで使える。API キーを渡すことになる点は、APIキーを直接渡さない構成で補いたい。
別案: 全部 CI に任せればいいのでは
穴を開けたくないなら、それで正しい。ただ結果が返るまでが長い。筆者の環境では、エージェントが 90 秒おきに CI の check を見に行き、最長 25 分待つ作りにしていた。E2E が落ちるたびに、この待ちが一周する。いまは払い出しが基本で、CI は払い出しが取れないときの受け皿にしている。
まとめ
決めるのは DB の立て方ではなく、どの口から受け取るかだった。接続 URL だけを受け取り、URL を読む場所・既存サーバー・二段の後始末・取れないときの戻り先をエージェントに確かめさせる。
人が呼び戻される経路の洗い出しは会話記録から詰まりを分ける記事にまとめた。エージェントに任せる開発の相談はAI 開発支援で受けている。



