postinstall strips Mach-O code signature on darwin-arm64 → macOS SIGKILLs claude on launch

Resolved 💬 3 comments Opened Apr 24, 2026 by shubhamattri-nova Closed May 28, 2026

Environment

  • macOS Darwin 25.2.0 (Apple Silicon, arm64)
  • Node v20.19.6, npm global install via Homebrew prefix (\/opt/homebrew/lib/node_modules\)
  • Claude Code installed globally: \@anthropic-ai/claude-code\

Symptom

After an auto-update, \claude\ is killed immediately on launch:

\\\
~ ❯ claude
[1] 90529 killed claude
\
\\

Root cause (diagnosed)

The native binary at \@anthropic-ai/claude-code/bin/claude.exe\ loses its code signature during the postinstall. macOS kernel then SIGKILLs it on exec.

\\\
$ codesign -dv /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe
...: code object is not signed at all
\
\\

The platform-native package under \node_modules/@anthropic-ai/claude-code-darwin-arm64/\ kept its signed copy:

\\\
$ codesign -dv .../claude-code-darwin-arm64/claude
Identifier=com.anthropic.claude-code
Format=Mach-O thin (arm64)
CodeDirectory v=20500 size=1654533 flags=0x10000(runtime)
Signature size=9046
\
\\

So the signature exists on the source binary but is missing on the destination after \install.cjs\ writes it to \bin/claude.exe\.

On this install the postinstall had also left \bin/claude.exe\ as the ASCII fallback stub (\"Native package … not found\"), because the \package.json\ of \claude-code-darwin-arm64\ was missing from \node_modules\ (only the \claude\ binary remained). \require.resolve('@anthropic-ai/claude-code-darwin-arm64/package.json')\ in \install.cjs:144\ then failed, so the real binary was never copied at all. A retained staging copy at \@anthropic-ai/.claude-code-*/\ had the complete package.

Reproduction sketch

  1. Fresh \npm i -g @anthropic-ai/claude-code\ on Apple Silicon.
  2. Trigger an auto-update (or any subsequent upgrade).
  3. Observe that \bin/claude.exe\ is either (a) the ASCII fallback stub, or (b) a Mach-O binary that reports \"not signed at all\" to \codesign -dv\.
  4. \claude\ → SIGKILL.

Manual fix that worked

\\\bash
src=/opt/homebrew/lib/node_modules/@anthropic-ai/.claude-code-<hash>/node_modules/@anthropic-ai/claude-code-darwin-arm64/claude
dst=/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe
rm -f \"$dst\" && /bin/cp \"$src\" \"$dst\" && chmod +x \"$dst\"
codesign -dv \"$dst\" # signature preserved
\
\\

Suggested fixes

  1. Preserve code signatures: \install.cjs\ should use a copy method that preserves extended attributes and code signing (e.g. \fs.copyFileSync\ with COPYFILE_FICLONE, or \cp -c\ via spawn). If the current impl does a \fs.writeFileSync(fs.readFileSync(...))\ round-trip, that loses the signature.
  2. Ad-hoc re-sign as a fallback: if signature preservation isn't feasible across copy paths, have the postinstall do \codesign --force --sign - <dst>\ after the copy on darwin. Ad-hoc signing is enough to stop the kernel killing it.
  3. Verify after install: add a \codesign -v\ check at the end of \install.cjs\ on darwin and bail loudly if the destination is unsigned, instead of leaving the user with a silently-broken install.
  4. Preserve package.json when overwriting optional dep dirs: something in the upgrade path stripped \package.json\/\LICENSE\/\README\ from \claude-code-darwin-arm64\, which was the original reason \install.cjs\ fell back to the stub. Worth investigating whether an atomic rename vs file-by-file overwrite would help.

Happy to test patches against my env if useful.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗