コード実行ツールでファイルが消える理由 — $OUTPUT_DIRの罠と対処法
code executionツールが生成したファイルは、$OUTPUT_DIR直下に置かれた分しか回収できません。消える3パターンと確実に回収する書き方を実例で解説します。
code executionツールにグラフやレポートを作らせたのに、レスポンスにfile_idが入っていない。Claudeのbashコマンドは成功しているのに、ファイルだけが消える。この症状は$OUTPUT_DIRの仕様を理解していないことがほぼ全てで、原因は3パターンに絞り込めます。
$OUTPUT_DIR直下に置いたファイルだけが回収される
code executionツールのbash_code_executionは、呼び出しごとに新しい空のディレクトリを用意し、これをコマンドから$OUTPUT_DIRとして参照できるようにします。コマンドが終了すると、このディレクトリの最上位にあるファイルだけがキャプチャされ、レスポンスのcontentリストにfile_id付きで返ります。
サブディレクトリに置いたファイルや、/tmpなど$OUTPUT_DIR以外の場所に書いたファイルは、その呼び出しの結果としては回収されません。コンテナ自体には残り続けますが、APIレスポンスのcontentには現れないということです。
もう一つ見落としやすいのが、$OUTPUT_DIRはbash_code_executionの呼び出しごとに新規作成されるという点です。ある呼び出しで$OUTPUT_DIRに書いたファイルはその呼び出しの結果としてすぐキャプチャされますが、次の呼び出しでは$OUTPUT_DIRは別の空ディレクトリに変わります。複数ステップの処理で「前のステップで$OUTPUT_DIRに置いたはずのファイル」を当てにすると、想定と違う挙動になります。
Claudeが1ターンの中でbash_code_executionを何度も呼び出すケースは珍しくありません。データを取得するコマンド、集計するコマンド、グラフを描くコマンドを別々に実行する組み方です。この場合、途中のコマンドで生成した中間ファイルを$OUTPUT_DIRに置いても、それはそのコマンド単体の結果として消費されるだけで、後続のコマンドから参照できるわけではありません。中間ファイルは/tmpなど通常の作業ディレクトリに置いてコンテナ内で使い回し、呼び出し元へ最終的に返したいファイルだけを、最後のコマンドで$OUTPUT_DIRにコピーする、という役割分担がこの仕様に沿った設計です。
なお$OUTPUT_DIRはあくまで出力専用の仕組みで、入力ファイルとは無関係です。CSVやExcelなどの手元のデータをClaudeに解析させたい場合は、Files APIでアップロードしたfile_idをcontainer_uploadコンテンツブロックとしてメッセージに含めます。これによってファイルはコンテナ内に配置され、Claudeがコードから読み込めるようになりますが、この経路と$OUTPUT_DIRの出力キャプチャは独立した別の仕組みです。「入力ファイルを渡したのに出力が返ってこない」という混同が起きたときは、まず入力と出力のどちらの経路の話かを切り分けてください。
ファイルが消える3つのパターン
| パターン | 何が起きるか | 症状 |
|---|---|---|
| サブディレクトリに保存 | 何が起きるか$OUTPUT_DIR/charts/output.pngのように階層を作って保存 | 症状コマンドは成功、contentは空 |
$OUTPUT_DIR以外に保存 | 何が起きるか/tmp/report.pdfなど作業用ディレクトリに保存したまま | 症状同上。ファイルはコンテナ内に残るが応答には出ない |
| コピー漏れ | 何が起きるか中間生成物を作った後、$OUTPUT_DIRへのコピー手順をコマンドに含めていない | 症状Claudeは処理完了と認識するが、呼び出し元は何も受け取れない |
1つ目のサブディレクトリ保存は、Claudeが出力を整理しようとして$OUTPUT_DIR/images/のようなフォルダを自発的に作るときに起きます。仕様上は「最上位のファイルだけ」がキャプチャ対象なので、os.makedirsやmkdirでサブフォルダを切った瞬間に、その中身は回収対象から外れます。
2つ目の「$OUTPUT_DIR以外に保存」は、matplotlib.pyplot.savefig("/tmp/chart.png")のように、コード側でパスを直書きしたときに起きます。Claudeが書くPythonコードのsavefigやto_csvの引数は、モデルが自由に決めるため、明示的な指示がないと/tmpや.(カレントディレクトリ)に書いてしまうことがあります。
3つ目が最も見落としやすいパターンです。Claudeは複数ステップの処理を書くとき、matplotlibでグラフを描いて/tmpに保存し、レポートをpandasで集計して別のファイルに書き出す、という手順を素直に実行します。この時点でどのファイルも$OUTPUT_DIRに置かれていなければ、bashコマンド自体はreturn_code: 0で成功していても、呼び出し元には1つもファイルが渡りません。しかもClaude自身は「処理が完了した」とだけ認識し、ファイルが返っていないことに気づけません。次の節で説明する通り、Claudeはレスポンスのcontentリストを見られないためです。
text_editorで作ったファイルはfile_idを持たない
もう1つ、bashのミスとは別軸で起きるパターンがあります。code executionツールはClaudeに2つのサブツールを渡します。シェルコマンドを実行するbash_code_executionと、ファイルの閲覧・作成・編集を行うtext_editor_code_executionです。Claudeがレポートをtext_editor_code_executionのcreateコマンドで直接$OUTPUT_DIRに書いたとき、その操作結果(text_editor_code_execution_create_result)が持つフィールドはis_file_update(既存ファイルを上書きしたかどうか)だけです。contentリストもfile_idも、このレスポンスには含まれません。
回収対象のfile_idが乗るのはbash_code_execution_resultのcontentフィールドだけで、これは「そのコマンドが$OUTPUT_DIRに残したファイル」を指します。つまりtext_editor_code_executionでどれだけ正しく$OUTPUT_DIR直下にファイルを作っても、その後に$OUTPUT_DIRを触るbashコマンドを1回挟まない限り、file_idは発行されません。ls "$OUTPUT_DIR"だけの空コマンドでも構わないので、ファイル生成をtext_editorで行うフローでは、最後に必ずbashを1回呼ぶ設計にしておく必要があります。
Claudeはキャプチャの成否を自分では確認できない
ここが本質的な理由です。code executionツールの説明文はClaudeに「$OUTPUT_DIRへコピーすること」を指示していますが、Claude自身はAPIレスポンスのcontentリスト(実際にキャプチャされたファイルの一覧)を見ることができません。Claudeが確認できるのは、そのコマンドのstdout/stderr/return_codeだけです。
そのため、ファイルを確実に受け取りたいアプリケーションでは、$OUTPUT_DIRへのコピーとlsによる確認を同じコマンド内に含めるようClaudeに指示するのが公式の推奨手順です。lsの出力がstdoutに乗ることで、Claudeは「コピーが実際に成功したか」を自分の目で確認でき、失敗していれば同じターン内でリトライできます。
python /tmp/make_report.py && cp /tmp/report.pdf "$OUTPUT_DIR/" && ls "$OUTPUT_DIR"このワンライナーは3つの役割を1コマンドに詰め込んでいます。①レポートを生成する ②$OUTPUT_DIR直下にコピーする ③lsで直下の中身をClaudeに見せる、の3段構成です。システムプロンプトやツール呼び出しの指示に「生成したファイルは必ず$OUTPUT_DIR直下にコピーし、同じコマンドでls確認すること」と明記しておくと、複数ステップの処理でも再現性が上がります。
システムプロンプトに書いておく1文
ユーザーの1回のリクエストごとに「$OUTPUT_DIRにコピーしてlsで確認して」と書くのは現実的ではありません。ファイル生成が絡むアプリケーションでは、システムプロンプト側に恒常的なルールとして書いておくのが実務的です。
ファイルを生成するタスクでは、最終的にユーザーへ返すファイルを必ず$OUTPUT_DIRの直下(サブディレクトリを作らない)にコピーし、コピーしたコマンドの中でlsを実行して結果を確認すること。中間ファイルは/tmp等の作業ディレクトリに置き、$OUTPUT_DIRには最終成果物だけを置く。この1文を入れておくだけで、Claudeが複数コマンドにまたがる処理を組んだときも「最後のコマンドで最終成果物だけを$OUTPUT_DIRにまとめる」という設計に自然と寄っていきます。ゼロから都度プロンプトに書くより再現性が高く、Agent SDKでエージェントを常駐させる構成でも1箇所直すだけで済みます。
生成したファイルはFiles APIで取得する
$OUTPUT_DIR直下でキャプチャされたファイルは、レスポンスのcontentリストにfile_idとして現れます。このfile_idはFiles API経由でダウンロードします(PDFの取得手順も同じAPIの流れです)。
def extract_file_ids(response):
file_ids = []
for item in response.content:
if item.type == "bash_code_execution_tool_result":
content_item = item.content
if content_item.type == "bash_code_execution_result":
for output_block in content_item.content:
file_ids.append(output_block.file_id)
return file_ids
for file_id in extract_file_ids(response):
file_metadata = client.files.retrieve_metadata(file_id)
file_content = client.files.download(file_id)
file_content.write_to_file(file_metadata.filename)Files APIでアップロードした通常のファイルは、expires_in_secondsを指定しない限りexpires_atがnullのまま保持され、明示的に削除するまで消えません。code executionが生成したファイルのfile_idも同じFiles APIの管理下に入るため、ダウンロードを後回しにしても即座に失われるわけではありません。ただし、コンテナ自体は作成から30日で期限切れになるため、$OUTPUT_DIRでのキャプチャそのものを取りこぼすと、file_idが発行されず打つ手がなくなります。
ダウンロードしたファイルが、コンテナ内で確認したサイズやチェックサムと一致しないことがありますが、これはバグではありません。PNG・JPEG・GIF・WebPなど対応形式の画像・動画をFiles API経由でダウンロードすると、Anthropicを発行者として記録するC2PA Content Credentials(改ざん検知用の署名メタデータ)が自動で埋め込まれます。署名はファイルサイズを数KB増やし、可視コンテンツ自体は変更しません。テキストファイルやPDF、Officeドキュメントは対応形式外のため署名されません。
$OUTPUT_DIRへのコピーを忘れてもコンテナ再利用で救える場合がある
$OUTPUT_DIRにコピーし忘れたファイルも、生成したbash_code_executionと同じコンテナの中には残っています。レスポンスに含まれるcontainer.idを次のリクエストのcontainerパラメーターへ渡してコンテナを再利用すれば、そのファイルはまだ/tmpなどの元の場所に存在するため、改めて「あのファイルを$OUTPUT_DIRにコピーして」とClaudeに指示すれば回収できます。
# 1回目のリクエストでファイルが/tmpに残ったまま終わったとする
response1 = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
messages=[{"role": "user", "content": "レポートを作って/tmp/report.pdfに保存して"}],
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
)
# container.idを渡して同じコンテナを再利用し、今度は$OUTPUT_DIRへのコピーを明示する
response2 = client.messages.create(
container=response1.container.id,
model="claude-opus-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": "/tmp/report.pdfを$OUTPUT_DIRにコピーしてlsで確認して",
}
],
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
)/tmp/number.txtにランダムな数値を書き込んだ後、別のリクエストで同じcontainer.idを渡して読み出す、という公式サンプルが示す通り、コンテナ内のファイルシステムはリクエストをまたいで維持されます。取りこぼしはこの性質を使えば「計算をやり直す」必要なく回収できます。
コンテナは作成から30日で期限切れになりますが、5分ほど操作がないとチェックポイントされ、30日以内であればIDを指定したリクエストで復元されます。取りこぼしに気づいた直後であれば、新しい会話ターンを起こすよりも同じコンテナを指定して回収する方が早く、Claudeが最初に行った計算や中間生成物を再実行せずに済みます。期限切れのコンテナは復元できないので、その場合はcontainerパラメーターを外して新規実行するしかありません。コンテナはFiles APIと同様にリクエストのワークスペース単位でスコープされるため、別ワークスペースのAPIキーでcontainer.idを渡しても再利用できない点にも注意が必要です。
まとめ
$OUTPUT_DIR直下に置かれた最上位ファイルだけが呼び出し元に返るという1点さえ押さえれば、ファイル消失の大半は防げます。サブディレクトリに保存しない、$OUTPUT_DIR以外の場所に生成物を放置しない、コピーとls確認を同じコマンドに含める、の3つをプロンプトやシステム指示に明記しておくのが実務上の対処です。取りこぼしに気づいたときも、コンテナが期限切れになる前ならコンテナ再利用で拾い直せます。MCPをコードとして呼び出す設計やClaude Agent SDK入門でエージェントを組む際は、ファイル生成を伴うステップの手前でこの挙動を前提に組んでおくと事故が減ります。