「role system must precede」400エラーをClaude Codeで切り分ける
400「role 'system' must precede an 'assistant' message」の意味と、v2.1.280以降も残る場合の切り分けです。プロキシやゲートウェイが挟んだsystemメッセージを疑う手順を示します。
API Error: 400 messages.6: role 'system' must precede an 'assistant' message or end the array; ... と出て、同じ会話が毎ターン失敗する。これは、会話の途中に置かれたsystemメッセージの位置をAPIが拒否した表示です。Claude Code自身が送った分ならv2.1.280以降で自動回復します。それでも出続けるなら、間に挟まるプロキシかLLMゲートウェイが自前のsystemメッセージを足している可能性が高くなります。
このエラーは何を拒否されているのか
APIが拒否したのは、messages 配列の途中に置かれた role: "system" のメッセージです。位置が許される場所から外れていました。
Claude Codeは、リマインダーや添付テキストの一部を、会話の中のsystemメッセージとして送ります。トップレベルの system フィールドを書き換えると、プロンプトキャッシュの先頭側が崩れるためです。履歴の末尾側に足せば、それまでのキャッシュはそのまま使えます。
この仕組みと位置の規則は、mid-conversation system messageの配置制約で表にしています。要点は、中身のあるsystemメッセージを置けるのが「user ターンの直後で、次が assistant か配列の末尾」という場所だけという点です。先頭や、tool_use と対応する tool_result の間は拒否されます。
エラー文の messages.6 は、messages 配列のうち拒否された要素を指す番号です。この番号を、あとで述べる切り分けで使います。
並びで見ると、拒否される形と通る形の違いが分かりやすくなります。左端の数字が messages の番号で、ここでは番号6が system です。
通る例 拒否される例
4 user 4 user
5 assistant 5 assistant
6 user 6 system <- 直前がassistant、直後がuser
7 system 7 user
8 assistant 8 assistant左の例では、番号7のsystemは user の直後にあり、次が assistant なので規則に合います。右の例の番号6は、次が user です。「assistant の前に来るか、配列の末尾で終わるか」のどちらにも当たらないため、must precede an 'assistant' message or end the array と拒否されます。エラー文は、この「どちらかでなければならない」という規則をそのまま述べています。
Claude Codeが送った分は自動で回復する
errorsページによると、APIが位置を拒否したとき、Claude Codeはそのテキストを通常のuserメッセージに変えて、リクエストを1回だけ再送します。次のような別の言い回しの拒否も、同じ回復の対象です。
use the top-level 'system' parameter for the initial system promptつまり、最新のClaude Codeを直接APIにつないでいる限り、この400は利用者の画面にほとんど出ません。出たとしたら、次のどちらかです。
エラーが画面に出る2つの経路
古いバージョン
この文言を認識できず、Claude Code自身が送ったsystemメッセージでも回復しません。会話の以降のターンが、すべて同じ形で失敗します。
経路上の第三者
拒否されたsystemメッセージがClaude Codeの送った分ではないため、Claude Codeには取り除けません。プロキシやゲートウェイが足した分が典型です。
まずバージョンを確かめる
最初に確認するのはバージョンです。v2.1.280より前なら、原因がプロキシであっても、まず更新して再度試す価値があります。
claude --version
claude update更新して会話を再開しても同じ400が出るなら、Claude Code自身の分は回復されている前提で、次へ進みます。
プロキシかゲートウェイが原因かを切り分ける
errorsページは、この状態を「refused system message isn't one Claude Code can remove」と説明し、間に挟まるプロキシかLLMゲートウェイが自前のsystemメッセージを足している可能性が高いとしています。切り分けは3段階です。
経路を疑うときの切り分け
- 1
ANTHROPIC_BASE_URLが設定されているか見る
値が空でなければ、そのシェルで起動したClaude Codeはプロキシやゲートウェイを通っています。
- 2
プロキシを外して同じ操作を試す
ANTHROPIC_BASE_URLを外した状態で起動し、エラーが消えるかを確かめます。消えれば、原因は経路上にあります。 - 3
新しい会話でも出るか見る
/clearで新しい会話にしても出るなら、原因は保存された会話ではなく、リクエストの経路にあります。
1段目の確認は、次のコマンドで足ります。
echo "${ANTHROPIC_BASE_URL:-未設定}"2段目は、環境変数を外して起動するだけです。
env -u ANTHROPIC_BASE_URL claudeこの方法は、変数をシェルで設定している場合に効きます。settings.json の env ブロックで値を入れている場合は、設定側を一時的に外す必要があります。管理者がマネージド設定で allowedProviders に customEndpoint だけを許可している環境では、ゲートウェイ以外に向けたセッションをClaude Codeが拒否します(v2.1.285以降)。その場合は直接つないで試せないので、運営者に再現を依頼する流れになります。
ANTHROPIC_BASE_URL の役割は、ANTHROPIC_BASE_URLの設定で扱っています。
ゲートウェイ側で何を見ればよいか
プロキシの運営者に渡す材料があると、報告が早く済みます。リクエスト本文を記録できる環境なら、messages の role の並びを番号つきで出せます。
jq -r '.messages | to_entries[] | "\(.key) \(.value.role)"' request.json出力のうち、エラー文にある番号(上の例なら6)の行が system です。その直前の行が user でない、または次の行が user(tool_result を含む)なら、位置の規則に当たります。ゲートウェイが自前のsystemメッセージを足したのか、Claude Codeが送った分の位置をずらしたのかは、元のリクエストとの差分で判別できます。
運営者側で見直す点は、LLMゲートウェイのプロトコルを扱うページにも載っています。
systemの配列は、受け取ったとおりに転送します。先頭に別のsystemブロックを足す、順序を入れ替える、1本の文字列にまとめるといった操作は、属性ブロックの除去を無効にします- 属性ブロックは配列の独立した1要素のまま残します。別のsystemテキストを混ぜると、混ぜた分まで属性として捨てられ、システムプロンプトの残りも消えます
- ゲートウェイが
systemの内容を整形する必要があるなら、クライアント側でCLAUDE_CODE_ATTRIBUTION_HEADER=0を設定し、ブロックを出さない運用が案内されています - プロンプトキャッシュを使うなら、
role: "system"の途中追加メッセージを含め、cache_controlはそのまま転送します。ブロック形式のコンテンツを文字列に変換しません
最後の項目(cache_control の転送)は、このエラーそのものではなくキャッシュの話です。ただし、systemメッセージに手を入れるゲートウェイが踏みやすい別の落とし穴として並びます。症状はエラーではなく、毎ターン入力トークンが未キャッシュ扱いで課金される形で現れます。
プロトコル全体の考え方は、LLMゲートウェイのプロトコル解説が詳しいです。
繰り返す場合の対処と報告
経路が原因と分かったら、次の順で動きます。
- ゲートウェイの運営者に、エラー文の全文とバージョン、再現したタイミングを渡して報告する
- 報告の返事を待つ間は、
/clearで新しい会話を始める。ゲートウェイが毎ターン同じ位置にsystemメッセージを足す構成なら再発するが、履歴が原因の場合は直る - 直接つなげる環境があるなら、その間だけ
ANTHROPIC_BASE_URLを外して作業する
ゲートウェイを経由する会話では、上流がsystemメッセージ自体を拒否した場合の動きも別にあります。上流が会話途中のsystemメッセージ、またはそのメッセージの cache_control マーカーを拒否すると、Claude Codeは再試行し、その会話の残りの間はその機能を無効にします。つまり、上流がmid-conversation system messageに対応していないだけの構成では、会話は最終的に進みます。毎ターン失敗し続けるのは、位置の拒否が、Claude Codeの取り除けないsystemメッセージに対して起きている場合です。
似た400系エラーとの見分け方
プロキシ経由の400は、文言ごとに原因と回復の形が違います。Unsupported tool content removed を含む近い項目との見比べは、Unsupported tool content removedの解説の切り分け表にまとめています。履歴の食い違いで出る別の400は、thinking blockの食い違いを直す手順が受け持ちます。
本記事のエラーを見分ける手がかりは、文言に role 'system' と must precede が両方入っている点と、メッセージ番号が付いている点です。
errorsページの「エラー文言から探す」一覧には、この項目が role 'system' must precede an 'assistant' message の形で載っています。実際の表示は末尾に or end the array; ... と続くため、一覧を文字列検索するときは前半だけで探すと見つかります。エラー文の中の番号は会話ごとに変わるので、検索語には番号を含めません。
設定を疑う順番の早見表
| 状況 | 疑う場所 | 最初の一手 |
|---|---|---|
| v2.1.279以前で出る | 疑う場所Claude Code自身のsystemメッセージ | 最初の一手claude update |
| v2.1.280以降、ゲートウェイなしで出る | 疑う場所経路上の別の仕組み(社内プロキシなど) | 最初の一手経路を確認し、運営者に報告 |
| v2.1.280以降、ゲートウェイ経由でのみ出る | 疑う場所ゲートウェイが足したsystemメッセージ | 最初の一手プロキシを外して再現を比べる |
/clear 後は消えるがまた出る | 疑う場所履歴ではなく、毎ターンの追加処理 | 最初の一手リクエスト本文の role 並びを記録する |