人の承認を挟みながら Claude Code のエージェントを何本もヘッドレスで走らせる技術検証で、PreToolUse フックの defer で止めたセッションを、承認のあとに --resume で再開する作りを試しました。このとき、-p に渡す再開のプロンプトにタスクを書き直せば二重になり、何も渡さなければエージェントが止まらなくなりました。プロセスが強制終了されたあとの再開では、エージェントが自分のした最後の作業を覚えていませんでした。

結論

  • --resume は、1つのプロセスで2ターン動きました。1ターン目で保留のツールを実行して元の会話の続きを自動で進め、2ターン目で -p に渡したプロンプトを処理します。プロンプトは自動の続きを置き換えず、そのあとに足されます。
  • そのため、再開のプロンプトにはタスクを書き直さず、承認の中身(承認・条件・差し戻しの理由)を書きます。result は2回流れるので、最後のものを採ります。
  • 区切りの無い再開では、エージェントが止まらずに動き続けました。「保留中の操作を実行し、結果を報告して停止する」のように終わる所をはっきり書き、使わせたくない道具は --disallowedTools で外します。
  • kill -9 で落ちたセッションも --resume で戻せましたが、最後のツール呼び出しとその結果は文脈に入っていませんでした。再開の指示に「実際の状態を確かめてから続ける」を入れます。

試した環境

  • Mac、Claude Code 2.1.170
  • 起こし方: claude -p --output-format stream-json --verbose --include-hook-events --settings <フックの設定> --setting-sources '' --session-id <uuid> --allowedTools Bash。再開には --resume <session-id> を付けました。

--resume は1プロセスで2ターン動く

承認のあと --resume で起こすと、stream-json に init から result までが2回流れました。1回目の result から2回目の始まりまでは 7ms でした。

1回の --resume で、init から result までが2回流れる

1ターン目は、こちらのプロンプトとは関係なく、保留していたツールを実行して元の会話を続けます。-p に渡したプロンプトが処理されるのは2ターン目です。つまり、再開のプロンプトは「続き」を指示するものではなく、続きのあとに足される1通です。

ここで元のタスクを書き直して渡すと、1ターン目ですでに済んだことを、もう一度頼むことになります。実測では、エージェントは「その操作はすでに実行済みです」と返しました。そこで、再開のプロンプトには承認の中身(承認か、条件付きか、差し戻しの理由は何か)を書き、元のタスクは書き直さないことにしました。

起こす側の読み取りにも気をつけます。1回目の result を受け取った時点では、プロセスはまだ終わっていません。私たちは、result を受けるたびに上書きして、最後のものを採るようにしました。

小さな仕事を終えたのに、引き出しや本を次々に開けて回り続けるロボット

区切りの無い再開は止まらなかった

再開のときにプロンプトを渡さなかった回では、エージェントが止まらなくなりました。元のタスクは echo を1回打つだけで、それはもう終わっていました。それなのに、再開したエージェントは次のことを始めました。

  • メモリのファイル、リポジトリ、git log、ドキュメントを読む
  • 承認の関門が見ている状態ファイルを自分で rejected に書き換え、deny の経路を試す
  • 結果の文書を書こうとする(Write を許可していなかったので、そこで止まった)

セッションは3分以上動きました。

そこで再開には、どこで止まるかがはっきりした指示を渡すことにしました。往復を確かめたときに渡したのは、次の1文です。

保留中の操作を実行し、結果を1行で報告して停止してください。

あわせて、再開のときの --allowedTools を最小にしました。ただし --allowedTools は境界ではなく、一覧に無い道具を使われることがあったので、使わせたくない道具は --disallowedTools で名前を挙げて外します。再開がすぐに終わるとは限らないので、同期で待つ前提でも組まないことにしました。

強制終了したセッションの再開では、最後の1手が抜ける

停電で落ちたエージェントの作業を、続きから戻すか捨てるかを選べるようにしたくて、強制終了からの再開も測りました。--session-id を指定して起こし、ツールの途中(sleep 30 の最中)で kill -9 を送ってから、--resume で戻しました。

  • 再開はできました。session_id も同じで、会話と計画は戻っていました。
  • ただし、最後のツール呼び出しとその結果は、文脈に入っていませんでした。tool_result が書かれる前にプロセスが落ちたためです。
  • 最初の手順で書いたファイルは実際に残っていたのに、再開したエージェントは「何も実行していません」と答えました。

再開したエージェントは、実際より1手遅れていると思い込みます。そこで、強制終了からの再開では、指示に「実際の状態を確かめてから続ける」を入れることにしました。

再開のプロンプトに入れたこと

まとめると、再開のプロンプトには次のことだけを書きました。元のタスクは書き直しません。

  • 承認の中身(承認・条件・差し戻しの理由)
  • どこで止まるか(保留中の操作を実行し、結果を報告して停止する)
  • 強制終了からの再開なら、実際の状態を確かめてから続けること

確かめていないこと

  • 申請1件あたりの再開の費用(キャッシュが切れたときの入力トークン)。止まらなくなった回が混ざって、測れませんでした。
  • 承認待ちが cleanupPeriodDays(既定 30 日)を超えるとセッションが消えること、再開のときに MCP が繋がっていないとエラーで終わること。どちらも公式に書かれていることで、私たちは測っていません。
  • 2.1.170 より後の版でも同じかどうか。

おわりに

--resume に渡すプロンプトは「続きを頼む」ためのものではなく、自動で進む続きのあとに足される1通でした。再開のプロンプトは、承認の中身と止まる所だけを書いた短いものにする、という形に落ち着きました。

※ この記事に書いた Claude Code の振る舞いは、執筆時点で私たちが測ったものです。いまは変わっている可能性があります。