postinstall strips Mach-O code signature on darwin-arm64 → macOS SIGKILLs claude on launch
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
- Fresh \
npm i -g @anthropic-ai/claude-code\on Apple Silicon. - Trigger an auto-update (or any subsequent upgrade).
- 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\. - \
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
- 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. - 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. - 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. - 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗