claude -pの終了コード|失敗に気づけないときの見分け方
「毎朝 cron で claude -p を回しているのに、止まっていたことに何日も気づかなかった」「ログを見たら終了コードは0だった」——無人で Claude Code を動かしていると、いつかこの場面に出会います。
結論から言うと、claude -p の終了コードは、認証の失敗・存在しないモデル・回数の上限への到達では1になりますが、中で呼んだ別の claude -p の失敗や、答えが空のまま終わった場合は0のままです。さらに 2.1.313 までは、起動そのものに失敗しても exit 0 で何も出さずに終わることがあり、2.1.314(2026年10月30日公開)でようやく exit 1 になりました。終了コードだけでは足りないので、JSON の中身と実行ログの文字列も合わせて見ます。
株式会社Fyveは、常時起動の Mac mini で claude -p の定期実行を毎日十数本回しています。この記事では、2026年10月31日に私が手元の 2.1.296 でわざと失敗させた5つの条件の結果と、認証切れが exit 0 のまま8日間見過ごされた私たちの失敗、その後に入れた見張りの作り方をお伝えします。
claude -p の終了コードは何を表すか
まず公式ドキュメントの説明を確認します。「Run Claude Code programmatically」のページには、claude -p の終了コードについて次のように書かれています(2026年10月31日に確認)。
- 成功したら0、実行が失敗したら0以外で終わる。スクリプトは終了コードで分岐できる
- 知らないオプションを渡したときは、実行が始まる前に stderr にエラーを出す
- 認証が無いなど、実行の中で起きた失敗は、結果として stdout に出す
- SIGTERM(
killなど)で止めると、終了コードは143になる。途中のターンは結果を残さない - パイプで渡す標準入力は10MBまで。超えるとエラーを出して0以外で終わる
ここで見落としやすいのは3つ目です。認証の失敗は stderr ではなく stdout に出ます。2>error.log のように stderr だけをエラー用のファイルに分けていると、エラーのファイルは空のまま、結果のファイルに「Failed to authenticate」と書かれて終わります。
終了コードの一覧
終了コード | 意味 | 出どころ |
|---|---|---|
0 | 実行が最後まで終わった | 公式ドキュメント |
1 | 実行の失敗(認証・モデル・回数の上限・知らないオプションなど) | 私の実測(2.1.296) |
143 | SIGTERM で止められた | 公式ドキュメント |
0(2.1.313まで)→ 1(2.1.314から) | 起動そのものの失敗 | 2.1.314 の変更履歴 |
公式ドキュメントは「0以外」とだけ書いており、失敗の種類ごとの番号の対応表は載っていません。1になったのは私の環境で試した範囲の結果です。スクリプトでは「0かどうか」だけで分け、具体的な番号には頼らないほうが安全です。
2.1.296 で実際に失敗させた結果
2026年10月31日、Mac mini の Claude Code 2.1.296 で、わざと失敗する条件を作って claude -p を実行しました。モデルは --model で明示し、標準入力は < /dev/null で閉じています。
条件 | 終了コード | エラー文の出先 | JSON の is_error | JSON の subtype | JSON の terminal_reason |
|---|---|---|---|---|---|
正常(1+1を答えさせる) | 0 | — | false | success | completed |
無効なログイン情報 | 1 | stdout | true | success | api_error(status 401) |
存在しないモデル名 | 1 | stdout | true | success | api_error(status 404) |
知らないオプション | 1 | stderr | (JSON は出ない) | — | — |
| 1 | — | true | error_max_turns | max_turns |
無効なログイン情報のとき、テキスト出力は「Failed to authenticate. API Error: 401 Invalid bearer token」の1行だけでした。--output-format json を付けると、この文が result に入り、api_error_status に401が入ります。
この表でいちばん意外だったのは、認証の失敗でも subtype が "success" になっていたことです。失敗を表しているのは is_error: true と terminal_reason: "api_error" のほうでした。JSON で成否を判定するなら、subtype ではなく is_error を見ます。
もう1つ、--max-turns の上限に届いたときは result が null(値なし)でした。「result を取り出して次の処理に渡す」作りのスクリプトでは、ここで空の文字列が後ろへ流れていきます。

2.1.314 で直ったこと:起動に失敗すると exit 0 で何も出なかった
2026年10月30日に公開された Claude Code 2.1.314 の変更履歴に、次の1行があります。
Fixed
claude -pexiting with code 0 and no output when startup failed; it now prints the error and exits 1
つまり 2.1.313 までは、claude -p の起動そのものが失敗したとき、終了コード0で、何も出力せずに終わることがありました。cron や launchd から見ると「正常に終わった、出力は空だった」としか見えません。終了コードだけを見ている見張りは、これを素通りさせます。
同じ版には、無人で回している人に関係する修正がほかにもあります。
- 組織の管理者がモデルの effort(考える量)に上限を設けていると、毎ターン「Invalid effort level」で失敗していた。2.1.314 から、effort を下げてやり直し、どの段階を使ったかを表示する
claude -p --resumeや--continueで入ったワークツリー(同じリポジトリの別の作業場所)に、サンドボックスの保護が効いていなかった- 多数のサブエージェントを動かす長いヘッドレスの実行で、メモリの使い方を改善した
自分の版に修正が入っているかの確かめ方
手元の版は claude --version で確かめます。2.1.314 以上なら、起動の失敗は exit 1 になります。
注意したいのは配布の経路です。2026年10月31日に npm で確かめた時点では、latest が 2.1.316、stable が 2.1.309 でした。安定版の経路で更新している環境には、この修正はまだ届いていません。私たちの Mac mini も自動更新を止めて 2.1.296 に固定しているため、修正の前の挙動のまま動いています。だからこそ、次の章の「終了コードで拾えないもの」への備えを外せません。
同じ 2.1.314 で入った、作業フォルダの外への書き込みを止める設定は、別の記事でまとめています。
終了コードでは拾えない4つの失敗
2.1.314 で起動の失敗が拾えるようになっても、claude -p が「最後まで動いた」以上は exit 0 になる失敗が残ります。公式ドキュメントの記載と私たちの運用から、4つに分けました。

1. 中で呼んだ別の claude -p の失敗
claude -p に Bash の実行を許していると、作業の途中で claude -p がもう1本の claude -p を呼ぶことがあります。1本目が作ったものを、別の指示を持たせた2本目に確かめさせる、といった使い方です。
このとき2本目が認証で失敗しても、1本目から見れば「コマンドを1つ実行したら、エラーの文が返ってきた」だけです。1本目はそれを読んで判断し、作業を最後まで終えるので、外側の終了コードは0になります。後の章で書く私たちの失敗は、まさにこの形でした。
2. 答えが空・期待した形でない
--output-format json と --json-schema で決まった形の答えを受け取る使い方では、公式ドキュメントに「Claude が依頼を断ったなどで有効な答えが作れなかったとき、structured_output の項目が無い」と書かれています。スクリプトの側で、項目があるかを確かめてから使う必要があります。
答えの文が空だった、途中で話がそれた、期待した見出しが無い、といった中身の崩れも、終了コードには表れません。
3. MCP サーバーの一部が読み込まれない
--mcp-config で渡した MCP サーバー(外部のツールをつなぐ仕組み)の設定に誤りがあると、Claude Code はその1件を飛ばして実行を続け、普通に終わります。公式ドキュメントには、JSON の system/init にある mcp_server_errors を見れば、読み込まれなかったサーバーが分かると書かれています。
stderr を端末に出していれば警告が表示されますが、cron のように stderr をファイルや別のプログラムが受け取る場合は警告が出ず、mcp_server_errors にしか残りません。
4. 権限が無くて断られた操作
無人の claude -p では、許可が必要な操作は、許可する人がいないので断られます。断られても実行は続き、最後まで終われば exit 0 です。公式ドキュメントによると、stream-json の出力では断られた操作が permission_denied として流れ、最後の結果の permission_denials に一覧で残ります。私の実測でも、--output-format json の結果に permission_denials の項目がありました。
このほか、バックグラウンドで始めた作業が10分間動かないまま待たされると、途中の結果を捨てて打ち切られる、とも書かれています(環境変数 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS で変えられる)。
私たちの失敗:認証切れが exit 0 のまま8日間続いた
ここからは私たちの運用の話です。Mac mini では、すべての定期実行を1本のシェルスクリプトで包み、その中で claude -p を呼んでいます。スクリプトは終わった後に終了コードを見て、0以外なら異常専用の Slack チャンネルへ通知する作りでした。
2026年8月、あるジョブの中で呼んでいた2本目の claude -p の認証が切れました。原因は2つ重なっていました。
- それまでは Mac のキーチェーンに保存されたログインで動いていたが、その保存されたログインの期限が切れ、更新もできなくなった
- 1本目の claude -p が Bash から起こした子のプロセスには、ログイン用のトークンの環境変数が引き継がれていなかった(その子のプロセスの中で、変数が見えるかを出して確かめた)
2本目は毎回「Failed to authenticate」を返し、1本目はそれを「確かめられなかった」と受け取って、安全側の扱いに回して作業を終えました。外側の終了コードはずっと0です。通知は鳴らず、8日間・25回の実行で、2本目の段は一度も働いていませんでした。
直し方そのものは簡単で、2本目を呼ぶ前に、環境変数をまとめたファイルを読み直すようにしました。問題は、壊れていることを知る手段が無かったことのほうでした。
入れた見張り:AI ではなくシェルが、ログの文字を見る
そこで、終了コードとは別に、実行ログを決まった文字列で探す段を足しました。当たれば、終了コードが0でも異常専用のチャンネルへ鳴らします。私たちが探している文字列は次のとおりです。
Failed to authenticate|Not logged in|OAuth session expired|could not be refreshedこれに、私たちのジョブが自分で書く「判定できなかった」を表す日本語の文言を足しています。探す対象は claude -p の出力だけでなく、中で呼んだ2本目が書き出したファイルも含めます。そのファイルは、今回の実行で書かれたもの(開始時に置いた目印より新しいもの)だけに絞っています。
この段は AI に任せていません。AI が壊れているときほど、AI はそれを報告できないからです。2本目の認証が切れていても、1本目は「作業を終えました」と報告してきました。見張りは、AI の自己申告ではなく、シェルが決まった文字を探す形にしています。
ほかに、次の3つも同じスクリプトに入れています。
- 最後の1行を記録する:claude -p の出力の最後の空でない1行を、日ごとの実行記録に残す。空なら「報告なし」と書く。出力が空のまま exit 0 で終わった回が、記録を見ればすぐ分かる
- 生存確認のファイル:ジョブごとに、最後に走った時刻と終了コードを小さな JSON に書く。止まったジョブは時刻が古いまま残る
- 通知先を2つに分ける:普段の報告(1日十数本)と、異常だけを流すチャンネルを分ける。異常のチャンネルは普段は鳴らないので、鳴ったら異常だと分かる

cron・launchd で組める最小の見張り
私たちの作りを、1本の claude -p を見張る最小の形にしました。終了コード・JSON の中身・ログの文字列の3段を順に確かめ、1つでも当たれば通知します。jq(JSON を読むコマンド)を使います。
#!/bin/bash
# 毎朝の claude -p を包む見張り(例)
OUT="$HOME/logs/daily-$(date +%F).json"
ERR="$HOME/logs/daily-$(date +%F).err"
claude -p "今日の作業の指示" \
--model claude-sonnet-5-5 \
--output-format json \
< /dev/null > "$OUT" 2> "$ERR"
RC=$?
ALERT=""
# 1段目:終了コード
[ "$RC" -ne 0 ] && ALERT="exit=$RC"
# 2段目:JSON の中身(subtype ではなく is_error を見る)
IS_ERR=$(jq -r '.is_error' "$OUT" 2>/dev/null)
RESULT=$(jq -r '.result // empty' "$OUT" 2>/dev/null)
[ "$IS_ERR" != "false" ] && ALERT="${ALERT:+$ALERT / }is_error=${IS_ERR:-JSONなし}"
[ -z "$RESULT" ] && ALERT="${ALERT:+$ALERT / }result が空"
# 3段目:ログの文字列(stdout と stderr の両方を見る)
if grep -qE 'Failed to authenticate|Not logged in|OAuth session expired|could not be refreshed' "$OUT" "$ERR"; then
ALERT="${ALERT:+$ALERT / }認証エラーの文字あり"
fi
if [ -n "$ALERT" ]; then
osascript -e "display notification \"$ALERT\" with title \"claude -p の異常\""
fi
exit "$RC"ポイントは4つです。
- stdout と stderr を別のファイルに分け、grep は両方にかける。認証の失敗は stdout、知らないオプションは stderr に出るためです
- JSON が読めなかったときも異常として扱う。起動の失敗で何も出なかった場合(2.1.313 まで)は、
is_errorが取れず空になり、ここで止まります - 通知は差し替えやすい1か所にまとめる。例では Mac の通知を出していますが、Slack などへ送るならこの行を変えるだけです
- 中で別の claude -p を呼ぶ作りなら、その出力ファイルも grep の対象に入れる。外側の終了コードには表れないためです
launchd で回すときは、ログイン時の環境変数がそのまま引き継がれない点にも注意します。PATH や、ログイン用のトークンを入れた環境変数は、スクリプトの先頭で読み直しておくと、私たちのような「子のプロセスに届いていなかった」を避けられます。launchd での定期実行の組み方は、こちらで詳しく書いています。
無人の実行が止まる別の型(設定が更新のたびに落ちる、外部ツールを待って止まる、メモリで止まる)は、こちらにまとめています。
よくある質問
Q1. 2.1.314 以上に上げれば、終了コードだけ見ていれば十分ですか
十分ではありません。2.1.314 で直ったのは「起動の失敗が exit 0 で何も出ない」ことです。中で呼んだ別の claude -p の失敗、空の答え、MCP サーバーの読み込み漏れ、断られた操作は、最後まで動いた以上 exit 0 のままです。JSON の is_error と、ログの文字列も合わせて見ます。
Q2. JSON の subtype で成否を判定してはいけないのですか
私の実測(2.1.296)では、認証の失敗と存在しないモデルのどちらも subtype が "success" で、is_error が true でした。subtype が "error_max_turns" になったのは回数の上限に届いたときだけです。成否は is_error で判定し、理由を知りたいときに terminal_reason や api_error_status を見るのが確実です。版によって変わる可能性があるので、上げたときは同じ失敗を1回作って確かめてください。
Q3. 失敗の種類ごとに終了コードの番号は決まっていますか
公式ドキュメントに書かれているのは「成功は0、失敗は0以外」「SIGTERM で止めたら143」までです。私の環境では、認証・モデル・オプション・回数の上限の失敗はどれも1でしたが、番号の対応表は公開されていません。スクリプトでは0かどうかだけで分け、理由は JSON で見ます。
Q4. 手元の Claude Code の版はどう確かめますか
claude --version で表示されます。自動更新を止めて固定している場合は、cron や launchd が呼んでいる実行ファイルの版も同じかを確かめてください。PATH の違いで、対話で使っているものとは別の実行ファイルが呼ばれていることがあります。
Q5. 通知が多すぎて見なくなりそうです
普段の報告と異常の通知を、別の通知先に分けるのが効きました。私たちは普段の報告を1日十数本流していますが、異常のチャンネルは平常時は鳴りません。鳴ったら必ず見る、という使い方ができます。
まとめ
- claude -p の終了コードは、成功で0、実行の失敗で0以外。私の 2.1.296 の実測では、認証・存在しないモデル・知らないオプション・回数の上限はどれも1だった
- 認証の失敗は stderr ではなく stdout に出る。JSON では
subtypeが "success" のままで、is_errorが true になる - 2.1.314(2026年10月30日)で、起動の失敗が exit 0・出力なしから exit 1 に直った。10月31日時点で stable は 2.1.309 で、まだ届いていない
- 中で呼んだ別の claude -p の失敗・空の答え・MCP の読み込み漏れ・断られた操作は、どの版でも exit 0 のまま
- 見張りは、終了コード・JSON の中身・ログの文字列の3段で組み、AI の自己申告ではなくシェルに見させる
私たちは、認証切れが8日間見過ごされたことで、終了コードを信じすぎていたと気づきました。株式会社Fyveでは、こうした無人運用の作りを自社の Mac mini で毎日回しながら直しています。claude -p を定期実行している方は、まずわざと1回失敗させて、自分の見張りが鳴るかを確かめてみてください。
Claude Codeを「素のまま」使うな

設定で差がつく——CLAUDE.md・権限・スキルの実物を公開(全24ページ)
素のClaude Codeは"優秀な新入社員"。仕事を教えるほど、自分専用になります。覚えさせる4点セット——会社の説明書(CLAUDE.md)・権限の柵・手順書(スキル)・フォルダの地図——を、1人会社の実運用からコピペで使える型つきで公開します。
- そのまま書き換えて使えるCLAUDE.mdの型
- お金と送信をAIに触らせない「3段階の柵」
- 1回教えたら何度でも動く、手順書のコピペ雛形
- AIが迷子にならないフォルダ構造の3原則
メールアドレス登録で他にも様々な資料を閲覧できます








+10PDF 19点・合計499ページ + すぐ使えるzip素材 3点
どれも登録後の受け取りページから、まとめてダウンロードできます。
毎週配信の無料ニュースレター「AIネイティブ超研究」の購読特典です。メール登録後すぐ、受け取りページのご案内が届きます。そこにはこの資料に加えて、過去の特典もすべてまとめて置いてあります。あわせて、AI活用に関するお知らせやお役に立てそうなご案内をお送りすることがあります。解除はいつでも1クリック。
御社の業務に合わせたClaude Code導入支援
「AIツールを導入したが、現場で使われない」を終わらせる。
業務課題のヒアリングから設計、ハンズオン実践、運用定着まで一貫して支援します。