メインコンテンツへスキップ
見出し画像

claude -pの終了コード0を疑う|査読論文16本


    この記事の結論

    ・手元で claude -p に失敗を9件起こすと(@ji_ai さんの分類に当たる8件と、対照1件)、8件は終了コード0で終わりました。終了コードと is_error が捕まえたのは9件中1件、成果物の更新時刻の比較は9件すべてです(1ケース1回の手元実験。今回の失敗はどれも「成果物ができない」形なので、更新時刻が全部捕まえるのは当然で、弱点は本文で書きます)。
    ・終了コード0になる道は2つありました。モデルが動いた6件では、AI が失敗を文章にしてターンを普通に閉じた。残る2件は、モデルが1ターンも動かないまま、Claude Code が「成功」を返した。人間への問い返しで終わったのは5件です(うち1件はプロンプトが「聞いて」と頼んだもの)。
    ・出発点は、cron で claude -p を2,114回回し、41回(1.9%)が終了コード0なのに成果物がなかった、という @ji_ai さんの記録です。その対策の1つである降参フレーズの grep は、作者の過去ログでは効いたそうですが、ぼくの9件の返事には、名前の分かっている2つのフレーズがどちらも出てきませんでした。

    朝、cron のログを開く。「成功」が並んでいる。でも、できているはずのファイルがない。

    終了コード0は、宅配の「配達完了」の通知に似ています。通知が来ても、玄関に荷物があるとは限りません。

    この記事は、@ji_ai さんの記録を出発点に、3つを突き合わせたものです。

    ・手元の再現実験:cron に近い最小の環境で、claude -p に失敗を9件起こさせた
    ・公式ドキュメントと GitHub の報告:Claude Code 側の仕様と既知の不具合
    ・査読論文16本:障害・設定ミス・ログ・タイムアウト・AIエージェントの失敗の研究

    途中に9件の結果の一覧、最後にテスト済みのラッパー(43行)を載せます。Claude Code に組ませたそのラッパーの最初の版が、新しい「止まる」を作りかけた話も書きます。全文無料です。

    先に、実務の結論

    ・合否は、成果物の更新時刻と中身(空でないか)で決める。終了コードと is_error は「配達完了」の通知として見るだけにする
    ・cron の PATH は自分で書き、使うコマンドがあるかを最初に確かめる。ラッパー自体も絶対パスで呼ぶ
    ・失敗は非0で終わらせ、何で落ちたかを1行残し、届く先を作る
    ・6つのガードの判定(判定の根拠と条件は本文):成果物の更新時刻は○、降参フレーズの grep・ロック・打ち切り・権限の明示は△、is_error は✕(Claude Code 自体のエラー用には残す)




    2,114回中41回。終了コード0の裏で起きていたこと


    @ji_ai さんは、4つのジョブを cron で90日間動かし、claude -p を計2,114回呼びました。そのうち41回(1.9%)は、終了コードが0なのに成果物がありませんでした。50回に1回ほどです。

    原因の内訳はこうです。

    ・ヘッドレス(画面なし)では使えないツールを呼ぼうとした:14回
    ・PATH に必要なコマンドがなかった:9回
    ・プロンプトにスラッシュコマンドが入っていた:7回
    ・git のロックがぶつかった:6回
    ・権限の確認待ちで止まった:5回

    この記事の出発点にした @ji_ai さんの X の投稿の要約では、約40行のラッパーで6つのガードをかけた、とされていました(投稿本文は、この記事を作った環境からは取得できていません)。

    ・成果物の更新時刻を、実行の前後で比べる
    ・返事から「I was unable to」などの降参フレーズを grep で探す
    ・mkdir でロックを取り、同時実行を防ぐ
    ・gtimeout 900 で、15分で打ち切る
    ・--permission-mode を明示する
    ・--output-format json の is_error を jq で検査する

    ひとつ断っておきます。X の投稿本文は、この記事を作った環境からは取得できませんでした。そこで、同じ数字を載せたベトナムの技術メディア NextFuture の記事(2026年9月11日)で照合しています。4ジョブ・90日・2,114回・41回と、原因の内訳は一致しました。

    ただし、ガードのうち gtimeout 900 や「約40行」という記述は、NextFuture の記事にはありません。そちらに載っているのは「4つのルール」と「15分の上限」までです。15分は、正常な実行の最長が6分12秒だったことから決めたそうです。mkdir でロックするのは macOS に flock がないから、とも書かれています。

    降参フレーズについては、作者が「I was unable to」「would need to be done manually」など5つのパターンで、過去ログのうち作業する代わりに説明で済ませた回を全部捕まえた、と紹介されています。

    NextFuture の記事にだけ載っている数字もあります。41回はすべてガードを入れる前の60日間に起き、入れた後の30日間は黙った失敗が0回。代わりに、タイムアウトや実行時刻の重なりといった「うるさい失敗」が19回あった、というものです。

    原典で確かめられていないので、ここは割り引いて読んでください。1人・4ジョブの記録で、比べる相手もいません。それでも読み方としては面白い。失敗が減ったのではなく、失敗が聞こえるようになった、と読めるからです。




    ぼくのログにも「完了・エラー0件・3ミリ秒」があった


    他人の話で終わらせないために、自分の記録を見返しました。

    ぼくは6月の一時期、記事と動画の制作を、claude -p を工程ごとに呼び出すツール(atelier)で回そうとしていました。その実行ログが8件残っています。この記事を作る途中で、Claude Code にそのログを読み直してもらいました。

    見つかったのは、こういう記録です。

    ・状態は「completed(完了)」、エラーは0件、全工程を実行済み
    ・なのに、所要時間が2〜4ミリ秒
    ・それが8件中5件

    何工程もある制作が、数ミリ秒で終わるはずがありません。ツールのソースコードを読むと、dry-run(実際には実行しない確認モード)では、各工程を動かさずに「完了」へ進めて件数だけ数える作りでした。

    所要時間とこの作りからは、dry-run だったと考えられます。当時の作業メモにも、工程の確認は dry-run で、とありました。

    だとすれば、失敗ではありません。問題は、ログに dry-run の印がないことです。後から見ると、本物の成功と区別がつきません。

    実際に動いた残り3件のうち、失敗は1件でした。調査の工程で「Medium exited with code 143: 」とあり、コロンの後ろは空です。

    143 は SIGTERM(終了の合図)で止められたという意味で、このツールは1工程を1,200秒で打ち切る作りでした。ログのどこにも「タイムアウト」とは書かれていません。ぼくが制作の実行方法を変えて、重い調査を claude -p から外した理由の1つが、この打ち切りです。

    ここで、グレー障害という考え方が役に立ちます。Huang ら(2017)は、「使う側は不調だと見ているのに、監視する側は健全だと見ている」ずれを、差分観測可能性と名づけました。

    ログの「completed」は監視する側の見え方で、ぼくが欲しかったのは成果物です。ただしこれはデータのない概念論文で、考え方の補助線として使っています。




    cronに近い最小の環境で、失敗を9件起こした


    次に、@ji_ai さんの5つの原因を手元で再現できるか試しました。実験は Claude Code に組ませ、失敗を起こすためのプロンプトも Claude Code が書いています。

    claude -p の実行は15回までという条件で、ぼくが許可しました。後述のラッパーの確認を含めて、15回を使っています。

    条件は次のとおりです。

    ・macOS、Claude Code v2.1.270、モデルは haiku(費用を抑えるため)
    ・cron に近い最小の環境を env -i で作った。渡したのは HOME・LOGNAME・USER・SHELL=/bin/sh・PATH=/usr/bin:/bin だけ(/usr/bin:/bin は macOS の cron 本体に書かれた既定値)。cron や launchd から起動したわけではなく、ターミナルから環境変数をそろえただけ
    ・自分の設定は読み込まない(--setting-sources project と --strict-mcp-config)。ぼくの普段の設定は許可が広く、そのままだと権限の失敗が起きないため
    ・1ケースにつき1回。プロンプトは失敗を起こすために書いたもので、自然に起きる頻度を測ったものではない

    まず claude -p を1回も実行せずに、cron と同じ PATH で何が見つかるかを見ました。jq・git・perl は見つかり、claude・node・gtimeout・timeout は見つかりません。この Mac の jq は、OS 標準の場所(/usr/bin)にありました。

    そのうえで起こした失敗を、@ji_ai さんの分類に沿って並べます。分類に当たるのが8件、最後の1件は対照です。Claude の返事は英語で、「」の中は訳して縮めたものです(英語のまま載せたものを除きます)。

    PATH の欠落。「node --version の出力をファイルに書いて」と頼みました。node は PATH にないので見つかりません。

    Claude の返事は「Node.js is not installed on this system」。実際はインストールされていて、PATH にないだけです。誤診したまま「別の場所を探すか、先にインストールするか、エラー文そのものを書くか」と聞き、終了コードは0でした。PATH に node の場所を足すと、同じ依頼が成功しました。

    ヘッドレスで使えないツール。「AskUserQuestion というツールでファイル名を聞いてから作って」と頼むと、「そのツールは使えないので直接聞きます。どのファイル名にしますか?」で終わりました。このケースは、プロンプトがそもそも「聞いて」と頼んでいます。

    読み込んでいない MCP のツールを指定すると、「このセッションでは使えません。代わりに Web 検索しましょうか?」で終わりました。どちらも終了コード0です。

    スラッシュコマンド。存在しない /daily-report を送ると、返事は「Unknown command: /daily-report」でした。ターン数0、費用の見積もりも0で、終了コードは0です。

    前処理のコマンドが失敗するスキルを呼ぶと、ターン数0・返事は空・終了コード0でした。これは GitHub に v2.1.233 で報告されている不具合(#87159)と同じ形で、v2.1.270 の手元でも同じでした。この2件では、モデルは1ターンも動いていません。

    git のロック。.git/index.lock が残った状態で「コミットして」と頼みました。Claude はロックを消す rm -f .git/index.lock を含むコマンドを実行しようとし、拒否されました(許可したのは git で始まるコマンドだけで、rm は入っていません)。返事は「手動で消してください。消したら進めましょうか?」。終了コードは0です。

    権限の確認待ち。権限を何も渡さずに touch を頼むと、9秒で拒否され、終了コード0で終わりました。止まったままにはなっていません。同じ依頼に --permission-prompts none を付けても、拒否されて終了コード0でした。

    公式ドキュメントにも、確認に答える相手(ホスト)がいない -p では、確認が要る操作は拒否されると書かれています。@ji_ai さんの「確認待ちで停止」は、ぼくの条件では再現しませんでした。バージョンや構成の違いかもしれませんが、そこは確かめていません。

    最後に対照として、存在しないモデル名を指定しました。これだけが終了コード1・is_error true の「うるさい失敗」でした。ただし、そのとき subtype は "success" のままです。

    9件の結果の一覧(1ケース1回・保存用)

    並びは「終了コード/is_error/成果物/返事の言い回しで拾えるか」です。

    ・PATH の欠落:0/false/なし/拾える
    ・AskUserQuestion を使わせる:0/false/なし/拾える
    ・読み込んでいない MCP のツール:0/false/なし/拾える
    ・存在しないスラッシュコマンド:0/false/なし/拾える
    ・前処理が失敗するスキル:0/false/なし/拾えない(返事が空)
    ・git のロック:0/false/なし/拾える
    ・権限の拒否(既定のまま):0/false/なし/拾える
    ・権限の拒否(--permission-prompts none):0/false/なし/拾える
    ・存在しないモデル名:1/true/なし/拾えない

    8件が終了コード0で終わり、5件は人間への問い返しで終わっています(うち1件は、前述のとおりプロンプトが「聞いて」と頼んだもの)。




    終了コードが0になる2つの道:AIが文章にして閉じる、CLIが先に成功を返す


    画像


    1つめの道は、モデルが動いたときです。6件(PATH・2つのツール・ロック・2つの権限)では、Claude はエラーに気づいていました。返事を読むと、何が起きたかをほぼ理解しています(PATH だけは誤診でしたが)。

    そのうえで Claude は、エラーを文章にして、ターンを普通に閉じました。プロセスから見れば、正常に最後まで走ったことになります。だから終了コードは0です。

    2つめの道は、モデルが動く前です。存在しないスラッシュコマンドと、前処理が失敗するスキルの2件は、ターン数0のまま、Claude Code 自体が「成功」として返しました。モデルには、気づく機会すらありません。

    どちらの道も、システム障害の研究で繰り返し見つかっている形に似ています。ここからは、ぼくの当てはめとして読んでください。

    Yuan ら(2014, OSDI)は、分散データ処理の5システムで報告された障害198件を手で分析しました。壊滅的な障害48件のうち92%は、ソフトウェア自身が通知した「致命的ではないエラー」の扱いを誤ったことが原因でした。3件に1件ほど(35%)は、エラーを無視する・ログに書くだけで先へ進む、といった単純なミスです。

    エラーは見つかっていた。壊れたのは、見つけた後の扱いです。AI が「できませんでした」でターンを閉じるのも、CLI が「Unknown command」を一行出して正常終了するのも、ログに書いて先へ進むエラー処理と同じ形に見えます。

    ただし、この研究は分散システムが対象で、AI エージェントは扱っていません。同じ研究では障害の76%が明示的な失敗メッセージを出していて、問題は「黙る」ことより「出たメッセージが合否に使われない」ことでした。

    記録そのものが残らない場合もあります。Yuan ら(2012, OSDI)は、Apache や PostgreSQL など5つのソフトの障害250件のうち、142件(57%)が診断に使えるログを残していないことを示しました。プログラム自身はエラーを検知していた例が61%あり、ログが残っていた障害は診断が平均2.2倍速かったそうです。

    AI エージェントの研究も、同じ場所を指しています。Cemri ら(2025, NeurIPS)は、マルチエージェントの実行記録1,642件の失敗を14種類に分け、23.5%が「タスクの検証」の失敗だと報告しました(早すぎる終了6.2%、検証なし・不十分8.2%、誤った検証9.1%)。cron の無人実行とは違う仕組みの数字ですが、「終わった」と「できた」を取り違える場所は同じです。

    AI の自己申告に頼るのも危うい。Huang ら(2024, ICLR)は、外からの手がかりなしに LLM に自分の答えを見直させると、正答率がむしろ下がることが多いと示しました(GPT-4 の算数の文章題で95.5%→89.0%)。推論課題の結果なのでそのまま当てはまるとは言えませんが、自分で自分を採点させるだけでは足りない、とは言えそうです。




    6つのガードを仕分ける:捕まえる・条件つき・素通り


    では、6つのガードは、手元の失敗9件にどう効くのか。録った出力に1つずつ当てて仕分けました。基準は「黙った失敗を捕まえるか」です。

    ○ 捕まえる:成果物の更新時刻

    9件すべてを捕まえました。今回の失敗はどれも成果物ができない形なので、捕まえて当然です。それでも、仕事が終わったかを確かめられるのは、仕事の端点だけです。

    Saltzer ら(1984)の「エンドツーエンド論証」は、正しさの確認は途中の層ではなく、目的を知っている端点でしか完全にはできない、と論じました(データのない設計論です)。配達で言えば、通知ではなく、置き配の写真を見るやり方です。

    プロセスが生きていても一部の機能だけ壊れる「部分故障」の研究では、100件のうち15件が無言で、診断までの時間の中央値は6日5時間でした(Lou ら 2020, NSDI)。生きているかどうかの信号だけでは、こういう失敗は見えません。

    弱点もあります。AI がエラー文をファイルに書き込めば、更新時刻は変わります。実際、PATH のケースで Claude は「エラー文そのものを書くか」と申し出ていました。更新時刻に加えて、中身が空でないか、返事がどうだったかも見る必要があります。

    △ 条件つき:降参フレーズの grep

    「I was unable to」は、9件の返事に一度も出てきませんでした。紹介記事で名前の出ているもう1つの「would need to be done manually」も0件です。

    作者は5つのパターンで過去ログの該当回を全部捕まえたそうなので、言い回しはモデルやプロンプトで変わる、ということだと思います(ぼくのほうは haiku で、失敗を起こすために書いたプロンプトです)。

    ぼくの9件の主な言い回しは「I cannot」「I don't have access to」「was blocked」「Would you like…?」「Unknown command」、そして空の返事でした。

    言い回しを広げた網(unable to/cannot/don't have access/not found/blocked/unknown command/would you like など)にすると、9件中7件を捕まえました。空の返事と、モデル名の誤りの返事は捕まえられません(後者は終了コードで捕まります)。

    主役にはできませんが、「書いたのに、返事では降参している」を拾う二段目の網としては使えます。

    △ 条件つき:mkdir のロック

    これは検知ではなく予防です。mkdir は「なければ作る」を1手で済ませるので、ロックに使えます。

    ただし、鍵の単位に注意が要ります。ジョブ名ごとの鍵だと、別のジョブが同じリポジトリを触るのは防げません。@ji_ai さんの git のロックの衝突も、別の実行が同じリポジトリを触った形だと思います(紹介記事に詳しい内訳はありません)。後で載せるラッパーは、cd した場所から鍵を作っています。サブフォルダから呼ぶと別の鍵になるので、リポジトリの最上位へ cd してから呼びます。

    気になったのは git のロックのケースです。Claude は、ロックファイルを自分で消そうとしました。今回は古いロックでしたが、もし別のジョブが実行中のロックだったら、排他を壊すところです。ロックの削除は AI の許可に入れないほうがいい、と思いました。

    「時々だけ落ちる」失敗の多くは、タイミングと並行性から来ます。原因を分類できた不安定なテスト161件では、非同期の待ちが45%・並行性が20%でした(Luo ら 2014, FSE)。

    並行処理のバグ105件のうち101件は、スレッド2本だけで再現できました(Lu ら 2008, ASPLOS)。ただしこれはプログラム内部の研究で、プロセス同士のファイルのロックを直接調べたものではありません。構造が似ている、という程度の根拠です。

    △ 条件つき:gtimeout 900 の打ち切り

    止まったまま終わらない失敗には効きます。タイムアウトの研究では、タイムアウト関連の不具合156件のうち83件(53.2%)がエラーメッセージを一切出していませんでした(Dai ら 2018, IC2E)。

    著者は、タイムアウトの欠落はハングを起こし、多くはメッセージを残さないと書いています。なお、この論文は見出しの割合と本文の件数が合っていないので、ここでは本文の件数を使いました。上限の決め方は、紹介記事の作者のやり方(正常な実行の最長の6分12秒から、15分に)が参考になります。

    問題は gtimeout そのものです。これは GNU coreutils の道具で、macOS には標準で入っていません。手元で試すと、cron と同じ PATH では gtimeout 900 true が終了コード127(コマンドが見つからない)で落ちました。ただし、この Mac には gtimeout 自体を入れていないので、見つからないのは当然です。

    Homebrew で入れた場合も、置き場所(/opt/homebrew/bin など)は cron の PATH(/usr/bin:/bin)の外です。PATH を書かない限り、同じく見つからないはずです。

    同じ理由で、定期実行が全部壊れていた例もあります。claude を定期実行するためのツール「Claude-Warp」(GitHub で公開)の修正(2026年9月4日)では、timeout が macOS になく、毎回127で落ちていました。しかもリトライの処理がそれを一時的な失敗と誤解して、リトライを使い切っていました。

    PATH の欠落は、設定ミスの研究で言う「潜在的な誤り」に当たります。Xu ら(2016, OSDI)によると、重要な設定の14.0〜93.2%は起動時に検査されていませんでした。調べた商用ストレージ1社では、深刻な設定問題の75.0%が、使われた瞬間に初めて表に出る種類でした。

    gtimeout がないことは、gtimeout を呼ぶ瞬間まで分かりません。クラッシュもメッセージもない静かな失敗が設定エラーで報告されていることは、Xu と Zhou(2015)のサーベイも整理しています。検知策そのものが、PATH の穴に落ちるわけです。

    △ 条件つき:--permission-mode の明示

    公式ドキュメントでは、-p の既定の権限モードはどのプランでも Manual(確認が要る)です。モードを明示すれば、何が許されるかが実行ごとに固定されます。v2.1.259 以降なら --permission-prompts none も使えて、「承認できる人はいない、再試行しないで」と Claude に伝わり、AskUserQuestion も外れます。

    手元では、モデルが動いた6件のうち、これを付けたケースだけ、返事が問い返しでなく説明で終わりました(1回ずつなので傾向とは言えません)。予防の設定で、検知ではありません。

    なお acceptEdits は、作業フォルダの中なら rm や rmdir も確認なしで許します(.git などの保護された場所は除く)。リポジトリの中に自前のロックファイルを置くなら、そこも消されうると考えておいたほうがいいです。

    ✕ 素通り:is_error の検査

    9件中、捕まえたのはモデル名の誤りの1件だけでした。終了コードと同じです。is_error は「Claude Code が止まった」を拾う網で、「仕事が止まった」は拾いません。

    それでも残す価値はあります。認証切れや利用上限のような Claude Code 自体のエラーは、ここに出るからです。公式ドキュメントでは、実行中の失敗は stderr ではなく stdout の結果に出ます。stderr だけを記録していたせいで、失敗14回のログが空だった、という報告もあります(dev.to の Lain さん)。

    仕分けると、○1・△4・✕1。黙った失敗を正面から捕まえたのは、成果物だけでした。




    ラッパーを組ませたら、新しい「止まる」を作りかけた


    画像


    @ji_ai さんのラッパーのコードは見られていないので、6つのガードの説明と、手元の実験で分かったことから組み直しました。原作とは別物です。組み直したのも、そのテストを書いたのも Claude Code です。

    変えた点はこうです。

    ・PATH を先頭で自分で書き、claude と jq があるかを最初に確かめる(後で使うものほど、先に検査する)
    ・ロックの鍵を、ジョブ名ではなく cd した場所(リポジトリの最上位)から作る
    ・打ち切りは gtimeout に頼らず、bash だけで見張る。SIGTERM で止まらなければ、10秒後に SIGKILL(claude -p は SIGTERM で終了コード143になる、とドキュメントにある)
    ・出力は stdout の JSON をファイルに残し、stderr は別のファイルに残す
    ・判定の順番は、打ち切り→終了コード→is_error→成果物の更新時刻→空でないか→返事の降参・問い返し(返事は全文を見る)
    ・見つけた失敗は非0で終わり、何で落ちたかを1行で fail.log に残す

    最後の点には根拠があります。設定ミス546件の研究では、原因を名指しするメッセージがあると、あいまいなメッセージに比べて診断時間が3〜13倍短くなっていました(Yin ら 2011, SOSP)。

    ここで、失敗を2つしています。

    1つめ。最初の版は、打ち切り役の sleep 900 を裏で走らせていました。ところがジョブが終わっても sleep が残り、出力のパイプを握ったままになる。テストでは、呼び出し側が900秒待たされる作りでした。

    cron もジョブの出力をパイプで受け取るので、同じことが起きうるはずです(cron そのものでは試していません)。検知策が、新しい「止まる」を作るところでした。気づけたのは、録った出力を流し込む再生テストが120秒たっても終わらなかったからです。

    2つめは、記事を書いた工程とは別の Claude(レビュー役のサブエージェント)が見つけました。返事のチェックが、ログ表示用に切り詰めた先頭200字しか見ていなかったのです。

    ロックのケースの返事は降参が200字より後ろにあるので、成果物さえ書けていれば OK で通ってしまう。スタブで再現してから、全文を見るように直しました。

    ロックの単位、引数の検査、打ち切りの二段構え、ラッパー自身が止められたときの後始末も、同じレビューで直しています。

    直した版の確認は2段階です。

    ・再生テスト:録った11件の出力と、追加の10ケース(打ち切り・ロック中・claude がない・書いたのに降参・「完了」と言ったのにファイルなし・200字より後ろの降参・別のジョブが同じリポジトリを使用中・引数不足・SIGTERM を無視する子・ラッパー自身が止められる)を流すと、すべて期待どおり。成功2件は OK、ほかは理由つきの FAIL で、引数不足は使い方を出して終了、ラッパーを止めると子のプロセス(claude の代わりのスタブ)も止まって、ロックが消えました
    ・本物の claude で2回:前述の cron に近い最小の環境(env -i)で、成功するケースは OK、PATH のケースは「成果物が更新されていない」で FAIL(返事はまたも「Node.js is not installed」でした)

    本物で試した2回は、モデル指定などを足す薄い中継スクリプト経由です。直す前の版でも同じ2ケースを1回ずつ試していて、結果は同じでした。

    使う前に、自分の環境に合わせて変えるところは4か所です。

    ・export PATH の行:claude や、ジョブで使うコマンド(node など)の場所を足す
    ・CLAUDE_BIN:claude を PATH に入れない場合は、絶対パスで渡す(nvm で入れた claude は、たいていこの PATH の外にあります)
    ・LIMIT_SEC:打ち切りまでの秒数(既定は900)。正常な実行の最長を測ってから決める
    ・--permission-mode:ジョブに必要な権限に合わせる(acceptEdits はファイルの編集と、作業フォルダ内の mkdir・rm などを許すモード)

    コードはこれです。

    #!/bin/bash
    set -u
    [ $# -eq 3 ] || { echo "使い方: cron-claude.sh ジョブ名 成果物のパス プロンプトのファイル" >&2; exit 2; }
    export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
    JOB=$1 ART=$2 PROMPT=$3
    CLAUDE=${CLAUDE_BIN:-claude}
    LIMIT=${LIMIT_SEC:-900}
    LOGDIR=${LOGDIR:-$HOME/.cron-claude}; mkdir -p "$LOGDIR"
    OUT="$LOGDIR/$JOB-$(date +%Y%m%d-%H%M%S).json"
    LOCK="/tmp/cron-claude-$(pwd -P | cksum | cut -d' ' -f1).lock"
    pid=
    fail() { echo "[$JOB] FAIL $(date '+%F %T') $1" | tee -a "$LOGDIR/fail.log" >&2; exit 1; }
    mtime() { date -r "$1" +%s 2>/dev/null || echo 0; }
    reply() { jq -r '.result // empty' "$OUT" 2>/dev/null; }
    said() { reply | tr '\n' ' ' | head -c 200; }
    stop() {
      kill -TERM "$pid" 2>/dev/null
      for i in 1 2 3 4 5 6 7 8 9 10; do kill -0 "$pid" 2>/dev/null || return 0; sleep 1; done
      kill -KILL "$pid" 2>/dev/null
    }
    command -v "$CLAUDE" >/dev/null || fail "claude が見つからない PATH=$PATH"
    command -v jq >/dev/null || fail "jq が見つからない"
    mkdir "$LOCK" 2>/dev/null || fail "同じリポジトリの前回の実行がまだ終わっていない: $LOCK"
    trap '[ -n "$pid" ] && stop; rmdir "$LOCK"' EXIT
    trap 'exit 1' INT TERM
    before=$(mtime "$ART")
    "$CLAUDE" -p "$(cat "$PROMPT")" --output-format json \
      --permission-mode acceptEdits --permission-prompts none \
      > "$OUT" 2>> "$LOGDIR/stderr.log" < /dev/null &
    pid=$!; t=0; timed_out=0
    while kill -0 "$pid" 2>/dev/null; do
      [ "$t" -ge "$LIMIT" ] && { timed_out=1; stop; break; }
      sleep 1; t=$((t + 1))
    done
    wait "$pid"; ec=$?; pid=
    [ "$timed_out" -eq 1 ] && fail "${LIMIT}秒で打ち切った"
    [ "$ec" -ne 0 ] && fail "終了コード $ec: $(said)"
    jq -e '.is_error == false' "$OUT" > /dev/null || fail "is_error: $(said)"
    [ "$(mtime "$ART")" -gt "$before" ] || fail "成果物が更新されていない: $ART / 返事: $(said)"
    [ -s "$ART" ] || fail "成果物が空: $ART"
    reply | grep -qiE "unable to|cannot|can.t|don.t have access|not found|blocked|unknown command|would you like|できません" \
      && fail "返事が降参か問い返し: $(said)"
    echo "[$JOB] OK $(date '+%F %T')"

    cron からは、ラッパーも絶対パスで呼びます。cron の PATH では、ラッパー自体が見つからないからです。「cd リポジトリの絶対パス && ラッパーの絶対パス ジョブ名 成果物のパス プロンプトのファイル」の形になります。cd する先はリポジトリの最上位にします(ロックの鍵になるため)。

    更新時刻は date -r で取るので、macOS と Linux のどちらでも動くはずです(試したのは macOS だけです)。

    注意が2つあります。--permission-prompts none は v2.1.259 より前だと起動時にエラーになりますが、その場合はそれ自体が「うるさい失敗」として出ます。また、ラッパー自体が kill -9 や電源断で止まると、ロックのディレクトリが残ります。次からは毎回「前回の実行がまだ終わっていない」で落ちるので、気づいたら人が確かめて消してください。

    コードの限界も2つあります。打ち切りが SIGKILL まで行ったときは、claude 本体だけを止めます。claude が起動したコマンドが残ることがあり、しかもロックは外れるので、次の実行と重なりえます(ドキュメントでは、SIGTERM を受けた claude は実行中のコマンドを止めるので、問題になるのは claude が10秒以上応じないときです)。

    また、claude が何も出さずに終わったときも、FAIL の理由は「is_error:」と出ます。

    cron のメールを通知に使うなら、最後の OK の行を別のファイルに回すと、メールは失敗のときだけになります。このコードのままだと OK も標準出力に出るので、成功のたびにメールが届きます。




    それでも捕まえられないもの


    このラッパーでも、見えないものが残ります。

    1つめは、中身の正しさです。更新時刻が変わっても、中身が正しいとは限りません。実務175タスクで AI エージェントを試した研究では、聞くべき相手が見つからないと、別のユーザーの名前をその人の名前に書き換えて「完了」にした例が報告されています(Xu ら 2025, NeurIPS)。中身の検査は、ジョブごとに書くしかありません。

    2つめは、繰り返したときの信頼性です。同じタスクを繰り返す研究では、小売の課題で gpt-4o が1回で成功する率は61.2%でも、8回続けて全部成功する率は25%未満でした(Yao ら 2025, ICLR)。1回うまくいったことは、次もうまくいく保証になりません。1回1.9%でも、2,114回なら41回です。

    3つめは、ぼくの実験の限界です。

    ・1ケース1回、haiku だけ、1台の Mac、v2.1.270
    ・cron や launchd から実際には起動していない(キーチェーンの認証や macOS の権限の違いは確かめていない)
    ・9件は @ji_ai さんの分類に当たる8件と対照1件で、権限の2件は同じ失敗をフラグの有無で比べたもの
    ・プロンプトは失敗を起こすために書いたもの。「確認待ちで停止」は再現していない
    ・cron で AI エージェントを動かしたときの失敗を扱った査読研究は、探した範囲では見つからなかった。成果物チェックや降参フレーズの grep の検出率を測った研究もない

    返事の網には誤検知もあります。成功した返事の最後に "Would you like me to do anything else?"(ほかに何かしましょうか?)と付くだけで、FAIL にします。うるさい誤検知ですが、黙るよりはましだと考えています。

    逆に、この網は英語の言い回し向けです。日本語の返事では「できません」以外、たとえば「見つかりません」や「〜しましょうか?」は素通りします。日本語で指示するなら、自分のジョブの返事を見て言い回しを足してください。

    4つめは、Claude Code 自体の変化です。ドキュメントには「以前は黙って〜していた」という記述がいくつもあります(不正な JSON スキーマを黙って無視していた、など)。--help にも、-p では検証に失敗した設定ファイルは黙って無視される、と書かれています。

    さらに --bare は将来 -p の既定になる予定で、bare モードはサブスクのログインを使いません。アップデートのたびに、再生テストを回し直すのが無難だと思います。

    ログを残す意味は、大きなサービスの障害でも同じです。32サービスの障害597件を調べた研究では、355件の根本原因が報告から分かりませんでした(Gunawi ら 2016, SoCC。ぼくは一次の PDF を開けず、テキスト抽出を経由した確認です)。




    明日の朝までにやる3つ


    ・合否を成果物で決める。更新時刻が変わったか、中身が空でないか。終了コードは「配達完了」の通知として見るだけにする
    ・失敗は非0で終わらせ、何で落ちたかを1行残し、届く先を作る。cron のメールでも、1日1回の「動いた」合図を外の監視サービスに送る方法でもいい。7つのエージェントのうち2つが初日から動いておらず、18日間気づかれなかった例もあります(dev.to の Ken Imoto さん)
    ・cron の PATH を自分で書き、使うコマンドがあるかを最初に確かめる。ラッパー自体も絶対パスで呼ぶ。gtimeout や jq を使うなら、それ自体も

    そして、ロックの削除は AI に任せないでください。




    最後まで読んでいただき、ありがとうございます。全文無料で公開しています。

    ・💬 コメントで1文字だけ教えてください。自分の環境で使いたいのはどれですか? A:macOS 用の launchd 設定+ラッパー/B:Linux 用の systemd timer+ラッパー/C:失敗を再生するテスト一式。票の多かったものから、実際に動かして確かめる予定です。
    ・👤 フォロー:投票で選ばれたものの検証も含めて、Claude Code の運用を手元の実験と査読論文で確かめる記事を続けています(note:designing_edges)。
    ・📚 関連記事①:「AIの失敗はエラーを出さない。実測7本の答え合わせ」。この記事は、その中の「終了した=達成した」を1本のデータで掘ったものです。 https://note.com/designing_edges/n/n874fd4f3bbce 
    ・📚 関連記事②:「Claude Codeが確認をやめる日|89%の読み方」 https://note.com/designing_edges/n/na013855aef9c 
    ・📚 関連記事③:「Claude CodeとCodexの使い分け|査読論文7本と企業3事例」 https://note.com/designing_edges/n/n19888b909ce6 




    参考にした査読論文16本と一次ソース


    査読論文は16本です。うち Saltzer 1984 と Huang 2017 は、データを持たない概念論文です。システム研究の数値は効果量ではなく、件数の割合です。

    ・Yuan D, et al. (2014) Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems. OSDI '14. https://www.usenix.org/conference/osdi14/technical-sessions/presentation/yuan 
    ・Yuan D, et al. (2012) Be Conservative: Enhancing Failure Diagnosis with Proactive Logging. OSDI '12. https://www.usenix.org/conference/osdi12/technical-sessions/presentation/yuan 
    ・Yin Z, et al. (2011) An Empirical Study on Configuration Errors in Commercial and Open Source Systems. SOSP '11. DOI: 10.1145/2043556.2043572
    ・Xu T, et al. (2016) Early Detection of Configuration Errors to Reduce Failure Damage. OSDI '16. https://www.usenix.org/conference/osdi16/technical-sessions/presentation/xu 
    ・Xu T, Zhou Y. (2015) Systems Approaches to Tackling Configuration Errors: A Survey. ACM Computing Surveys 47(4). DOI: 10.1145/2791577
    ・Lou C, Huang P, Smith S. (2020) Understanding, Detecting and Localizing Partial Failures in Large System Software. NSDI '20. https://www.usenix.org/conference/nsdi20/presentation/lou 
    ・Huang P, et al. (2017) Gray Failure: The Achilles' Heel of Cloud-Scale Systems. HotOS '17. DOI: 10.1145/3102980.3103005
    ・Saltzer JH, Reed DP, Clark DD. (1984) End-to-End Arguments in System Design. ACM Transactions on Computer Systems 2(4):277-288. DOI: 10.1145/357401.357402
    ・Dai T, et al. (2018) Understanding Real-World Timeout Problems in Cloud Server Systems. IEEE IC2E 2018. DOI: 10.1109/IC2E.2018.00022
    ・Gunawi HS, et al. (2016) Why Does the Cloud Stop Computing? Lessons from Hundreds of Service Outages. SoCC '16. DOI: 10.1145/2987550.2987583
    ・Luo Q, et al. (2014) An Empirical Analysis of Flaky Tests. FSE 2014. DOI: 10.1145/2635868.2635920
    ・Lu S, et al. (2008) Learning from Mistakes: A Comprehensive Study on Real World Concurrency Bug Characteristics. ASPLOS '08. DOI: 10.1145/1346281.1346323
    ・Cemri M, et al. (2025) Why Do Multi-Agent LLM Systems Fail? NeurIPS 2025 Datasets and Benchmarks Track. https://arxiv.org/abs/2503.13657 
    ・Yao S, et al. (2025) τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains. ICLR 2025. https://arxiv.org/abs/2406.12045 
    ・Huang J, et al. (2024) Large Language Models Cannot Self-Correct Reasoning Yet. ICLR 2024. https://arxiv.org/abs/2310.01798 
    ・Xu FF, et al. (2025) TheAgentCompany: Benchmarking LLM Agents on Consequential Real World Tasks. NeurIPS 2025 Datasets and Benchmarks Track. https://arxiv.org/abs/2412.14161 

    査読以外の一次ソース(個人の記録・公式ドキュメント・GitHub・ブログ):

    ・@ji_ai さんの X の投稿(本文はこの記事を作った環境から取得できず、数字は下の記事で照合)
    ・NextFuture「claude -p trong cron: 41 lần fail im lặng trên 2.114 lần」(2026-09-11) https://nextfuture.io.vn/blog/claude-p-trong-cron-41-2-114-lan-fail-ma-exit-code-van-0 
    ・Claude Code Docs「Run Claude Code programmatically」(2026-09-14 閲覧) https://code.claude.com/docs/en/headless 
    ・Claude Code Docs「Choose a permission mode」(2026-09-14 閲覧) https://code.claude.com/docs/en/permission-modes 
    ・anthropics/claude-code Issue #87159 https://github.com/anthropics/claude-code/issues/87159 
    ・lucagattoni/Claude-Warp Pull Request #73 https://github.com/lucagattoni/Claude-Warp/pull/73 
    ・Lain「The Claude CLI writes errors to stdout, not stderr」(dev.to) https://dev.to/lainagent_ai/the-claude-cli-writes-errors-to-stdout-not-stderr-5c1n 
    ・Ken Imoto「I Cron-Scheduled 7 AI Agents. 2 Silently Failed for 18 Days.」(dev.to) https://dev.to/kenimo49/i-cron-scheduled-7-ai-agents-2-silently-failed-for-18-days-tracing-wouldnt-have-caught-it-24fa 


    あなたへのおすすめ