Claude Agent SDKがLinuxでmusl版バイナリを優先するバグの原因と対処
TypeScript Agent SDKがLinuxでネイティブバイナリを自動選択する際、glibc環境でもmusl版を先に試して失敗する既知バグの原因と回避策をまとめます。
Claude Agent SDKのバイナリ自動選択バグとは
TypeScript版Claude Agent SDK(@anthropic-ai/claude-agent-sdk)は、Linux環境でネイティブバイナリを自動検出する際に動作しないmusl版を先に選んでしまう既知のバグを抱えていました。対象はUbuntuやDebianなど、glibcベースの一般的なLinuxディストリビューションです。musl版のパッケージがインストールされていれば、SDKはそちらを優先してspawnし、ENOENTで失敗します。
このバグはネイティブバイナリパッケージが導入されたv0.2.113以降で顕在化し、v0.2.141で修正されました。本記事ではこの自動選択ロジックの不具合に絞って扱います。SDK自体の使い方を最初から知りたい場合はClaude Agent SDK入門を、CLI本体側のmusl/glibc判別はlibstdc++.so.6エラーの原因と対処を参照してください。
どのバージョンで発生し、いつ直ったか
GitHub Issue #296(anthropics/claude-agent-sdk-typescriptリポジトリ)には、2026年4月21日の報告を起点に9件のコメントが寄せられ、複数の利用者が同じ症状を再現しています。
| 時期 | バージョン | 状態 |
|---|---|---|
| 〜v0.2.112 | バージョンネイティブバイナリパッケージ導入前 | 状態問題なし(バイナリはCLI本体に同梱) |
| v0.2.113〜v0.2.140 | バージョンネイティブバイナリパッケージ導入後 | 状態musl版が先に選ばれるバグが存在 |
| v0.2.141以降 | バージョン修正版 | 状態ランタイムでlibcを判定して選択 |
筆者はnpmレジストリから各バージョンのパッケージを実際に取得し、sdk.mjsを直接確認しました。v0.2.140までは修正前のロジックのままです。v0.2.141(2026年5月13日公開)からは、process.report.getReport().header.glibcVersionRuntimeを使ったランタイム判定に置き換わっていました。この判定は、Node.jsがglibcにリンクされていれば値を返し、muslであればundefinedになる性質を利用したものです。Issue内でmitchellbreust氏とbubblesorted氏がそれぞれ独立に提案していた修正案と一致します。この判定ロジックは、そのあとのv0.3.270まで変更されずに使われ続けています。
興味深いのは、修正が入ったv0.2.141のリリース日時が、bubblesorted氏がワークアラウンドを投稿した同じ日の数時間後だったことです。Issueスレッド自体は2026年7月16日のコメントを最後に動きが止まっています。ただし、それは「すでに5月の時点で直っていた」ことの遅れた確認に過ぎません。修正の実体は、スレッドが盛り上がっていた5月中旬の時点で既に配信されていました。バージョンを跨いだ調査をせず新しいコメントだけを読むと、直った時期を7月だと誤認しやすい構造になっています。
なぜmusl版が誤って選ばれるのか
Issue本文と複数のコメントを突き合わせると、原因は「パッケージング」と「実行時の選択ロジック」の2層に分かれています。
レイヤー1: パッケージのlibcフィルタが欠けていた
@anthropic-ai/claude-agent-sdk-linux-x64-muslのpackage.jsonは、当初osとcpuのフィールドだけを宣言し、libcフィールドを持っていませんでした。npm 10以降はoptionalDependenciesのlibcフィールドでインストール対象を絞り込めますが、フィールド自体が無ければ絞り込みが効きません。そのため、pnpmはCPUアーキテクチャが一致する限りmusl版とglibc版の両方をインストールし、npmでもホストの実際のlibcとは無関係にmusl版が入るケースが報告されています。
レイヤー2: 実行時の選択ロジックの欠陥
修正前の選択関数は、Linux向けの候補配列を次の順序で定義していました。
function W7($, X=process.platform, J=process.arch) {
let Q = X === "win32" ? ".exe" : ""
let z = (X === "linux"
? [`@anthropic-ai/claude-agent-sdk-linux-${J}-musl`,
`@anthropic-ai/claude-agent-sdk-linux-${J}`]
: [`@anthropic-ai/claude-agent-sdk-${X}-${J}`]
).map((G) => `${G}/claude${Q}`)
for (let G of z) {
try { return $(G) } catch {}
}
return null
}配列の先頭がmusl版であるうえ、ループの中身はrequire.resolveが成功するかどうかしか見ていません。require.resolveが確認するのは、パッケージディレクトリが存在するかどうかだけです。バイナリのELFインタープリタが実行環境で読み込めるかまでは検証していません。そのため、musl版のパッケージが存在してさえいれば、たとえglibc環境で実行不可能でもそのパスがそのまま返っていました。
発生した2つの実害パターン
Issueのコメントからは、パッケージマネージャーによって症状の出方が異なることが分かります。
- pnpm環境: 両方のバリアントが物理的にインストールされる。musl版のバイナリファイル自体は存在するが、実行はできない。
ld-musl-x86_64.so.1という動的ローダーがglibc環境には無いため、spawnがENOENTで失敗する - npm環境: musl版パッケージが入らないケースもある。それでも選択ロジックはmuslパスを先に試すため、存在しないファイルに対して「バイナリが見つからない」というエラーになる
どちらのケースでも、実際に表示されるエラーメッセージは次のような内容で、原因がmusl/glibcの不一致であることを直接示しません。
FAILED: Claude Code native binary not found at
.../claude-agent-sdk-linux-x64-musl/claude.
Please ensure Claude Code is installed via native installer or
specify a valid path with options.pathToClaudeCodeExecutable.Issueの報告者の一人は、ゲートウェイが古いまま気づかず動き続けた状態が38時間続き、その間にまずpnpm-lockのずれという別の問題を疑って調査したのちにこの不具合にたどり着いたと述べています。デプロイ先として名前が挙がっているのは、Railway、Fly、Render、Vercel、ECS Fargateなど、いずれもDebianやUbuntuベースのglibcコンテナです。
さらに厄介なのは、症状が常に同じエラーメッセージで現れるとは限らない点です。あるコメントでは、devcontainerを再ビルドしたあとにquery()を呼ぶと即座にクラッシュしたと報告されています。エラーはENOENTではなくEPIPE(書き込み先が既に閉じている)や、まれにSIGTRAPを含む例外でした。原因は同じmuslバイナリの誤選択でも、子プロセスの起動失敗が表面化する形は実行タイミング次第で変わります。ストリームの書き込みエラーになるかシグナルになるかが一定しないため、表面上の症状だけを見て「別の不具合では」と誤診しやすくなっていました。
自分の環境が影響を受けるか確認する
SDKのバージョンを上げる前に、いま使っているバージョンが対象範囲(v0.2.113〜v0.2.140)に入っているかどうかをpackage-lock.jsonやpnpm-lock.yamlなどのロックファイルで確認するのが最初の一歩です。あわせて、Node.jsが実行時にglibcとmuslのどちらにリンクされているか、両方のバリアントパッケージが実際にインストールされているかを見ておくと、修正前のバージョンから上げられない事情がある場合の回避策選びに役立ちます。
# インストールされている SDK のバージョンを確認
npm ls @anthropic-ai/claude-agent-sdk
# ホストが glibc / musl のどちらにリンクされているか(値があれば glibc)
node -e "console.log(process.report.getReport().header.glibcVersionRuntime)"
# musl 版・glibc 版どちらのパッケージが実際に存在するか
ls node_modules/@anthropic-ai/ | grep claude-agent-sdk-linuxglibcVersionRuntimeが値を返す環境で、なおかつclaude-agent-sdk-linux-x64-muslのディレクトリも存在する場合は、修正前のバージョンであれば誤ってmusl版が選ばれる条件がそろっています。
影響を受けた利用形態と回避策
バグが存在したv0.2.113〜v0.2.140の期間にSDKを利用していた場合、次のいずれかの回避策が報告されています。
| 回避策 | 内容 | 備考 |
|---|---|---|
pathToClaudeCodeExecutableを明示指定 | 内容glibc版バイナリのパス、またはwhich claudeで見つかる別インストールのパスを直接渡す | 備考Issue内で最も多く採用された方法 |
overridesでmusl版を無効化 | 内容package.jsonのoverrides(pnpmならpnpm.overrides)でmusl版パッケージを空パッケージに差し替える | 備考npm/pnpmのビルド設定だけで完結し、コード変更が不要 |
| v0.2.112以前へダウングレード | 内容ネイティブバイナリパッケージ導入前のバージョンに戻す | 備考一時しのぎで、根本解決にはならない |
| Dockerfileでシンボリックリンクを作成 | 内容musl版のパッケージディレクトリを、実体はglibc版バイナリを指すシンボリックリンクに置き換える | 備考npm環境でmusl版が未インストールの場合に有効 |
Dockerコンテナ上でSDKを動かす構成そのものの組み方はAgent SDKのsandbox設定をコードから制御するでも扱っています。pathToClaudeCodeExecutableを渡す場合の実装例は次の通りです。
import { createRequire } from 'node:module'
import path from 'node:path'
const require = createRequire(import.meta.url)
function getClaudeCodeExecutable(): string | undefined {
if (process.platform !== 'linux') return undefined
if (process.arch !== 'x64' && process.arch !== 'arm64') return undefined
const report = process.report?.getReport() as
| { header?: { glibcVersionRuntime?: string } }
| undefined
const isMusl = !report?.header?.glibcVersionRuntime
const variant = isMusl ? `linux-${process.arch}-musl` : `linux-${process.arch}`
const pkg = `@anthropic-ai/claude-agent-sdk-${variant}`
try {
const pkgJsonPath = require.resolve(`${pkg}/package.json`)
return path.join(path.dirname(pkgJsonPath), 'claude')
} catch {
return undefined
}
}process.report.getReport().header.glibcVersionRuntimeは、Node.jsがglibcにリンクされていれば値を持ち、muslにリンクされていればundefinedになります。追加のサブプロセスやlddの呼び出しなしにホストのlibcを判定できるため、SDK側の修正でも同じ仕組みが採用されました。
v0.2.141以降で何が変わったか
修正版のロジックは、候補配列を並べる前にホストのlibcを判定します。glibcVersionRuntimeの値の有無でglibc環境かmusl環境かを判定し、glibc環境ではglibc版を、musl環境ではmusl版を先頭に置くよう順序を反転させる仕組みです(v0.2.141のsdk.mjsで確認)。候補の並び順がホストに合わせて変わるため、npm環境でmusl版パッケージが最初からインストールされていないケースでも、実際に存在するglibc版が先に試されます。
エラーメッセージについても整理が入りました。バイナリファイルが存在するのに起動に失敗した場合には「native binary at ... exists but failed to launch」という文言が返ります。これはファイル不在時の「not found」とは異なる文言で、libc不一致とファイル欠落の切り分けがしやすくなっています。
ランタイム判定への一本化がpnpmとnpmの両方に効いた
今回確認できたのは、SDK側の選択ロジックがrequire.resolveの成否だけを見る方式から、glibcVersionRuntimeによるランタイム判定で候補の並び順を反転させる方式へ切り替わったことです(v0.2.141のsdk.mjsで確認)。pnpm環境(両バリアントが物理的に存在)とnpm環境(muslが入らないケースもある)では壊れ方が異なりますが、候補の並び順をホストの実際のlibcに合わせて決める方式であれば、どちらの環境にも同じロジックで対応できます。パッケージ側にlibcフィールドを追加する案もIssue内で提案されていましたが、実際の配布パッケージに反映されたかどうかは今回確認できませんでした。
もう一つ確認できたのは、エラーメッセージの文言がv0.2.141で分かれたことです。修正前は「バイナリが見つからない」という単一の文言が、ファイルの不在とロードの失敗という性質の異なる原因を区別せずに表示していました。v0.2.141以降は、バイナリが存在するのに起動に失敗した場合に「exists but failed to launch」という別の文言が返るようになっています(実パッケージのsdk.mjsで確認)。Issueの報告者がpnpm-lockのずれを疑って38時間を費やした例は、この文言の曖昧さがどれだけ実害につながったかを示しています。
まとめ
TypeScript Agent SDKでLinux上のquery()呼び出しが「Claude Code native binary not found」で失敗する場合、まずSDKのバージョンを確認してください。v0.2.113からv0.2.140までの範囲であれば、今回取り上げた既知バグに該当する可能性が高く、pathToClaudeCodeExecutableの明示指定で当面回避できます。v0.2.141以降にアップグレードできる環境であれば、アップグレードが最も確実な解決策です。CLI本体側のmusl/glibc関連のエラー(libstdc++.so.6など)に遭遇した場合は、libstdc++.so.6エラーの原因と対処を確認してください。