
- claude -p --output-format jsonはstdout末尾に無関係なログが混ざり、Extra dataで失敗することがある
- json.loads()は末尾の余分な文字列に弱く、raw_decode()なら先頭のJSONだけ安全に取り出せる
- 成功時はresult、失敗時はerrors→subtype等の順にフォールバックするとメッセージを取りこぼさない
claude -p --output-format jsonの出力をjson.loads()に渡すと、json.decoder.JSONDecodeError: Extra data: line 1 column X (char Y)という例外で落ちることがある。
ターミナルに表示された文字列を目で追う分には、中身は正しいJSONにしか見えない。だがjson.loads()はそうは判定してくれない。
個人開発で運用している自動化ツールからclaude -pを定期的に呼び出しているが、そこでもこの失敗を何度か踏んだ。原因は1箇所、直し方も1行で済む。
直し方: json.loads()をraw_decode()に置き換える
--output-format jsonは、stdoutにJSON本体を書き出したあと、その後ろへ無関係な文字列を続けて書き出すことがある。json.loads()は文字列全体を1つのJSON値として厳密に読む関数なので、後ろに1文字でも余分な文字が残っていると、そこでExtra dataエラーを出して止まる。
対処は、json.loads()をやめてjson.JSONDecoder().raw_decode()に置き換えるだけである。
import json
# NG: 末尾に余分な文字列が残っていると Extra data で落ちる
data = json.loads(stdout)
# OK: 先頭から読めるところまでだけを読む
decoder = json.JSONDecoder()
data, end_index = decoder.raw_decode(stdout)
# stdout[end_index:] に何が残っていても無視できる
json.loads()は「文字列全体を読んでJSONでなければ失敗」という向きの関数である。raw_decode()はその逆で、「先頭から読めるところまで読み、読み終えた位置(インデックス)も返す」という向きの関数になる。末尾に何が付いていても、先頭のJSON値さえ正しければ構わず取り出せる。
原因: MCPの内部ログがJSON本体の後ろに付く
無関係な文字列の正体は、MCP (Model Context Protocol、Claude Codeが外部ツールと連携するための仕組み)が出す内部ログである。
最初に踏んだのは2026年8月14日、画像を生成する処理の被写体生成呼び出しでのことだった。この時点では約3分の1の呼び出しがこの理由で失敗しており、原因の文字列そのものは特定できていなかった。「JSON本体の後ろに何か(原因未特定)が続く」とだけ記録し、ひとまずraw_decode()に切り替えて凌いだ。
正体がわかったのは2026年8月18日、WebSearchツールを有効にした呼び出しを調べたときである。stdoutの末尾に、次の一文がそのまま付いていた。
Client.listTools() called but server does not advertise tools capability - returning empty list
MCPサーバーがtools機能を広告していないのに一覧を要求された、という趣旨の内部ログで、WebSearchの有無を問わず起きる。8月14日の「原因未特定」だった混入も、同じ種類のログだったとみてよい。
応用: 失敗時はresultではなくerrorsを見る
raw_decode()で本体を取り出せても、claude -pの応答はもう一段複雑である。成功時と失敗時でJSONの形が違う。
成功時はresultフィールドに本文がそのまま入る。失敗時(--max-budget-usdの予算上限を意図的に超過させて実測)はresultが存在しないことがあり、代わりにerrorsという配列にメッセージが入る。
message_parts = []
result_field = data.get("result")
if isinstance(result_field, str) and result_field:
message_parts.append(result_field)
errors_field = data.get("errors")
if isinstance(errors_field, list) and errors_field:
message_parts.extend(str(e) for e in errors_field)
if not message_parts:
for key in ("subtype", "terminal_reason", "api_error_status"):
value = data.get(key)
if value:
message_parts.append(f"{key}={value}")
result→errors→subtype/terminal_reason/api_error_statusの順に見ていくと、失敗理由を取りこぼしにくくなる。この並びは今のところ実際に観測できた形に基づくもので、利用上限(週間/月間)に達したときの応答がどちらの形で返るかは、正直まだ両方とも実測できていない。
もう一つの流儀: 1回で諦めて例外を投げる設計
同じリポジトリの中に、raw_decode()を使いながら中身の設計が違う実装がもう一つある。短い文章を1回だけ生成させる、別の軽量な呼び出し処理である。
こちらは失敗を検知したら即座に例外を投げ、リトライはしない。claude -pはHTTPステータスのような明確な失敗コードを返さず、is_errorと自然文の組み合わせでしか失敗を伝えてこない。「待てば直る失敗」と「何度やっても同じ失敗」を機械的に見分けられない以上、下手にリトライすると、数時間戻らないセッション上限に当たった場合はその待ち時間をまるごと無駄にする。1回で諦めて理由ごと外に出すほうが、この用途では安全という判断である。
一方、複数の候補から本命を選んで長めの文章を組み立てる別の処理で使う_parse_claude_json_output()は例外を投げず、(text, failure_kind, detail)というタプルを返す。呼び出し元が「モデルを格下げして再試行する」という判断を必要とするためで、失敗を握りつぶさずに一段上へ運ぶ責務を、例外ではなく戻り値の側に持たせている。
同じraw_decode()という道具でも、呼び出し元が何を必要とするかで設計は分かれる。
教訓: 自然文のgrepから構造化パースへ
このraw_decode()への切り替えは、--output-format jsonを使い始めた当初からの設計ではなかった。以前はstdoutとstderrを素の自然文として扱い、"spend limit"のような部分文字列が含まれているかどうかで利用上限の発生を判定していた。
この判定は、Anthropic側が失敗メッセージの文言を変えるたびに壊れた。2026年8月14日には"monthly spend limit"という文言で判定できていたが、8月17日には"weekly limit"という別の文言に変わり、判定漏れが発生した。
自然文のgrepは、相手(この場合はAnthropicのCLI)が文言を変えないという前提の上にしか成立しない。構造化されたJSONを正しくパースし、is_errorやapi_error_statusのようなフィールド名で判定する設計に切り替えたのは、この前提が崩れたあとの対処である。
この自動化ツールも、_parse_claude_json_output()を通した応答をもとに後続の処理を進めている。目で見れば正しいJSONにしか見えないものが、機械的なパースだけを裏切る。人が結果を確認しながら使うときには気づかれず、完全に自動で走らせているときにだけ表面化する壊れ方である。利用上限到達時の応答がresult側に来るかerrors側に来るかは、次にこの経路で実際に踏んだときに追記する。
よくある質問
json.decoder.JSONDecodeError: Extra dataはなぜ 起きますか?
claude -p --output-format jsonのstdoutが、正しいJSON本体を書き出したあとにMCP関連の内部ログなど無関係な文字列を続けて出力することがあるためです。json.loads()は文字列全体を1つのJSONとして厳密に読むため、後ろに余分な文字が残っていると失敗します。
json.loads()と json.JSONDecoder().raw_decode()は何が 違いますか?
json.loads()は文字列全体が1つのJSONであることを前提に読みます。raw_decode()は文字列の先頭から読めるところまでを読み、読み終えた位置も返すため、末尾に余分な文字列が付いていても影響を受けません。
他の claude -p呼び出しでも同じ 対処で 直りますか?
--output-format jsonを使う呼び出しであれば、多くの場合同じ対処で直ります。ただし失敗時の応答はresultではなくerrorsフィールドに入ることがあるため、resultが空でもerrorsやsubtype等を順に確認するフォールバックを合わせて実装すると取りこぼしが減ります。




