Claude Codeのサンドボックス、bwrapの一時ディレクトリが消えずに/tmpに溜まり続ける不具合を追った

Claude Codeのサンドボックス、bwrapの一時ディレクトリが消えずに/tmpに溜まり続ける不具合を追った

Claude Codeのbubblewrapサンドボックスが拒否パスをブロックするたびに作るclaude-empty-*ディレクトリが/tmpに残り続ける不具合(#96129)。公式が認めている別の掃除漏れとの違いと、今できる回避策を整理した。

エンジニアのゆとです。

Linux(WSL2含む)でClaude Codeのサンドボックスを有効にして長時間使っていると、/tmp(やos.tmpdir()が指す場所)にclaude-empty-から始まる空ディレクトリがどんどん増えていく——という報告がGitHub Issueに上がっている。1個あたりは4KBの空ディレクトリだから「容量」としては大した量じゃない。問題は数だ。報告者の環境では、複数セッションとサブエージェントを並行して普通に使っていただけで、12時間で2,126個溜まったという。

ファイル数(inode)がじわじわ食われていくタイプの漏れは気づきにくい。ディスク使用率のダッシュボードを見ても「あれ、空き容量はあるのに」となりがちで、dfではなくdf -iを見て初めて気づくケースも多い。しかもこれ、Claude Code自身がサンドボックスの中で「拒否パスをブロックする」という、正規の防御動作の副作用として発生する。攻撃でもミスでもなく、普通に使っているだけで溜まる類の不具合だ。

自分もサンドボックスを有効にしたままVPS上で長時間セッションを回すことがあるので、他人事ではない。GitHub Issue #96129と公式ドキュメントを突き合わせて、何が起きているのか、公式が既に認めている「似たケース」とどう違うのか、今どう付き合えばいいのかを整理する。

まず結論——何が起きていて、何が公式に認められているか

項目内容
症状サンドボックス有効時、$TMPDIR(未設定ならos.tmpdir())配下にclaude-empty-XXXXXXという空ディレクトリが作られ、セッション終了後も削除されずに残り続ける
発生条件存在しない親パスへの書き込みを拒否するとき、bwrapが空ディレクトリを--ro-bindでマウントして「ここには何も作らせない」という防御を行う。このとき使うホスト側の元ディレクトリが記録されず、掃除の対象から漏れる
対象環境Linux・WSL2でサンドボックスのファイルシステム隔離が有効な場合。記事執筆時点(2026年9月22日報告)でClaude Code 2.1.280
実測量報告者の環境で12時間に2,126個(関連するsrt-mux-*.sockは21時間で1,236個)
公式の認識.claude設定パス配下の0バイトプレースホルダーが残る別の掃除漏れは公式docsが明記しclaude doctorで検知できる。だが/tmp直下のこの漏れは記事執筆時点で公式ドキュメントに記載なし、claude doctorの対象パスとも異なる
今できること影響範囲を実測で確認したうえで、CLAUDE_CODE_TMPDIRを専用ディレクトリに切り出し、定期的な掃除をcron/systemd-tmpfilesに任せる
github.com
[BUG] Linux sandbox leaks claude-empty-* mount-source directories in the temp dir (never removed) · Issue #96129 この記事の一次情報源。de-minifyしたコード引用付きで原因箇所を特定し、12時間で2,126個蓄積した実測ログを添えている。

何が起きているか——bwrapの「空ディレクトリで塞ぐ」という防御の副作用

まず前提として、Claude Codeのサンドボックスがなぜ「存在しないパスへの書き込み」をわざわざ空ディレクトリでブロックするのかを押さえておく。公式ドキュメントのトラブルシューティングには、似た仕組みの別の症状としてこう書かれている。

0-byte read-only files appear at .claude settings paths, and “Yes, and don’t ask again” doesn’t save: on Linux and WSL2, the sandbox holds a write denial on a file that doesn’t exist yet by creating a 0-byte read-only placeholder there while a sandboxed command runs. The sandbox removes the placeholder afterward.

要するに、まだ存在しないファイル・ディレクトリへの書き込みを拒否したい場合、bwrapは「そこに読み取り専用の空の何か」を先にマウントしてしまうことで、書き込みそのものを起こらせなくしている。この仕組み自体は理にかなっていて、セッション終了時にちゃんと片付けば何の問題もない。

Issue #96129が指摘しているのは、この「片付け」が一部のケースで機能していないという点だ。報告者が2.1.280のコードを逆コンパイルして確認した該当箇所がこれだ(de-minify済み)。

if (blockViaEmptyDir) {
  const src = fs.mkdtempSync(path.join(tmpdir(), "claude-empty-"));
  bwrapArgs.push("--ro-bind", src, mountPoint);
  mountPoints.add(mountPoint);   // マウント先だけが記録される
  ...
}

fs.mkdtempSyncでホスト側の一時ディレクトリ(src)を作り、それをサンドボックス内のmountPointへ--ro-bindする、という処理自体はよくある実装だ。問題は次の行にある。掃除の対象を記録するmountPointsというSetに追加されているのは、サンドボックス内側のmountPointだけ。ホスト側で実際にファイルシステムの容量を食っているsrc(=mkdtempSyncが作った本体)は、どこにも記録されない。

セッション終了時のクリーンアップはmountPointsを辿って[Sandbox Linux] Cleaned up bwrap mount point (dir): ...というログを出しながらサンドボックス内側のマウントポイントだけを片付け、最後にmountPoints.clear()する。ホスト側のsrcはそもそも追跡対象に入っていないので、この掃除のどのステップにも引っかからない。結果、mkdtempSyncで作られた空ディレクトリだけがホストの/tmpに永遠に残る。

キルされたセッションが悪さをしている、という話ではない。正常に終了したセッションでも、拒否パスをブロックする処理が一度でも走れば、その分だけ漏れる。報告者の環境では「1回のサンドボックスセットアップにつき2つの拒否パスをブロックする」構成だったらしく、claude-empty-*が2個ずつ同じタイムスタンプで対になって増えていたという。

Claude Codeサンドボックス、ファイルと通信を別々に緩める設定が来た
Claude Codeサンドボックス、ファイルと通信を別々に緩める設定が来たClaude Code v2.1.216とv2.1.219で追加されたsandbox.filesystem.disabledとsandbox.network.strictAllowlistを検証。ファイル隔離だけ外す設定と、許可リスト外の通信を黙って弾く設定をsettings.json実例つきで解説する。読む →

公式が既に認めている「似たケース」との違い

ここでややこしいのが、公式ドキュメントが実は同じ「拒否パスを空もので塞ぐ」仕組みに起因する別の不具合を、既に1つ認めて対処法まで用意しているという点だ。両者を混同しないよう整理しておく。

公式が認めている掃除漏れIssue #96129
対象パスプロジェクト内の.claude設定ファイル(例: .claude/settings.local.json)OSの一時ディレクトリ全体($TMPDIRまたはos.tmpdir())
実体0バイトの読み取り専用プレースホルダー「ファイル」mkdtempSyncで作られる空の「ディレクトリ」
発生条件セッションがSIGKILL等で強制終了され、通常のクリーンアップが走らなかった場合拒否パスのブロックが発生した通常終了セッションでも、ホスト側パスが最初から追跡対象外なので毎回漏れる
検知方法claude doctor(または/status)が「Stale sandbox mask files left by a killed session」として警告し、最大3件のパスを名指しした上で残数をカウントする記事執筆時点で公式の検知手段なし。手動でlsするしかない
対処法該当プロジェクトで他のClaude Codeセッションが動いていないことを確認してrmで削除記事執筆時点で正式な回避策なし(後述の暫定策のみ)
バージョン2.1.257でclaude doctorが検知するようになった。それ以前は検知すらされず放置されていた2.1.280時点でオープン
code.claude.com
Stale sandbox mask files left by a killed session - Claude Code Docs claude doctorが検知するプレースホルダーファイルの仕組みと対処法。対象はプロジェクト内の.claude設定パスに限られ、OSの一時ディレクトリは対象外。

つまり「拒否パスを空もので塞いでおいて、掃除を忘れる」という設計上のパターンは、Anthropic側も一度は経験して直している。ただしその修正(およびclaude doctorでの検知)は.claude設定パスというプロジェクト内の特定領域に閉じていて、OS全体の一時ディレクトリに作られるclaude-empty-*は対象になっていない。コード上も別の追跡変数(mountPoints)を使う別の処理なので、同じ「掃除漏れ」という現象でも実装としては別バグだと考えたほうがいい。claude doctorを実行しても、このディレクトリ漏れは指摘してくれない。

影響を受けやすい環境——「一時ディレクトリが永続ディスクかどうか」で差が出る

Issue本文でも触れられている通り、この漏れの実害の大きさは環境によってかなり変わる。

  • 一時ディレクトリがtmpfs(メモリ上のファイルシステム)の環境: 再起動すれば消える。実害は「再起動までの間、inodeとメモリを少しずつ食う」程度
  • 一時ディレクトリが永続ディスク上にある環境: 再起動しても消えない。長期間稼働させ続けるVPSや、複数セッション・サブエージェントを並行で回すサーバー用途ほど蓄積が早い

具体的には次のような環境が要注意になる。

  • WSL2(報告環境そのもの。Windowsのファイルシステムをマウントしている構成では特に一時ディレクトリが永続化されやすい)
  • 契約VPS上でClaude Codeを常駐させて使っている場合(自分もこのパターンで運用しているので他人事じゃない)
  • CI/CDのセルフホストランナーなど、同じマシンでサンドボックス付きのセッションを繰り返し起動する構成
Claude CodeがWSLで重い・権限プロンプトが消えない——症状別に公式ドキュメントとGitHub Issueで原因を特定した
Claude CodeがWSLで重い・権限プロンプトが消えない——症状別に公式ドキュメントとGitHub Issueで原因を特定したClaude CodeをWSL2で使うと応答開始まで1〜6分かかる、起動直後にフリーズする、settings.jsonの許可設定が効かない——症状別にGitHub Issueと公式ドキュメントの一次情報で原因を切り分け、対処法とネイティブWindows/WSLの選び方をまとめた。読む →

逆に言うと、tmpfsが/tmpに割り当てられている一般的なLinuxデスクトップ・短命なCIコンテナでは、実害はそこまで大きくない可能性が高い。まず自分の環境がどちらに当てはまるか確認するのが先だ。

自分の環境でどれだけ溜まっているか確認する

対処を考える前に、まず実態を把握する。

# 一時ディレクトリの場所を確認(CLAUDE_CODE_TMPDIRを設定していなければ$TMPDIRかos.tmpdir()のデフォルト)
echo "${TMPDIR:-/tmp}"

# claude-empty-*ディレクトリの個数を数える
ls -d "${TMPDIR:-/tmp}"/claude-empty-* 2>/dev/null | wc -l

# 関連するUnixソケットの残骸も一緒に確認(Issue内で言及されている副次的な漏れ)
ls "${TMPDIR:-/tmp}"/srt-mux-*.sock 2>/dev/null | wc -l

# 作成日時の古いものから数件だけ中身を見て、本当に空か確認しておく
ls -la "${TMPDIR:-/tmp}"/claude-empty-* 2>/dev/null | head -5

Issueの報告環境ではこのclaude-empty-*とは別に、srt-mux-<pid>-<n>.sockというUnixソケットの残骸も溜まっていたと書かれている。こちらはmuxサーバーのclose()処理でアンリンクされる設計になっているが、セッションがclose()を経由せずキルされると同様に残る。21時間で1,236個という数字が報告されている。仕組みは別物だが、症状としては「正常な掃除ロジックはあるのに、それを通らない終了経路がある」という点で似ている。

数が数百〜数千のオーダーで出てくるなら、放置せず次の対処に進んだほうがいい。

今できる回避策

修正を待つ間の現実的な手を優先度順に挙げる。

1. CLAUDE_CODE_TMPDIRで専用ディレクトリに切り出す

公式ドキュメントによると、CLAUDE_CODE_TMPDIRはClaude Code自身が使う一時ディレクトリを上書きする環境変数で、Unix系では指定したパスの下に/claude-{uid}/を付加した場所が実際に使われる。デフォルトはmacOSが/tmp、Linux/Windowsはos.tmpdir()だ。

export CLAUDE_CODE_TMPDIR="$HOME/.cache/claude-tmp"

これ自体は漏れそのものを止める修正ではないが、OS共通の/tmpに他のプロセスのファイルと混ざった状態で溜まるより、専用ディレクトリに寄せておいたほうが「どれがClaude Code由来の漏れか」を把握しやすくなる。次の定期掃除とも相性がいい。

2. 定期的な掃除をcron/systemd-tmpfilesに任せる

漏れているディレクトリは中身が空(4KB)であることが確認できているので、一定時間より古いものは安全に消せる可能性が高い。ただし、いま実際に稼働中のセッションが使っているマウントポイントを誤って消さないよう、更新時刻に十分な余裕を持たせる。

# 例: 1日以上前に作られたclaude-empty-*だけを消す(cronで日次実行する想定)
find "${CLAUDE_CODE_TMPDIR:-$TMPDIR}/claude-"* -maxdepth 1 -name "claude-empty-*" -mtime +1 -exec rm -rf {} \; 2>/dev/null

systemd-tmpfilesを使える環境なら、/etc/tmpfiles.d/にclaude-empty-*向けのエージング設定を1本足しておくほうが、cronスクリプトを自作するより運用が楽だ。いずれにせよ、稼働中セッションが今まさに使っているマウントポイントを巻き込まないよう、閾値は最低でも数時間以上に設定しておいたほうが安全だと思う。

3. Issueをウォッチしつつ、df -iで定点観測する

inodeの枯渇はdfの空き容量表示だけでは気づけない。ディスク容量に余裕があるように見えてもdf -iでinode使用率だけ100%に近づいている、という事態は起こり得る。長時間稼働のサーバーでサンドボックスを使っているなら、監視項目にdf -iを一つ足しておくと安心材料になる。

df -i "${TMPDIR:-/tmp}"
Claude Codeのsandbox.excludedCommandsが効かない——リストに書いたコマンドがまだサンドボックスの中にいる
Claude Codeのsandbox.excludedCommandsが効かない——リストに書いたコマンドがまだサンドボックスの中にいるClaude Codeのsandbox.excludedCommandsにdocker *やgh *を登録しても、ps/pgrepやgit -Cなど一部のコマンドは依然サンドボックス内で失敗する。GitHub Issue群と公式settings-referenceを突き合わせて原因パターンを整理し、実務での回避策をまとめた。読む →

FAQ

この漏れはセキュリティ上の問題ですか?

いいえ。漏れているのは中身が空の読み取り専用ディレクトリで、機密情報や実データが残るわけではない。あくまでディスクのinode・エントリ数を消費し続ける「運用コスト」の問題であって、サンドボックスの隔離そのものが破られているわけではない。

dangerouslyDisableSandboxでサンドボックスを無効にすれば漏れは止まりますか?

止まるはずだが、それはサンドボックスによるファイルシステム隔離自体を丸ごと手放すということでもある。この漏れ単体のためにサンドボックスを無効化するのは本末転倒で、前述のCLAUDE_CODE_TMPDIR切り出し+定期掃除で運用したほうが現実的だと思う。

claude doctorを実行すれば検知・掃除してくれますか?

されない。claude doctorが検知する「Stale sandbox mask files left by a killed session」は.claude設定パス配下の0バイトプレースホルダーが対象で、Issue #96129が報告しているOS一時ディレクトリ配下のclaude-empty-*ディレクトリとはコード上の追跡経路が別になっている。この記事の対処法は、あくまでclaude doctorの対象外を自衛的に運用でカバーするものだと理解しておいたほうがいい。

まとめ

サンドボックスが拒否パスを空ディレクトリで塞ぐという設計自体は理にかなっているし、公式もその仕組みに起因する掃除漏れを一度(.claude設定パスのプレースホルダーファイル)経験してclaude doctorでの検知まで用意している。Issue #96129が指摘しているのは、同じパターンの別バリエーション——OS共通の一時ディレクトリに作られるホスト側のマウント元ディレクトリが、そもそも掃除対象として記録されていないという、コードレベルでは別経路の漏れだ。

記事執筆時点(2026年9月22日報告、Claude Code 2.1.280)でオープンのままなので、修正が入るまでは実害の大きさが環境依存であることを踏まえつつ、CLAUDE_CODE_TMPDIRでの切り出しと定期掃除、df -iでの定点観測というあたりで自衛しておくのが現実的だと思う。

inodeやfdといったOS側のリソース上限がらみでいうと、Claude Codeが出すsudo launchctl limit maxfilesのヒントをそのまま実行してmacOSのターミナルが起動しなくなる別の不具合も報告されている。こちらも合わせて読んでおくと、macOS上でのリソース上限系のハマりどころが一通り把握できる。

Claude Codeが出すsudo launchctl limit maxfilesの案内を実行すると、Macのターミナルが全部起動しなくなる
Claude Codeが出すsudo launchctl limit maxfilesの案内を実行すると、Macのターミナルが全部起動しなくなるClaude Code(内蔵Bun)がファイルディスクリプタ不足エラー時に出す`sudo launchctl limit maxfiles 2147483646`のヒント。実行するとmacOSのlogin処理が固まり、新規ターミナルが3分間ハングする。原因をGitHub Issue一次情報で追い、直し方と安全な設定値を整理した。読む →
code.claude.com
Configure the sandboxed Bash tool - Claude Code Docs サンドボックスの仕組み・$TMPDIRの扱い・トラブルシューティングをまとめた公式ページ。この記事の仕様部分の一次情報源。
← 記事一覧に戻る