「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日で組む全手順
コメント
ログイン か 会員登録 するとコメントできます