0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ヘッドレス実行で SIGTERM 中断に耐えるパイプラインの設計:本番自動化で「途中で殺されても落ちない」実装

0
Posted at

本番の自動化パイプラインで claude -p をヘッドレス実行していると、デプロイのローリング更新・CI のジョブタイムアウト・Kubernetes の Pod 退避といった「外から来る停止」に必ず当たります。問題は停止そのものではなく、停止されたときにセッションがどの状態で止まり、どこから安全に再開できるかが設計されていないことです。本稿は 2026-10-23 時点の headless ドキュメントで確認できる挙動だけを根拠に、SIGTERM 中断から再開までを 1 本の通し実装で組み立てます。

SIGTERM が来たときヘッドレスの Claude Code が実際にすること

SIGTERM で停止された claude -p プロセスは exit code 143 で終了します。

Claude Code exits with code 143

この 143(128 + 15)は、オーケストレータ側で「正常終了」と「外部停止」を切り分ける最初の手がかりになります。終了時、Claude Code は実行中のすべての Bash コマンドのプロセスツリー全体を終了させ、SessionEnd フック以外のフックを一切実行しません。新しいツール呼び出しもモデルリクエストも発行しません。パーミッションプロンプトの待機中に SIGTERM を送ると、そのプロンプトは未回答のまま残ります。つまり「止める」という操作は、進行中の作業を綺麗に畳むのではなく、その場で打ち切る操作です。

「中断ターンは記録されない」を前提に完了判定を設計する

ここが自動化で最も事故る点です。SIGTERM を受けたとき、Claude Code は進行中のターンを未完了のまま残し、そのターンの結果を記録しません。

leaves the turn that was in progress unfinished and records no result for it

結果として、セッションのターン履歴は「完了判定の真実」になりません。再開時の履歴を見て「このターンは終わったはず」と判断する設計は、中断の瞬間に破綻します。設計上の指針は 1 つです。完了シグナルは Claude の内部記録に預けず、タスク自身が最後のステップで書く外部マーカー(ファイル)で表現する。マーカーの有無だけを完了条件にすれば、143 で止められようが途中で死のうが、再開ロジックが迷いません。

ゼロから通す実装:run.sh と prompt.txt

空のディレクトリに 2 ファイルだけ置きます。前提は Linux、bash、Claude Code CLI がインストール済み(2026-10-23 時点)、認証は環境変数で済んでいることとします。

prompt.txt(タスク本体。再実行しても同じ結果になる=冪等に書くのが要点):

あなたはリリースノート生成タスクを実行します。次を順に行ってください。

1. カレントディレクトリに REPORT.md を作成し、本日の作業サマリを Markdown で書く。
   既に REPORT.md があれば内容を上書きして最新化する(再実行しても結果が同じになるようにする)。
2. REPORT.md が完成したあと、最後のステップとして空ファイル REPORT.done を作成する。
   REPORT.done はパイプラインの完了シグナルなので、REPORT.md の完成後にのみ作成すること。

run.sh(ドライバ。143 と未完了を検知し、同一セッションを再開する):

#!/usr/bin/env bash
# run.sh — SIGTERM 中断に耐えるヘッドレス・パイプライン・ドライバ
set -uo pipefail   # -e は使わない。終了コードを手で分岐するため。

WORKDIR="$(cd "$(dirname "$0")" && pwd)"
cd "$WORKDIR"

STATE_DIR="$WORKDIR/.pipeline"; mkdir -p "$STATE_DIR"
SESSION_FILE="$STATE_DIR/session_id"
STREAM="$STATE_DIR/stream.jsonl"
DONE_MARKER="$WORKDIR/REPORT.done"
MAX_ATTEMPTS="${MAX_ATTEMPTS:-5}"

# 中断ターンを再開時にそのまま継続させる
export CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1

extract_session_id () {
  grep -o '"session_id":"[^"]*"' "$STREAM" 2>/dev/null | head -n1 | cut -d'"' -f4
}

attempt=0
while : ; do
  attempt=$((attempt + 1))
  if [ "$attempt" -gt "$MAX_ATTEMPTS" ]; then
    echo "[run.sh] attempts exhausted" >&2; exit 1
  fi

  args=( -p --output-format stream-json --verbose --permission-mode acceptEdits )
  if [ -s "$SESSION_FILE" ]; then
    args+=( --resume "$(cat "$SESSION_FILE")" )
  fi

  : > "$STREAM"
  claude "${args[@]}" < prompt.txt > "$STREAM" 2> "$STATE_DIR/stderr.log"
  code=$?

  sid="$(extract_session_id)"
  [ -n "$sid" ] && printf '%s' "$sid" > "$SESSION_FILE"

  # 完了判定は Claude のターン記録ではなく外部マーカーで行う
  if [ -f "$DONE_MARKER" ]; then exit 0; fi

  # マーカーが無ければ 143 でも exit 0 でも再開して続ける
done

実行:

mkdir pipe && cd pipe
# 上の2ファイルを置く
chmod +x run.sh
./run.sh

動作確認:終了コードとマーカーで判定する

ヘッドレスの成否は「端末に何が出たか」ではなく、終了コードと副作用(ファイル)で判定します。推測の出力文言に依存しないため、本番でもそのまま監視条件に使えます。

./run.sh; echo "run.sh exit = $?"
test -f REPORT.done && echo "MARKER OK"
test -s REPORT.md   && echo "REPORT OK"

成功とみなせる条件は次の 3 つが同時に満たされること、と定義します。run.sh の終了コードが 0、REPORT.done が存在する、REPORT.md が空でない。失敗の見分け方は、run.sh が 1 で終わり(attempts 枯渇)、REPORT.done が存在しないことです。中断からの再開を確かめるには、別シェルから実行中の claude に SIGTERM を送ります。

pkill -TERM -f 'claude -p'   # 子の claude だけを止める

この場合、その回の claude は 143 で終わり、run.sh のループが同一セッションを再開します。最終的に REPORT.done が作られていれば、中断をまたいで完了できたことになります。

再開とバックグラウンド・予算・MCP の落とし穴

再開時に中断ターンを捨てて新しい入力から始めるのではなく、中断ターンをそのまま継続させたい場合は環境変数を設定します。

continue the interrupted turn on resume instead, set CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1

長時間ジョブでもう 1 つ効く上限が、バックグラウンド待機です。デフォルトでは連続アイドル 10 分で部分結果が破棄されます。

stops whatever is still running after 10 minutes of continuous idle waiting

この上限は環境変数 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS で変更でき、0 にすると上限なしで待機します。--bare モードではバックグラウンドタスクは一切実行されず、タイムアウトに達したコマンドはバックグラウンドへ移らず停止します。--max-budget-usd の上限に達した場合は、残りのバックグラウンドワークを待たずに停止します。--continue は終了済みのバックグラウンドセッションを開けますが、まだ実行中のものは開けません。

入出力面の既知の上限も監視条件に入れます。パイプ経由の stdin は 10MB が上限で、超過すると明確なエラーと非ゼロステータスで終了します。stdin が読めない場合は stderr に警告を出してコマンドラインのプロンプトで継続します。--mcp-config 併用時は、デフォルト 30 秒の MCP_TIMEOUT までサーバー起動を待ってから最初のターンを実行します。ストリーム消費が遅いと、キュー済み出力のドレインを最大 30 秒待ってから終了します。リトライ可能な API 失敗時は、リトライ前に system/api_retry イベントを送出します。apiKeyHelper の資格情報が 401/403 で拒否された場合は、最初の 2 回は無音でリトライし、3 回目以降からイベントを出します。パーミッションモードを何も設定しないと Manual モードで開始するため、無人実行では明示設定が必須です。--permission-prompts none は v2.1.259 以降でのみ使えます。ワーキングディレクトリが途中で消えてもセッションは継続しますが、シェルコマンドはディレクトリ復元まで失敗します。

References

[1] https://code.claude.com/docs/en/headless

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?