[Bug] Native installer produces unsealed macOS app bundle rejected by code signature validation
Bug Description
Bug: native installer produces a malformed (unsealed) ClaudeCode.app → macOS reports it as "damaged and can't be opened"
Summary
The Claude Code native installer creates ~/.local/share/claude/ClaudeCode.app without the code-signing seal (Contents/_CodeSignature/CodeResources). The bundle's Mach-O binary carries an Anthropic Developer ID signature whose CodeDirectory indicates sealed resources must be present, but the bundle on disk has none. macOS therefore fails signature validation and shows:
"ClaudeCode.app" is damaged and can't be opened. You should move it to the Trash.
The bundle is not quarantined — this is not a Gatekeeper/quarantine prompt, it's a broken/incomplete signature. It is reproducible: claude install latest --force regenerates the same malformed bundle.
Environment
- Claude Code CLI: 2.1.190
- Affected app bundle:
~/.local/share/claude/ClaudeCode.app(CFBundleIdentifier = com.anthropic.claude-code,LSUIElement = true) - macOS: 26.5.1 (build 25F80), Apple Silicon (arm64)
- Installed native versions present: 2.1.183, 2.1.185, 2.1.186, 2.1.187, 2.1.190
- Launcher:
~/.local/bin/claude→ symlink →~/.local/share/claude/versions/2.1.190
Steps to reproduce
- Have the CLI manage the native build (auto-update or
claude install). - Trigger whatever launches the helper app (or simply double-click
~/.local/share/claude/ClaudeCode.app). - macOS shows the "damaged and can't be opened" dialog.
claude install latest --force→ reinstall succeeds, but the regenerated bundle is still malformed (same dialog).
Evidence
Malformed bundle layout (missing _CodeSignature/ and CodeResources):
ClaudeCode.app/Contents/
├── Info.plist
└── MacOS/claude # 217 MB Mach-O, signed Developer ID Application: Anthropic PBC (Q6L2SF6YDW)
A healthy reference bundle (the claude.app bundled inside Claude Desktop, v2.1.181) — for contrast, it has the seal:
claude.app/Contents/
├── _CodeSignature/ # present
├── CodeResources # present
├── Info.plist
└── MacOS/
Gatekeeper / codesign verdict on the malformed bundle:
$ spctl -a -vvv ClaudeCode.app
ClaudeCode.app: code has no resources but signature indicates they must be present
$ codesign --verify --deep --strict --verbose=2 ClaudeCode.app
ClaudeCode.app: code has no resources but signature indicates they must be present
No quarantine attribute (rules out a normal Gatekeeper download prompt):
$ xattr -l ClaudeCode.app
com.apple.macl: … # only this (TCC); no com.apple.quarantine
The embedded signature is genuine Anthropic, with hardened runtime + entitlements — the binary is fine; only the bundle seal is missing:
Identifier=com.anthropic.claude-code
Authority=Developer ID Application: Anthropic PBC (Q6L2SF6YDW)
TeamIdentifier=Q6L2SF6YDW
CodeDirectory … flags=0x10000(runtime)
Entitlements: com.apple.security.device.audio-input,
com.apple.security.automation.apple-events,
com.apple.security.cs.allow-jit,
com.apple.security.cs.allow-unsigned-executable-memory,
com.apple.security.cs.disable-library-validation
Likely root cause
When the installer materializes ClaudeCode.app, it writes Info.plist and the (separately signed) MacOS/claude binary but does not include/produce the bundle's _CodeSignature/CodeResources seal. Because the binary's CodeDirectory expects sealed resources, the bundle fails validation as a whole and macOS marks it "damaged." The shipped/notarized bundle should include the seal (as the Claude Desktop reference bundle does).
Impact
- The native helper app (voice dictation / URL opening / Apple Events) cannot be launched; users get a "damaged — move to Trash" dialog that wrongly implies malware/corruption.
- Confusing because the CLI itself (
~/.local/bin/claude) works perfectly — only the.appwrapper is broken. - Repeated trash-and-recreate by the updater left multiple stale
ClaudeCode.appcopies in~/.Trash.
Suggested fix
Have the installer either (a) ship/copy the complete signed .app bundle including Contents/_CodeSignature/CodeResources, or (b) re-sign the bundle after assembling it on disk so the seal matches the contents. A post-install codesign --verify --deep --strict check would catch this regression.
Local workaround (temporary; undone by next update)
Re-sign the assembled bundle locally, preserving entitlements and hardened-runtime flags:
codesign --force --deep --preserve-metadata=entitlements,flags -s - \
~/.local/share/claude/ClaudeCode.app
After this, codesign --verify --deep --strict reports "valid on disk / satisfies its Designated Requirement" and the app launches (it is not quarantined). Note this…
Note: Content was truncated.
Showing cached comments. Read the full discussion on GitHub ↗
9 Comments
Confirming on CLI 2.1.197 / macOS 26.5.1 (25F80), plus a runtime trigger not captured here: the agents view (FleetView).
Same root cause —
ClaudeCode.appships withoutContents/_CodeSignature/CodeResources;spctl/codesign --verify --strictreturncode has no resources but signature indicates they must be present; nocom.apple.quarantine(onlycom.apple.macl);claude install latest --forceregenerates the same unsealed bundle.Two additional data points:
claude agents/ FleetView) is a concrete trigger, and the symptom is worse than a static dialog. Entering the agents view launchesClaudeCode.app; on macOS 26 this shows the "damaged" dialog and the spawned worker is kernel-killed —[worker crashed (SIGKILL (137)) — respawning…]in a loop, leaving the UI on "Waiting for session to redraw…". So the impact isn't limited to the dictation/URL helper; it breaks the background-agents UI at runtime.codesign --force --deep --preserve-metadata=entitlements,flags -s - ClaudeCode.app(reports valid on disk), simply re-entering the agents view re-materializes the bundle —Contents/MacOS/claudereverts to a fresh hardlink of the currentversions/<x>binary (confirmed by inode), dropping the seal again. So for the agents-view path the workaround doesn't survive a single launch.Repro: CLI 2.1.197, macOS 26.5.1 build 25F80, Apple Silicon, native install (
~/.local/bin/claude→versions/2.1.197).I also had the agents view be a trigger for me.
Reproduced on 2.1.201 (native install, macOS 26.5.1 arm64). Note: the CLI regenerates the unsealed bundle at
~/.local/share/claude/ClaudeCode.appon startup if it's missing, so deleting it or re-running the installer doesn't help — the fresh bundle failscodesign --verifythe same way ("code has no resources but signature indicates they must be present"; noContents/_CodeSignature/CodeResources, Info.plist not bound). Direct exec of the inner binary is fine; only LaunchServices launches hit the "damaged" dialog.Reproduced on CLI 2.1.206, macOS 26.5.1 (25F80), Apple Silicon (arm64), native install.
Same signature:
spctl -a -vvv/codesign --verify --deep --strict→code has no resources but signature indicates they must be present;Contents/_CodeSignature/absent entirely; nocom.apple.quarantine.Additional data point: a full clean reinstall (
rm -rf ~/.local/share/claude/ClaudeCode.app ~/.local/share/claude/versionsfollowed by a freshcurl -fsSL https://claude.ai/install.sh | bash, not justclaude install latest --force) reproduces the identical unsealed bundle — rules out any local/incremental-update corruption, confirms the malformed bundle is baked into the distributed installer/build itself.Also confirms the bundle self-regenerates: deleting
ClaudeCode.appcauses it to reappear (still unsealed) the next time something triggers a LaunchServicesopenagainst it — matches @mprokopov's note above.Another reproduction: macOS 26.5.2 (25F84), CLI 2.1.204, native installer (
~/.local/bin/claude->ClaudeCode.app/Contents/MacOS/claude).Two data points not yet in this thread:
1. Reproduces on a plain terminal exec, not just LaunchServices/agents-view. We point
~/.local/bin/claudedirectly at the bundle's inner binary to get a stable TCC (Full Disk Access) identity across auto-updates — working around the FDA re-prompt issue in #66216. First terminal exec of that path after an auto-update + reboot raised the standard macOS "ClaudeCode.app is damaged and can't be opened. You should move it to the Trash." dialog — so this isn't limited to double-click / dictation-helper / FleetView launches; a bare shell invocation through the bundle path triggers the same Gatekeeper re-validation.2. Timeline evidence that the exec hardlink inside the bundle lags the version drop.
versions/2.1.204landed with mtime2026-07-15 18:23.Contents/MacOS/directory mtime didn't move to reflect a hardlink update until2026-07-16 14:59— a ~20.5h window where the bundle's exec was presumably still linked to2.1.202(that file's nlink is now 1, i.e., detached).Contents/MacOS/claudenow shares inode246047670withversions/2.1.204(nlink=2). So the re-hardlinking runs asynchronously from the version download via the CLI's own update machinery, and the unsealed bundle gets re-materialized either way.Also:
Open Anywayin Privacy & Security unblocks a given build, but the exception is keyed to the binary (cdhash) — it recurs on every auto-update since the hash changes. And confirmed nocom.apple.quarantineanywhere on the bundle (onlycom.apple.macl/com.apple.provenance) — this is a bundle-seal integrity failure, not a quarantine/notarization prompt.Core evidence:
No
_CodeSignature/CodeResourcesanywhere in the bundle — confirms the assembled.appnever gets sealed after the inner binary is signed.Ask: ship the bundle already sealed post-assembly (or re-sign the assembled bundle, not just the inner executable, as a build step), and add
codesign --verify --deep --strictto release smoke tests so this can't regress silently. Fixing it would also unblock the stable-TCC-identity workaround in #66216, which currently can't be used without hitting this bug on every update.Reproduced on CLI 2.1.212, macOS 27.0 beta (26A5378n), Apple Silicon (arm64), native install. Same verdict:
codesign --verify --strict→code has no resources but signature indicates they must be present;Contents/_CodeSignature/absent; nocom.apple.quarantine(onlycom.apple.macl). Trigger was the agents view here too — the dialog fired right as "Agents work started" appeared.One data point I don't see in the thread yet:
Contents/MacOS/claudeis a hardlink to the versioned binary, not a copy or symlink:Two consequences:
codesign --forceon the bundle rewrites the shared inode — i.e. it strips the Developer ID signature off the very binary theclaudelauncher runs. Anyone reaching for the obvious re-sign workaround should break the link first.Workaround that works here (bundle then passes
codesign --verify --strict,gktool scanreports "allowed by system policy", LaunchServices launch succeeds, and the versioned CLI binary keeps its Anthropic signature):Holds until the next auto-update re-links the new version binary into the bundle.
I had this same issue today with Agents View.
Reproduced on CLI 2.1.220, macOS 26.5.2 (25F84), Apple Silicon (arm64), native install — same verdict as the thread (
code has no resources but signature indicates they must be present,Contents/_CodeSignature/absent,Info.plist=not bound, nocom.apple.quarantine, onlycom.apple.macl).Rather than repeat the signature evidence, here is the causal chain out of the shipped 2.1.220 binary — it identifies what creates the bundle, which code path launches it, and why the failure is fatal rather than a soft fallback. I don't see these three functions in the thread yet.
(Recovered via
stringson the 2.1.220 executable. Minified module-local identifiers are renamed for readability —vDe→fs,agt→path,SLt()→dataDir(),Lt()→platform(); function nameso8y/ahp/s8yand the string literals are verbatim.GE()below is left minified since I can't confirm what it tests.)1. It is the CLI at runtime, not the installer
Worth amending the issue title's premise. The bundle is fabricated by the running CLI on startup, which is why @mprokopov's observation (deleting it doesn't help) holds and why @chr0m1ng's clean reinstall reproduces it — the installer is not the author:
mkdir+writeFile(Info.plist)+link(...)— and no_CodeSignature/CodeResourcesis ever written. It also re-links whenever the inode differs, which is why it re-breaks after every auto-update.2. Why the bundle exists at all: a TCC disclaim re-exec
The bundle is a TCC identity wrapper:
macDisclaimResponsibility: truemakes the child own its permission prompts (Info.plistdeclaresNSMicrophoneUsageDescription,NSAppleEventsUsageDescription,NSLocalNetworkUsageDescription) instead of inheriting the terminal's.CLAUDE_BG_TCC_DISCLAIMEDis a one-shot recursion guard, consumed on read.Note there is already an
?? process.execPathfallback for wheno8y()returnsnull— it just never fires here, becauseo8y()succeeds at producing an unsealed bundle.3. Trigger:
runPtyHost, which is why the agents view is the reproThe disclaimed re-exec runs whenever a PTY host starts, unless
--bg-spareappears in the args after--. That pins down why @00mkp, @jeroenvisser101 and @tylerbustard all hit it via the agents view while ordinary sessions don't: an in-terminal session runsbackendType: "in-process"(per~/.claude/teams/session-*/config.json) and never entersrunPtyHost.4. Why it's fatal, not a soft fallback — and the observable cost
The
try { process.execve(...) } catch {}cannot rescue this.execvereplaces the process image; Gatekeeper's kill lands on the replaced image, so there is no surviving JS frame for thecatchto run in. The process is simply gone.In FleetView that surfaces as a completed-but-dead teammate:
So the practical impact is stronger than a cosmetic dialog — dismissing the dialog does not save the spawn; that agent produced nothing. Users reasonably read "cancel and carry on" as harmless because a subsequent spawn or the in-process path does work.
Possibly the same root cause as #74750 (
SIGKILL (137) before init, recorded asstate.detailin~/.claude/jobs/<id>/state.json). Flagging it as worth checking rather than asserting it: that report involves a bg spare, and pers8yabove spares pass--bg-spareand should therefore skipahp()— so it only unifies if the reattach path spawns its PTY host without that flag.Suggested fix
Two narrow options, in preference order:
ClaudeCode.app(as Claude Desktop's innerclaude.appalready is) instead of assembling one from a hardlink at runtime. A flat-signed Mach-O cannot be made bundle-valid by adding anInfo.plistaround it.o8y()'s result.ahp()already has the correct fallback path — haveo8y()returnnullwhenContents/_CodeSignature/CodeResourcesis absent (or when the bundle fails assessment). That degrades to the flatprocess.execPath, losing the dedicated TCC identity but never surfacing a "damaged" dialog or losing an agent spawn. Cheap, and it fails safe.Follow-up with
~/.claude/daemon.logevidence, and a correction to §4 of my comment above.The lost-spawn claim holds: confirmed crashed, never retried
Two independent events on this machine, both
state=crashed, neither followed by a successful retry of that job id:The second correlates exactly with a FleetView pane showing
parallel agent orchestration — SIGKILL (137) before initunder Completed. So the user-visible "completed" teammate did no work, and the spare's stderr is 0 bytes (/tmp/cc-daemon-501/<id>/spare/*.pty.sock.err) — consistent with SIGKILL, and matching the same observation in #74750.Both crashes landed on the first daemon generation after an auto-update (2.1.219 and 2.1.220 respectively). That is suggestive here, because
o8y()mustunlink+ re-linkthe bundle's inner hardlink wheneverprocess.execPath's inode changes — i.e. the bundle is rebuilt, still unsealed, immediately after every upgrade.Correction: I over-claimed the attribution
In §4 above I presented that SIGKILL as the cost of this bug. I should not have — my own
s8ysnippet argues against it for these particular processes:The daemon log shows the hosts involved are spare-pool hosts (
bg spare spawned host,bg spawned … (spare)). If those are spawned with--bg-spare, they skipahp()entirely and never touch the bundle — in which case the unsealed bundle is not what killed them. I flagged exactly this tension in #74750 and should have carried it here instead of asserting the stronger reading.What I can still state without reservation:
runPtyHost→ahp()→o8y()is the code path that launches through it, and the?? process.execPathfallback never fires becauseo8y()succeeds at producing an invalid bundle.What is not established: that the
SIGKILL (137) before initcrashes share this root cause. Whether the claim path re-execs a spare host without--bg-spareis the discriminating question, and only someone with the unminified source can answer it. If the answer is no, these are two co-occurring macOS bugs that happen to surface on the same screen, and #74750 is the right home for the second.Apologies for the noise — better to correct it in-thread than leave a wrong causal claim where maintainers are triaging.