Claude Code SDK や標準入出力パイプ経由で stream-json をやりとりしている開発者向け
ブラウザのチャットUIや会話ロガー、マルチターン対話の自動テストなど、ユーザー発言とアシスタント応答を stdout 1本のストリームで扱いたい場面で、<code>--input-format stream-json</code> と <code>--output-format stream-json</code> の2つ(加えて stream-json 自体に <code>--print</code> が必要)と一緒にこのスイッチを足して起動する
Claude Code を SDK や標準入出力パイプ経由で動かしているとき、自分が送ったメッセージを stdout 側にも「user タイプのメッセージ」として戻してほしい場面があります。--replay-user-messages はそのための起動スイッチです。stdin から流し込んだ SDKUserMessage を、stdout の stream-json に同じ形式で再送してくれる。
このスイッチが固有に求める前提は --input-format stream-json と --output-format stream-json の2つ。両端 stream-json でないと初期化時点で蹴られます。なお、stream-json で入出力するにはそもそも --print(-p)が要りますが、これは --replay-user-messages に限らない stream-json モード全体の前提です。
噛み砕くと
普段の claude -p 起動は、stdin から問い合わせを受けて、stdout からアシスタント側の返事だけを流す「片側中継」の形になっています。質問は入口、答えは出口、別の口。
これだと、上流の UI(チャット画面)やロガーで「ユーザーが何を言って、それに対してアシスタントが何を返したか」を stdout 1本にまとめたい時、入口側を別管にして自分で同期させる手間が出る。
--replay-user-messages をつけると、入口に来たユーザーメッセージが出口にもそのままコピーされて、出口の1本に user と assistant の発言が並んで流れます。録画スタジオで言うと、出演者のマイク音声を返しで卓側にも戻して、ミキサー1台で全員の声を記録するイメージ。
必要な前提:--input-format stream-json と --output-format stream-json の2つ
このスイッチが固有に求める前提は2つです。公式ドキュメントの Requires 節に明示されています。
--input-format stream-jsonで stdin を stream-json として受ける--output-format stream-jsonで stdout を stream-json として返す
この2つが揃っていないとエラーで終了します。--output-format json や --output-format text と組み合わせるとエラー。入口側を --input-format text にしてもエラー。両端 stream-json でないと「再送」の意味がないので、Claude Code 側が初期化時点で拒否します。
なお、そもそも stream-json で入出力するには --print(-p)で印刷モードに入っている必要がありますが、これは --replay-user-messages に限らない stream-json モード全体の前提です。stream-json モードで起動している時点で --print は既に効いているはず。
「ブラウザのチャットUIに組み込む」を例に、実際の挙動を見る
Webサービスの裏側で Claude Code を回して、ブラウザ側のチャット画面にユーザー発言とアシスタント応答を両方表示するシナリオで動かしてみます。
ステップ1: まずは --replay-user-messages 無しで普通に起動
サーバー側プロセスが stdin に stream-json でユーザー発言を流し込みます。
$ claude -p --input-format stream-json --output-format stream-json
stdin に投げ込む JSON は1行で、こんな形。
< {"type":"user","message":{"role":"user","content":"世界の魚を紹介するサイトを作りたい"}}
すると stdout には次のような流れだけが返ってきます。> 印が stdout 側に流れた行。
> {"type":"system","subtype":"init",...}
> {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"分かりました。まず..."}]}}
> {"type":"result","subtype":"success",...}
stdout を見ると assistant の返事はあるけれど、ユーザー側が何を言ったかは出てこない。stdin 側を別途録音しないと対話が片側だけになります。
ステップ2: --replay-user-messages を足して再起動
$ claude -p --input-format stream-json --output-format stream-json --replay-user-messages
同じ stdin を流し込みます。
< {"type":"user","message":{"role":"user","content":"世界の魚を紹介するサイトを作りたい"}}
今度は stdout 側にこう流れる。
> {"type":"system","subtype":"init",...}
> {"type":"user","message":{"role":"user","content":"世界の魚を紹介するサイトを作りたい"}}
> {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"分かりました。まず..."}]}}
> {"type":"result","subtype":"success",...}
2行目に "type":"user" のメッセージが追加されているのが分かります。stdin から入ってきたユーザー発言が、そのまま stdout 側にもコピーされて戻ってきた状態。
なお、stdout に流れる順序は公式には明示されていません。上の例のように system → user → assistant の並びになることが多いものの、消費側は順序依存で処理せず、type フィールドで user / assistant / system / result を振り分けることを推奨します。
ステップ3: ブラウザ側で表示を組み立てる
サーバー側の Node.js / Python は、stdout の1本のストリームを type で振り分けて WebSocket で前段のブラウザに流すだけで済みます。
// 疑似コード(Node.js)
for await (const line of stdoutLines) {
const msg = JSON.parse(line);
if (msg.type === "user") browser.send({role: "user", text: msg.message.content});
if (msg.type === "assistant") browser.send({role: "assistant", text: extractText(msg)});
}
ブラウザ画面には「あなた: 世界の魚を紹介するサイトを作りたい」と「Claude: 分かりました。まず...」の吹き出しが並びます。順序の組み立ては stdout に流れてきた順番でそのまま追記すれば、ストリーム上の自然な並びでUIに反映されます。
ステップ4: 前提を崩すとどうなるか確認
ここで初心者がやりがちな勘違いがある。出力を読みやすくしたくて --output-format json に切り替えてみるパターン。
$ claude -p --input-format stream-json --output-format json --replay-user-messages
error: --replay-user-messages requires --output-format stream-json
初期化時点で蹴られる。入口側を --input-format text にしても同じくエラーです。両端 stream-json でないと、user メッセージを「同じ形式で」流し返せないのでそうなる。
ステップ5: --include-partial-messages と組み合わせる
アシスタント側の応答をリアルタイムに1文字ずつ流したいなら --include-partial-messages を足します。
$ claude -p --input-format stream-json --output-format stream-json \
--replay-user-messages --include-partial-messages
これで stdout には user メッセージに加えて、アシスタント応答の差分イベント(text_delta)が連発で流れ、最後に確定した assistant メッセージが来ます。ブラウザ側でタイピング演出を作りたい時の定番組み合わせ。
ステップ6: マルチターン対話で履歴の1本化を確認
同じセッションでユーザー側が連続して質問を投げる場合も、stdout 側に user メッセージが順番に出てくるので、サーバーは「ストリームを順番に保存するだけ」で会話ログが完成します。
> {"type":"user","message":{"content":"魚の図鑑ページから作りたい"}}
> {"type":"assistant","message":{...}}
> {"type":"user","message":{"content":"次は産地マップを"}}
> {"type":"assistant","message":{...}}
つまり --replay-user-messages は何をしてくれるのか
- やってくれる: stdin に流した SDKUserMessage を、stdout の stream-json に「user タイプ」として再送。会話ログを stdout 1本に集約できる
- やってくれない:
--output-format textや--output-format jsonでの再送、アシスタント側の差分配信、フックイベントの再送。差分配信は--include-partial-messages側、フックイベント再送は--include-hook-events側の役割 - 意味が薄い場面: 単発の質問1回だけで終わる用途、対話モードのインタラクティブ画面、ユーザー発言を画面に表示しない裏方バッチ処理
使いどころ3シナリオ(具体題材で再現)
シナリオ1: ブラウザのチャットUIに組み込むとき
「世界の魚を紹介するサイトを作りたい」みたいなユーザー発言を、画面の左側に「あなた」、右側に「Claude」と並べて表示したいケース。サーバー側で stdout を読みながらブラウザに WebSocket で流すだけで両方の発言を出せます。入口と出口を別管理する設計だと、片方が遅延した時に表示順がズレてバグるので、stdout 1本に集約できるメリットが大きい。
シナリオ2: 対話ログを長期保存したいとき
社内ナレッジ用に「ユーザーが何を聞いて、Claudeが何を答えたか」を JSON Lines ファイルに丸ごと貯めておく運用。--replay-user-messages 付きで起動して stdout を1本のファイルに tee で書き出せば、後から検索する時もファイル1本だけ見れば全部分かる。stdin 側を別途記録する仕組みを足す必要がない。
シナリオ3: マルチターン対話のテストを自動化したいとき
「魚の図鑑→産地マップ→料理レシピ」と順番に投げて、Claude が文脈を維持できているか自動テストするケース。テストランナーは stdout を読むだけで「自分が何を投げて、何が返ってきたか」を1ストリームで取得できるので、テストアサーションが書きやすい。--include-partial-messages を足すと差分まで拾えるので、応答の途中で詰まる回帰バグも検知できる。
初心者が踏みやすい落とし穴
- 必要な前提を欠くと初期化で蹴られる。
--input-format stream-jsonと--output-format stream-jsonの2つが揃っていないとエラーで終了。「出力だけ stream-json にしておけば大丈夫」と思って入口側を text のままにすると蹴られる。あわせて stream-json モード自体に--printが必要なので、対話モードで起動した時点で出番がない --output-format jsonとの組み合わせはエラー。1リクエスト1応答の json 形式は、途中で user メッセージを差し込む構造を持っていない。stream-json でしか動かない仕様--include-partial-messagesと混同しがち。あちらはアシスタント側の text_delta を流す機能、こちらはユーザー側の SDKUserMessage を再送する機能。役割が違うので両方つけて初めてリアルタイム対話表示が完成する- ターミナルから
tee経由で stdout を分岐する時の情報漏れ。--replay-user-messagesを付けたまま stdout をtee logs/conv.jsonlに流すと、ユーザー発言の内容もログに残る。個人情報を含むやりとりを扱う時は出力先の権限とローテーションに注意 - 対話モードのインタラクティブ画面では効かない。stream-json モード(=印刷モード)専用なので、普通に
claudeだけで起動した対話画面では関係がない。SDK や pipe 経由運用の専用スイッチ - system / result イベントが消えるわけではない。replay が足すのは user メッセージだけ。
system(初期化)やresult(終了サマリ)は引き続き stdout に出てくる。ストリーム消費側はtypeで正しく振り分けること - 「アシスタント応答に自分のプロンプトが混ざるのが邪魔だから外す」は危険。外すと UI 側で「誰が何を言ったか」の対応関係を別途同期する必要が出る。表示が崩れる原因になりやすいので、表示する/しないは消費側で
type==="user"をフィルタする方が安全
書き方
claude -p --input-format stream-json --output-format stream-json --replay-user-messages
# このスイッチが固有に求めるのは --input-format stream-json と --output-format stream-json の2つ。-p(--print)は stream-json モード全体の前提
やってみるとこうなる
入力
$ echo '{"type":"user","message":{"role":"user","content":"世界の魚を紹介するサイトを作りたい"}}' | claude -p --input-format stream-json --output-format stream-json --replay-user-messages
出力例
{"type":"system","subtype":"init",...}
{"type":"user","message":{"role":"user","content":"世界の魚を紹介するサイトを作りたい"}}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"分かりました。まず..."}]}}
{"type":"result","subtype":"success",...}
このページに出てきた言葉
- stdin
- プログラムへの入力口。パイプ <code>|</code> でつなぐと前のコマンドの出力がここに流れ込む
- stdout
- プログラムからの出力口。画面に出るか、次のコマンドに渡すか選べる
- stream-json
- 1行に1つの JSON オブジェクトを流す方式(JSON Lines)。会話の進行をリアルタイムに流せる
- SDKUserMessage
- Claude Code の Agent SDK が定義するユーザー発言の JSON 形式。<code>{"type":"user", "message":{...}}</code> の形
- 印刷モード
- <code>--print</code>(<code>-p</code>)で入る、対話画面を立ち上げず標準入出力経由でやりとりするモード
- text_delta
- アシスタント応答の文字差分イベント。<code>--include-partial-messages</code> でリアルタイム流せる
- WebSocket
- ブラウザとサーバーが双方向リアルタイム通信する仕組み。チャットUIの裏側でよく使う