1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeの中身はBunだった。メモリ逼迫の自動停止は発火せず

1
Posted at

はじめに

Claude Code v2.1.193(2026-06-25)のリリースノートに、こんな一文が追加されました。

Added automatic memory-pressure reaping for idle background shell commands (disable with CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1)

「メモリが逼迫したら、アイドル状態のバックグラウンドシェルを自動的に片付ける」機能です。長時間セッションで run_in_background: true を多用していると、放置したシェルがじわじわメモリを食っていくのが気になっていたので、実際にどう動くのか手元のバイナリを直接調べてみました。

この記事で分かること

  • Claude Code CLIバイナリの正体(実行ファイルの中身を strings で確認した結果)
  • memoryPressure という機能が依存しているイベントは、公式Node.js docsには存在しないこと
  • 実機で数GB分のメモリを確保しても、そのイベントが発火しなかった実測結果

対象読者

  • run_in_background でバックグラウンドシェルを多用している人
  • Claude Codeの内部実装に興味がある人

前提環境

  • 実行中の Claude Code CLI: 2.1.202(Linux x64。機能が入った v2.1.193 より新しいビルド)
  • 検証用ランタイム: システムにインストール済みの Bun 1.3.11 / Node.js 22.22.2(バイナリ内蔵のBunは1.4.0だが、単独実行できないため近いバージョンのBunで検証)

TL;DR

  • Claude Code CLIの実行ファイルは、Node.jsではなく Bunでコンパイルされた単一バイナリ だった(strings でBun関連の埋め込み文字列を多数確認)
  • 自動リーピング機能は process.on("memoryPressure", ...) に依存しているが、この名前のイベントは Node.js公式ドキュメントに一切記載がない
  • 実際にBun/Node.js双方の環境で最大10GB程度のBufferを確保してみたが、memoryPressure イベントは一度も発火しなかった

発見の経緯

claude コマンドの実体がどこにあるか確認するところから始めました。

$ which claude
/opt/node22/bin/claude
$ readlink -f /opt/node22/bin/claude
/opt/claude-code/bin/claude
$ file /opt/claude-code/bin/claude
/opt/claude-code/bin/claude: ELF 64-bit LSB executable, x86-64, ...

/opt/node22/bin/claude はただのシンボリックリンクで、実体は /opt/claude-code/bin/claude という約250MBの単一ELFバイナリでした。Node.jsのランタイムディレクトリの下に置かれているので一見Node.js製に見えますが、strings で埋め込み文字列を見ると印象が変わります。

$ strings /opt/claude-code/bin/claude | grep -iE "bun v[0-9]|bun_version|Bun\.version" | head
Bun v1.4.0 (63bb0ca0d) Linux x64 (baseline)
bun pm version v
bun pack v
bun publish v
...

さらにフィードバック送信機能のコード断片には Bun.randomUUIDv7()Bun.versionBun.revision の呼び出しがそのまま残っていました。これは Bunの bun build --compile で作られた自己完結型バイナリ の特徴そのものです。Claude Code CLIは、Node.jsではなく Bun 1.4.0 の上で動いています(Node.jsが同梱ディレクトリに置かれているのは、Claude Codeがユーザーの node/npm を呼び出す場面向けのようです)。

検証1: 自動リーピングの実装を覗く

同じバイナリの中に、今回追加された機能の実装も見つかりました(変数名は圧縮・難読化されていますが、該当ロジックだけ抜粋します)。

if (i === void 0 && !mn() && !ke.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP) {
  let a = () => {
    // ...アイドル判定・時間経過チェック...
    xe("task_local_shell_pressure_reap"), /* シェルをkillする処理 */
  };
  process.on("memoryPressure", a);
  s = () => process.off("memoryPressure", a);
}

環境変数名(CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP)・テレメトリイベント名(task_local_shell_pressure_reap)とも、公式リリースノートの説明と一致しています。実装は確かに存在します。ポイントは、この処理が process.on("memoryPressure", ...) というイベントリスナーに完全に依存していることです。

検証2: memoryPressure はNode.js公式docsに存在しない

Node.js公式ドキュメント(v26.4.0)の「Process events」セクションに列挙されているイベントは以下の通りです。

  • 'beforeExit' / 'disconnect' / 'exit' / 'message' / 'rejectionHandled' / 'workerMessage' / 'uncaughtException' / 'uncaughtExceptionMonitor' / 'unhandledRejection' / 'warning' / 'worker' / シグナル系('SIGINT' など)

memoryPressure はこの一覧に 含まれていませんprocess.on() はEventEmitterなので任意の文字列を登録できてしまい、実際に何かがそのイベントを発火させない限り、リスナーは永久に呼ばれないコードになります。

Bunのプロセス関連ドキュメント・ブログ記事を検索した範囲でも、memoryPressure を公式APIとして明記した記述は見つかりませんでした(V8の内部埋め込みAPIである MemoryPressureNotification が近い名前として存在しますが、これはNode.js/BunのC++実装が内部的に呼び出す側のAPIで、JS側に自動転送される保証はありません)。

検証3: 実際にメモリを確保してイベント発火を試す

「ドキュメントに無い」だけでは実際に発火するかは分からないので、Bunとsystem Node.js双方で意図的にメモリを積み上げてみました。

// mp-test.js
let fired = false;
process.on("memoryPressure", () => { fired = true; console.log("FIRED"); });
const chunks = [];
for (let i = 0; i < 200; i++) {
  chunks.push(Buffer.alloc(50 * 1024 * 1024)); // 50MBずつ確保(200回で合計10GB)
  if (i % 40 === 0) console.log("allocated MB:", (i + 1) * 50, "fired=", fired);
}
setTimeout(() => console.log("final total MB=", chunks.length * 50, "fired=", fired), 300);

Bun 1.3.11(Linux)での実行結果:

$ bun mp-test.js
allocated MB: 50 fired= false
allocated MB: 2050 fired= false
allocated MB: 4050 fired= false
allocated MB: 6050 fired= false
allocated MB: 8050 fired= false
final total MB= 10000 fired= false

system Node.js 22.22.2(--max-old-space-size=256 付き、Buffer確保はヒープ外なので上限に関係なく積める)でも、全く同じスクリプトで試しましたが結果は同じでした。

$ node --max-old-space-size=256 mp-test.js
allocated MB: 50 fired= false
allocated MB: 2050 fired= false
allocated MB: 4050 fired= false
allocated MB: 6050 fired= false
allocated MB: 8050 fired= false
final total MB= 10000 fired= false

BunでもNode.jsでも、合計10GBのBufferを確保し終えるまで memoryPressure は一度も発火しませんでした。

これが意味すること

過度に「機能が壊れている」と決めつけるのは早計です。V8のメモリ圧迫通知は、OSやコンテナのcgroup(Linuxの場合はPSI=Pressure Stall Information)からの通知に連動する設計が一般的で、単にプロセス内でBufferを積むだけでは条件を満たさない可能性があります。macOSの DISPATCH_SOURCE_TYPE_MEMORYPRESSURE のように、プラットフォーム依存で実装されているケースも珍しくありません。

ただ、実務的に重要なのは「今回試した範囲(Linux上でプロセスのメモリ使用量を素朴に増やす)では発火しなかった」という事実です。クラウドの開発コンテナ・CI環境など、Linux上でClaude Codeを動かすケースは決して珍しくありません。バックグラウンドシェルが際限なくメモリを食い始めても、この機能が「勝手に片付けてくれる」ことを期待して放置するのは危険だと分かりました。長時間ジョブは自分で capMs や明示的な kill ・タイムアウト監視を組み込んでおくほうが安全です。

著者視点の発見ポイント

筆者が驚いたのは、リリースノート1行の裏側に「実はBunでビルドされている」という情報と「実装がまだ実環境で検証しづらい未確認のイベントに依存している」という2つの事実が隠れていた点です。公式ドキュメントの文言だけを鵜呑みにせず、実際にインストール済みのバイナリを strings で覗き、簡単なスクリプトでメモリを積んで挙動を試す――このくらいの手間で、ドキュメントだけでは分からない「機能の実際の発火条件」まで踏み込めることが分かりました。

まとめ

  • Claude Code CLIバイナリは、Node.jsではなくBun(v1.4.0)でコンパイルされた単一実行ファイルだった
  • v2.1.193で追加された「アイドルバックグラウンドシェルの自動リーピング」は process.on("memoryPressure", ...) に依存しているが、これはNode.js公式docsに存在しないイベント名
  • Bun・Node.js双方の実機で最大10GB程度のメモリ確保を試したが、一度も発火しなかった
  • Linux環境でバックグラウンドシェルを長時間動かす場合は、自動リーピングに頼らず自分でタイムアウト・kill処理を用意するのが安全

参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?