🤖無人のClaude Codeが同じタスクを6日で17回!残量−8003でも走った暴走を引き算で止めるまで

bokuwalily
bokuwalily

@bokuwalily ・ 106本

サムネイル

「Claude Code環境」シリーズの第8弾です。会話ログを長期記憶に変える話で無人ジョブを紹介しました。今回はその親玉の話です。ユーザー入力ゼロで、Claude Codeに自分の環境を改善させ続ける「autopilot」を作りました。

自律ループと聞くとかっこいいのですが、無人でLLMを回すと怖いことが2つ起きます。ひとつはプラン枠を使い切ってしまうこと。もうひとつは、同じタスクを延々と繰り返すことです。私はどちらも実際に踏みました。

履歴を見返して、同じタスクを6日間で17回やっていたと分かった時は声が出ました! 見た目はちゃんと毎回動いているので、異常に見えないのがいちばん怖いところでした。この記事は、その2つを機械で止めるまでの記録です。

毎朝1 Phaseだけ進める仕組み

launchdから毎朝5:00に起動し、claude -p のヘッドレスで動かします。やることは「改善タスクを1つ拾い、1 Phaseだけ進めて終わる」の繰り返しです。

  • 触っていいのは ~/.claude/ 配下だけ(個人プロジェクトやVaultの個人情報には触らせません)
  • モードは dry(プロンプト生成のみ)、apply、once の3つ
  • 走らせる前にコスト枠を確認し、危なければその場でskip

1回で大改造はさせません。無人のまま、小さく賢いことを少しずつ積んでいく設計にしました。

💬 集客・導線設計と、AI導入・自動化のご相談はこちら

暴走対策1 走る前にコスト枠を確認する

最初はラベルで判定していました

最初のゲートはコストです。5時間ブロックの出力トークンを見て、危険水位なら走らせる前に止めます。

BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
  log "ABORT: budget critical"; exit 0
fi

🟡のまま残量マイナス、606秒走った事故

ところが、このラベル判定には穴がありました。表示は 🟡burst なのに、実際の残量はもうマイナス。このケースが素通りしてしまったんです。

ログを見ると、残量−8003の状態で重いモデルが606秒走っていました。黄色信号のつもりが、とっくに赤だったわけです笑

そこでラベルとは別に、残量を数値で計算し直すようにしました。0以下なら実行しません。

REMAINING=$((800000 - BLOCK_OUT))    # 5h block cap 800k 想定
if [ "$REMAINING" -le 0 ]; then
  log "SKIP: block exhausted"; exit 0
fi
色や文字列のしきい値は、境界で必ずすり抜けます。コスト上限は残量の数値で持ち、最後は引き算して <= 0 かを見る。これがいちばん確実でした。

残量に合わせてeffortとモデルを変える

残量に応じて、effort と max-turns も変えています。残りが少なければ軽く短く、余裕があれば高めにします。

無人ジョブの既定モデルは安いものにしました。以前、高性能モデルを常用していたら5hブロックを使い切り、その後のスロットが全部SKIPになったからです。重いタスクを意図的に回す時だけ、環境変数で上書きします。

MODEL="${AUTOPILOT_MODEL:-claude-sonnet-4-6}"   # 既定は安価モデル

max-turnsでは止まらなかった

max-turnsが縛るのはターン数だけです。実際、7.25時間走り続けたことがありました! そこで壁時計のtimeoutも被せて、gtimeout 7200(2時間)で頭打ちにしています。

暴走対策2 同じタスクを繰り返させない

候補が毎回空っぽだった

いちばん大きかったのはこの修正です。タスク候補は next-session-todo.md の「High impact」セクションから拾います。ところが最初に書いたawkが壊れていて、候補が毎回空でした。

# 旧実装の範囲パターン /^### High impact/,/^###/ は
# 開始行自身が終端条件にも一致して即終了 → 常に空。
# → フォールバックの固定タスクが毎回選ばれ、同一タスクを6日で17回反復。

フォールバックの固定タスクが17回選ばれていた

/^### High impact/,/^###/ と書くと、開始行そのものが終わりの条件にも一致します。始まった瞬間に終わるので、中身は常に空でした。

候補が空なら、フォールバック用の固定タスクが選ばれます。それが毎回続いた結果、同じタスクを6日間で17回やっていたわけです。たった1行のパターンのせいで、17回分の実行が同じ作業に使われていました。

気づいてすぐ、範囲パターンをフラグ方式に書き直しました。候補が正しく並ぶのを確かめてから、履歴を使ったdedupeも足しています。

履歴JSONLで「最近やった」を判定する

  • 直近48時間に exit 0 で終わったタスクはskip
  • 直近2回続けて失敗しているタスクもskip(無理なものを叩き続けない)
if any(r.get("exit_code") == 0 for r in recent):
    print("done-recently")          # 最近やった → 次の候補へ
if len(recent) >= 2 and all(r["exit_code"] != 0 for r in recent[-2:]):
    print("failing-repeatedly")     # 連続失敗 → 諦めて次へ

履歴は1行1レコードのJSONLです。タスク名、exit code、所要秒数、モデル、effortを残します。「最近やった」「詰まり続けている」を、機械が判定できるようになりました。

AIの自己申告を実測で確かめる

du -skで前後を測る

無人で回していると、claude -p が「N MB回収しました」と報告してきます。でも、その数字を確かめる人は誰もいません。そこでharness側で実測を取り、結果ファイルに並べて書くことにしました。

DISK_BEFORE_KB=$(du -sk "$HOME/.claude" | awk '{print $1}')
# ...claude -p 実行...
DISK_AFTER_KB=$(du -sk "$HOME/.claude" | awk '{print $1}')
DISK_DELTA_KB=$(( DISK_AFTER_KB - DISK_BEFORE_KB ))
FILES_TOUCHED=$(find "$HOME/.claude" ... -newer "$RUNSTART_REF" | wc -l)

主張と実測を同じファイルに並べる

結果ファイルの末尾に「## 自己検証(harness実測 / claudeの主張ではない)」という欄を作ります。disk deltaと変更ファイル数を書き、「本文の数値主張がこの実測と乖離する場合は本文を疑う」と添えておきます。

AIに語らせた数字の隣に、OSが測った数字を必ず置く。無人運用を信用できるようになったのは、この仕組みを入れてからです。

プラン枠をステータスラインに出す

stdinからrate_limitsを読む

暴走を止めるには、人間の側もコストをいつも見ておきたいところです。私はClaude Codeのステータスラインに、5hと7dのプラン使用率を出しています。

これがstdinから直接取れると知った時は嬉しかったです! 認証もエンドポイント呼び出しもいりません。

H5=$(j '.rate_limits.five_hour.used_percentage')
H5R=$(j '.rate_limits.five_hour.resets_at')
D7=$(j '.rate_limits.seven_day.used_percentage')
CTX=$(j '.context_window.used_percentage')

初回ターンは「--」で待つ

rate_limits は、サブスクで最初のAPI応答が返った後にしか現れません。なので初回ターンまでは -- を表示します。

これで「🕐 5h 42% ⏪14:30 / 📅 7d 18%」のように、使用率とリセット時刻がいつも見えるようになりました。autopilotが裏で使った分も、ここにすぐ出ます。

踏んだ落とし穴の一覧

  • ラベル判定だけだと残量マイナスが素通りした → 残量を数値で計算し直し、<= 0 で止める
  • max-turnsだけでは7時間走った → gtimeout で壁時計の上限をかける
  • 高性能モデルの常用で枠を使い切り、全部SKIPになった → 無人の既定は安いモデル、重い時だけ上書き
  • awkの範囲パターンが空を返し、固定タスクを17回繰り返した → フラグ方式+履歴dedupe
  • AIの自己申告を誰も確かめていなかった → harnessで実測し、結果に並べて書く
  • launchdの最小PATHでclaudeが見つからなかった → PATH、.local/bin、nvmの順に探す

まとめ

  • 無人ループは、走らせる前にコスト枠を数値で確認する(ラベル判定は境界で漏れます)
  • 暴走は、残量の上限・壁時計timeout・安いモデルを既定、の3つを重ねて止める
  • 同じタスクの反復は、履歴JSONLのdedupe(最近完了・連続失敗をskip)で防ぐ
  • AIの自己申告は、harnessの実測と必ず並べる。盛りや読み違いに気づけます
  • プラン枠はステータスラインのstdinから取れる。いつも見えていることが監視の出発点です

これで8本になりました。記憶、スキル、コンテキスト、launchd、協業、セキュリティ、長期記憶、自律ループと、私のClaude Code環境のほぼ全部を書いています。エンジニア出身ではない私がここまで組めたのは、事故を1つずつ記録して、次は機械で止めるようにしてきたからだと思います。最後まで読んでくれて、ありがとうございました。

📕 この記事の内容を、実装まで全部まとめた有料記事があります 手順・コード・テンプレをそのまま渡しています AIで「寝てても回る仕組み」をゼロから30日で組む全手順