Claude Code を CI/cron で claude -p 自動化している人向け
CI や cron で claude -p を毎日自動実行していて、主モデル(Opus など)が混雑にぶつかった時にジョブを落とさず別モデルで続行させたい場面で、起動コマンドの後ろに書き足す
Claude Code の --fallback-model は、CIやcronで claude -p を叩いて自動化している時、主モデルが「混雑してます」エラーを返した瞬間に、別モデルへ自動で切替えて続行させるための起動時の追加指定です。print mode 専用で、対話モードに付けても効きません。
毎朝5時のCIで Opus に記事下書きを書かせている時、Opus 側の混雑にぶつかって job が落ちると、その日の運用が止まる。そこで「Opus がダメな時は Sonnet で続けて」と先回りで宣言しておく場所がこれです。
噛み砕くと
主モデルへの依頼が、Anthropic 側の混雑で受付不可になった時の「代替宛先」を先に登録しておくのに近いです。指定された配達員が今日いっぱいパンク中なら、別の配達員に荷物を回す宅配の仕組みみたいなもの。
違うのは、料金が配達員ごとに違うところです。Opus から Haiku に切り替わると、品質はガクッと落ちる代わりに料金もガクッと下がる。Opus から Sonnet への切替なら、品質はそこまで落ちずに料金は半額くらいに収まる。なので、ただ「動けばOK」では決め切れず、「主モデルが落ちたとき、何にスライドさせるか」を業務ごとに考える話になります。
大事な前提:この指定は claude -p でしか効かない
公式仕様では「print mode only」と明記されています。普通の対話モードに付けても、無視されるか拒否されます。CI/cron の自動化スクリプトでだけ意味がある指定です。
もう1点。切替えが発動する条件は overloaded エラーのみ。「rate limit」エラーは別物で、こちらにぶつかった時は fallback は発動せずそのまま落ちます。ここを「どんな失敗でもフォールバックしてくれる安全装置」と勘違いすると、深夜に rate limit でジョブが落ちて朝に気付く事故になります。
「料理ブログの記事下書きを毎朝5時にCIで生成する」を例に、実際の手順を見る
料理ブログの記事下書きを毎朝5時にCIで作っているとします。主モデルは Opus で、混雑時の代替を Sonnet にしておきたい、という前提で組み立てます。
ステップ1: 主モデルだけで動かしているスクリプトを用意する
まず、いまの fallback なしの状態を整理します。CI上の cron が毎朝5時に動かしているスクリプトの中身がこうだと仮定します。
$ claude -p --model opus "今日の旬の食材を1つ選んで、レシピ記事の下書きを書いて"
このまま動かすと、Opus が混雑していた朝は、エラーで何も出力されずに終わります。下書きが0件の朝が出てくる。
ステップ2: --fallback-model sonnet を追加する
同じスクリプトに1つ追加するだけで、Opus が混雑した時だけ Sonnet にスライドして処理が続行されるようになります。
$ claude -p --model opus --fallback-model sonnet "今日の旬の食材を1つ選んで、レシピ記事の下書きを書いて"
Anthropic 側に余裕がある日は Opus が普通に答えます。Opus が混雑して overloaded エラーが返ってきた日だけ、内部的に Sonnet へ切替えて再試行し、Sonnet が答えを返します。CI から見ると「とにかく下書きが出てきた」状態になり、blog 自動投稿の続きの処理が止まらずに進みます。
ステップ3: 切替が発生したかを stream-json で機械的に追う
ここで初心者がやりがちな勘違いを1個挟みます。「fallback が起きたかは、本文の中に何か書いてあるはず」と思い込むパターン。実際には本文(普通の応答テキスト)には書かれません。どのモデルが返したかを後から判定したいなら、出力フォーマットそのものを切り替える必要があります。
fallback が起きたかを後から追いたいなら、--output-format stream-json --verbose をセットで使うのが確実です。各イベントが JSON 行で流れてくるので、どのモデルが実際に応答したかをプログラムから機械的に拾えます。
$ claude -p \
--model opus \
--fallback-model sonnet \
--output-format stream-json \
--verbose \
"今日の旬の食材を1つ選んで、レシピ記事の下書きを書いて" \
> /tmp/claude-events.jsonl
あとは /tmp/claude-events.jsonl を1行ずつ JSON として読み、応答イベントに乗っている model の値が opus 系か sonnet 系かを見れば、その回が fallback を踏んだかどうかが判定できます。料金は Opus と Sonnet で違うので、fallback が連発した月は請求書が想定と微妙にズレます。この判定ログを残しておくと、月末に「Opus 100% 想定だったけど実は20回 Sonnet に落ちてた」みたいなことが見えて、来月の予算組みに反映できます。
ステップ4: バージョン固定したい場合はエイリアスではなくフルIDを書く
受け付ける値は、sonnet haiku opus のエイリアスか、claude-sonnet-4-6 のようなフルモデルIDのどちらか。エイリアスで書いておくと、Anthropic 側で sonnet の指す中身が新世代に切り替わった時に自動で追随します。
逆に「下書きの文体が突然変わると困る、いまの sonnet 世代に固定したい」みたいなブログ運営の都合があるなら、フルIDで書きます。
$ claude -p \
--model opus \
--fallback-model claude-sonnet-4-6 \
"今日の旬の食材を1つ選んで、レシピ記事の下書きを書いて"
ステップ5: 安全側に倒すなら fallback 先を Haiku にする
料金事故を絶対起こしたくないなら、fallback 先を Haiku にしておく手もあります。Opus が落ちた日は Haiku に降りる分、品質はかなり落ちますが、「とにかく落とさない、品質が下がったらその日は人間が見て直す」運用は組めます。
$ claude -p --model opus --fallback-model haiku "..."
つまり --fallback-model は何をしてくれるのか
- やってくれる: 主モデルが overloaded(混雑)エラーを返した時、指定したモデルに自動で切替えてリクエストを再試行する
- やってくれる:
--output-format stream-json --verboseと組み合わせれば、どのモデルが実際に応答したかをJSONイベントから機械的に判定できる - やってくれない: rate limit(利用枠超過)エラーでの自動切替。混雑とは別物として扱われ、こちらは普通に失敗で終わる
- やってくれない: 対話モードでの切替。print mode 専用なので無視される
- やってくれない: 複数モデルのチェーン切替、つまり Opus→Sonnet→Haiku のように何段も予備を並べる形。fallback 先は1つだけ指定可能で、カンマ区切り等は使えない
- 気を付けるところ: 切替先のモデルは料金が違うので、fallback が頻発した月は請求書が想定とズレる。
stream-jsonで各回のモデルを記録しておくと、月末に発火回数を把握できる - 意味が薄い場面: そもそも対話で1〜2回叩くだけの個人用途。混雑時は手元でリトライすればいいので、わざわざ書き足す価値が薄い
使いどころ3シナリオ(具体題材で再現)
シナリオ1: 料理ブログの記事下書きを毎朝5時にCIで生成する
cron で毎朝5時に claude -p --model opus "..." を走らせて、その日の旬食材のレシピ記事下書きを Markdown でファイル保存場所に push する運用。Opus が朝の時間帯に混雑することがあり、月に2〜3回は下書きが0件になる事故が出ます。
ここに --fallback-model sonnet を1つ足すだけで、Opus が落ちた朝は Sonnet が代打で書いてくれます。文体がやや変わるので、--output-format stream-json --verbose を併用して JSONイベントから「今日はSonnet が返した」と判定できたら Slack に通知して、「今日は Sonnet 版だから後でレビュー強めに」みたいな運用に繋げます。
シナリオ2: 過去記事500本をまとめて要約し直す大量バッチ処理
WordPress に貯まった過去記事500本を一気に Claude に流して、各記事の要約を生成し直すバッチ。1本ずつ claude -p --output-format json で叩いて結果を保存する形だと、500回のうち何回かは混雑にぶつかるのが普通です。
バッチを途中で止めたくないなら、--fallback-model sonnet を全リクエストに書き足しておくと、混雑にぶつかった分だけ Sonnet で消化されて、最後まで走り切ります。Sonnet 行と Opus 行が混ざるので、保存先のJSONに「どのモデルが返したか」フィールドを残しておくと、後で品質チェックの優先順位を付けやすい。
シナリオ3: 本番アプリの裏で Claude を呼び出している API サーバー
ユーザーからのチャット問い合わせを裏で claude -p に投げて返している本番サービス。ピーク時間帯に Opus が混雑するとユーザー側で「応答が来ない」体験になります。
fallback を Sonnet にしておくと、混雑時間帯のユーザーには「やや精度が下がったが応答は来る」という体験を返せます。SLA を守りたいケースで効きます。逆に「混雑時はあえてエラーを返して、ユーザーに後でリトライしてもらう」という設計を取るならこの指定は付けません。SLA重視 vs 品質重視で判断が分かれる場面です。
初心者が踏みやすい落とし穴
- 対話モードに付けても効かない。print mode つまり
claude -p形式専用。普通にclaudeだけで起動した対話セッションで--fallback-modelを書いても、無視されるか拒否される - rate limit エラーでは切り替わらない。発動条件は overloaded(混雑)のみ。利用枠超過にぶつかった時はそのまま落ちる。「どんなエラーでもフォールバックする保険」と思い込むと、深夜にジョブが落ちて朝気付くことになる
- 料金が変わる事実を運用に組み込まないと請求書で詰む。Opus→Sonnet で半額くらい、Opus→Haiku でさらに大きく下がる。fallback が連発した月は予算とズレるので、
--output-format stream-json --verboseで各回のモデルをログに残し、月末に発火回数を確認する習慣を作る - 主モデルを
--modelで明示していないと、何から何に切り替わったか分からなくなる。主モデルは--model指定 or 設定 or デフォルトで決まる。fallback の話をする前に、まず主モデルが何かをスクリプト上に書いておくのが安全 - チェーンはできない。Opus→Sonnet→Haiku のように複数段の予備を並べることはできない。fallback 先は1つだけ。Sonnet も同時に混雑していたら、その時は落ちる
- エイリアス指定は世代が動く前提。
sonnetで書いておくと、Anthropic 側で sonnet の指す中身が新世代に切り替わった時に自動追随する。文体や挙動を固定したいならフルモデルID、たとえばclaude-sonnet-4-6で書く - fallback が起きたかは普通の応答からは判定できない。本文側を眺めても、どのモデルが返したかは見えない。判定したいなら
--output-format stream-json --verboseをセットで使い、各イベントのJSONからmodel値を拾うのが確実 - 「fallback さえ付けておけば安全」ではない。混雑が長引く時間帯はそもそも fallback 先も混んでいる。本番運用では fallback と合わせて、リトライ回数の上限と、最終的に失敗した時の通知(Slack/メール)まで組んでおく
書き方
claude -p --fallback-model <モデル名> "依頼内容"
# --model と併用してフォールバック先を明示
claude -p --model opus --fallback-model sonnet "依頼内容"
# フルモデルIDでバージョン固定
claude -p --model opus --fallback-model claude-sonnet-4-6 "依頼内容"
# どのモデルが実際に応答したかを機械的に判定したい場合
claude -p --model opus --fallback-model sonnet --output-format stream-json --verbose "依頼内容"
# 受け付ける値: sonnet / haiku / opus のエイリアス、または claude-sonnet-4-6 等のフルモデルID
やってみるとこうなる
入力
$ claude -p --model opus --fallback-model sonnet "今日の旬の食材を1つ選んで、レシピ記事の下書きを書いて"
出力例
# Opus が応答できた場合 → 通常の応答テキストが返ってくる(Opus が書いた下書き)
# Opus が overloaded(混雑)で返せなかった場合 → 内部で Sonnet に切替えて再試行され、Sonnet が書いた下書きが返ってくる
# どちらが応答したかを機械的に判定したい場合は --output-format stream-json --verbose を併用し、各イベントの JSON 行に乗っている model 値を見る
# rate limit(利用枠超過)の場合 → fallback は発動せず、そのままエラー終了
このページに出てきた言葉
- print mode
- <code>claude -p "依頼内容"</code> のように対話画面を開かず、依頼を一発渡して結果だけ標準出力に返す呼び方。CIやcronで定型処理を組む時はほぼこれを使う
- overloaded
- Anthropic 側のサーバーが一時的に混雑していて、リクエストを受け付け切れない状態。数分待てば直ることが多い
- rate limit
- 契約プランごとに決まっている「1分あたり/1日あたりの利用上限」に到達した状態。混雑とは別で、自分の利用枠が回復するまでは解消しない
- Opus / Sonnet / Haiku
- Anthropic が出している3つのモデル名。Opus が一番賢くて高い、Haiku が一番安くて速い、Sonnet がその中間。同じ依頼でも結果の精度と料金が変わる
- stream-json
- <code>--output-format</code> の値の1つ。やり取りを1行=1イベントの JSON で逐次出してくれるので、プログラムから処理しやすい
- verbose
- 詳細出力モード。<code>stream-json</code> と組み合わせると、応答だけでなく各イベントが JSON 行で流れてくる
- エイリアス
- 正式名の別名。ここでは <code>sonnet</code> のように、世代が変わっても自動で最新版を指す短い呼び名
- CI
- ソースコードを更新したり時刻になったりした時に、サーバー上で自動でスクリプトを走らせる仕組み。GitHub Actions、GitLab CI などが代表例
- cron
- パソコンやサーバーに「毎日5時にこのコマンドを実行」と予約しておく機能