TL;DR
- AIエージェントや自動化ジョブが固まると、次の処理が始まらず放置されます。GNU
timeoutで上限時間を付けます。 - 落とし穴は2つです。通常のタイムアウトは終了コード
124、TERMを無視する相手は--kill-afterがないと制限時間で終わらず、KILLされると137になります。 - bash 5.2.37 / GNU coreutils timeout 9.7 で、正常終了・タイムアウト・TERM無視・通常失敗の4ケースを確認しました。
構成
まず素の timeout で止まることを確認する
制限を1秒、対象を10秒の sleep にします。約1秒で戻り、終了コードは 124 でした。
$ timeout 1 sleep 10; echo rc=$?
rc=124
$ timeout 5 sleep 0.2; echo rc=$?
rc=0
timeout は、タイムアウトしたときにコマンドへまず TERM を送ります。ここまでは単純です。問題は、相手が TERM を受けても終わらない場合です。
落とし穴1: TERMを無視されると制限時間で終わらない
TERM を無視してから sleep に置き換わるプロセスでは、--kill-after がないと、sleep 自体が終わるまで timeout は戻りません。下記は対象が3秒で自然に終わった例ですが、10秒や無限ループならその分だけ待たされます。
$ timeout 1 bash -c 'trap "" TERM; exec sleep 3'; echo rc=$?
rc=124
対策は、TERM のあとに猶予を付けて KILL することです。制限1秒+猶予5秒の指定では、約6秒で戻りました。
$ timeout --kill-after=5s 1 bash -c 'trap "" TERM; exec sleep 10'; echo rc=$?
rc=137
-
124: TERM で終了したタイムアウト -
137: KILL で終了したタイムアウト(128 + 9) - 合計の上限は「制限時間+猶予」になります。猶予を長くすると、その分だけ停止も遅くなります。後片付けが必要なコマンドでは猶予を短くしすぎず、不要なら短めにします。
落とし穴2: 終了コードの読み方を混ぜない
--preserve-status を付けると、タイムアウトでも相手の状態が返ります。TERM で終わった場合は 124 ではなく 143(128 + 15)になりました。タイムアウト判定を 124 前提で書くなら付けません。
$ timeout --preserve-status 1 sleep 10; echo rc=$?
rc=143
もう1点、124 はコマンド自身が出すこともあります。次も 124 ですが、タイムアウトではありません。
$ timeout 5 sh -c 'exit 124'; echo rc=$?
rc=124
そのため、この判定は「制限付きで呼んだコマンド一般」ではなく、対象コマンドが 124 を自ら返さない範囲で使います。厳密に分けたい場合は、対象側の終了コードの範囲をあらかじめ決めておく必要があります。
最小ラッパー
呼び出し側が ok / timeout / failed を読み分けられるようにした最小形です。元のコマンドの終了コードはそのまま返します。
#!/usr/bin/env bash
# 使い方: ./run_timeout.sh <秒> <command...>
# 例: ./run_timeout.sh 30 claude -p "..."
set -uo pipefail
limit="${1:?usage: run_timeout.sh SECONDS command...}"
shift
timeout --kill-after=5s "${limit}s" "$@"
rc=$?
case "$rc" in
0) echo "result=ok rc=0" ;;
124|137) echo "result=timeout rc=$rc (limit=${limit}s)" >&2 ;;
*) echo "result=failed rc=$rc" >&2 ;;
esac
exit "$rc"
検証ログ
同じラッパーを4ケースで実行しました。KILL のケースでは、シェルから Killed の通知が出ることがあります。
$ ./run_timeout.sh 2 sleep 0.2; echo shell_rc=$?
result=ok rc=0
shell_rc=0
$ ./run_timeout.sh 1 sleep 10; echo shell_rc=$?
result=timeout rc=124 (limit=1s)
shell_rc=124
$ ./run_timeout.sh 1 bash -c 'trap "" TERM; exec sleep 10'; echo shell_rc=$?
result=timeout rc=137 (limit=1s)
shell_rc=137
$ ./run_timeout.sh 2 sh -c 'exit 3'; echo shell_rc=$?
result=failed rc=3
shell_rc=3
実測時間は、2番目が約1.0秒、3番目が約6.0秒(制限1秒+猶予5秒)でした。
使い方の要点
- 制限時間はタスクごとに付けます。一律に長くすると、固まったときの発見が遅れます。
-
124 | 137のときは後続処理を続けず、まず対象が中途半端な出力やファイルを残していないかを確認します。 - 猶予5秒は固定値の例です。終了処理に時間のかかるコマンドでは、必要な後片付けの時間に合わせて調整します。
まとめ
-
timeoutの通常タイムアウトは124、KILL まで進むと137です。 - TERM を無視する相手には
--kill-afterが必要で、上限は制限時間と猶予の合計です。 -
--preserve-statusの有無でコードが変わる点と、対象自身の124とは区別できない点に注意します。
参考
- GNU Coreutils
timeoutのマニュアル: https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html