Skip to content

Linux: musl binary preferred over glibc in v0.2.116 CLI auto-discovery #296

Description

@gh-gill

Summary

On Linux, SDK v0.2.116's CLI auto-discovery resolves to the musl variant before the glibc variant. If both platform packages are installed (via pnpm, which may install all optional deps for the current CPU), query() picks the musl binary path. Spawning a musl-linked ELF on a glibc system fails with ENOENT because the musl dynamic loader is missing — but the resulting error message says "Claude Code native binary not found", which is misleading on a system where the file very much exists.

Environment

  • SDK: @anthropic-ai/claude-agent-sdk@0.2.116
  • Node: v24.13.1
  • OS: Ubuntu 24.04 (glibc 2.39)
  • Package manager: pnpm 10.32.1
  • Both installed: @anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.116 and @anthropic-ai/claude-agent-sdk-linux-x64@0.2.116

Reproduction

import { query } from '@anthropic-ai/claude-agent-sdk'

const stream = query({
  prompt: 'list models',
  options: {
    cwd: process.cwd(),
    maxTurns: 0,
    permissionMode: 'plan',
  },
})
await stream.supportedModels()

Actual

FAILED: Claude Code native binary not found at
  .../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk-linux-x64-musl@0.2.116/
  node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl/claude.
Please ensure Claude Code is installed via native installer or
specify a valid path with options.pathToClaudeCodeExecutable.

The file referenced in the error exists and is readable:

$ ls -la .../claude-agent-sdk-linux-x64-musl/claude
-rwxr-xr-x 1 user user 231918016 ... claude

$ .../claude-agent-sdk-linux-x64-musl/claude --version
bash: .../claude: cannot execute: required file not found   ← dynamic loader missing

$ .../claude-agent-sdk-linux-x64/claude --version
2.1.116 (Claude Code)                                       ← glibc variant works

Passing pathToClaudeCodeExecutable to the glibc binary explicitly also works, confirming the SDK itself runs fine once the correct binary is selected.

Expected

Either (a) the SDK detects the host's libc and picks the matching binary, or (b) the SDK tries glibc first and only falls back to musl when glibc is absent (glibc is by far the more common Linux environment). The ENOENT at spawn time should also be handled so the fallback iteration actually reaches the next candidate.

Root cause

In sdk.mjs, the resolver function iterates candidates in this order on 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
}

Two issues here:

  1. The musl candidate is first. On a system where both optional deps are installed (common with pnpm), require.resolve succeeds for musl and that path is returned without ever considering glibc.
  2. The try/catch guards require.resolve, not the downstream spawn. A musl binary that resolves but can't execute on glibc does not trigger fallback — the caller just gets a spawn ENOENT with a misleading error message.

Suggested fix

One of, in order of robustness:

  1. Detect libc at runtime (e.g. check for /lib/ld-musl-* vs /lib64/ld-linux-*, or use process.report.getReport().header.glibcVersionRuntime) and pick the matching variant.
  2. Reverse the array so glibc is tried first. Much simpler; works for the common case.
  3. Spawn-probe each candidate (e.g. --version with small timeout) and only accept the first that actually runs.

Option 1 is the correct long-term fix, option 2 unblocks most users immediately.

Workaround

Pass pathToClaudeCodeExecutable explicitly, either to the glibc variant package binary or to a separately-installed @anthropic-ai/claude-code binary (e.g. from which claude).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions