From ab7334599bd3a6e9e224a616b3c649c71757e19e Mon Sep 17 00:00:00 2001 From: dallasbpeters Date: Mon, 24 Aug 2026 12:41:57 -0500 Subject: [PATCH] Stop stamping hardened runtime onto ad-hoc builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.0.1 could not be opened by anyone. The DMG installed, the app landed in /Applications, and it exited before drawing a window. The ad-hoc signing step passed --options runtime, matching what electron-builder applies to a certificate-signed build on the reasoning that the two should differ only in the identity. That reasoning is what shipped an unopenable app. Hardened runtime plus an ad-hoc signature is fatal to Electron. Electron validates its helper processes' code signature, that validation needs the task port, and hardened runtime denies it: codesign_util.cc] task_name_for_pid: (os/kern) failure (5) The evidence is a pair of flag sets on one machine. The release was flags=0x10002(adhoc,runtime) and would not start. The dev build, which runs perfectly and has all along, is flags=0x20002(adhoc,linker-signed) — the same ad-hoc signature without hardened runtime. Hardened runtime is mandatory for notarization and buys nothing without a real identity, so it belongs on the signed path only, where it already is. TCC is unaffected: grants key to a stable signature, which this still produces, not to hardened runtime. The step now also fails the build if the flag turns up anyway, because the symptom of getting this wrong is a green release that nobody can open — which is exactly how it shipped the first time. Co-Authored-By: Claude Opus 5 --- .github/workflows/build.yml | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 756356bce..7cdc09f8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -416,17 +416,40 @@ jobs: # false, and the editable-cursor preflight in useScreenRecorder re-opened # the same dialog on every press of record — recording was impossible. # - # Signed with the same runtime and entitlements electron-builder applies, - # so a locally signed build and a certificate-signed one differ only in the - # identity. Both arches on purpose: 26.8.1 only fell back on arm64, which - # left Intel DMGs unsigned for their whole existence. + # Entitlements, but deliberately NOT --options runtime. + # + # This used to match what electron-builder applies to a certificate-signed + # build, reasoning that the two should differ only in the identity. That + # reasoning shipped an app nobody could open. + # + # Hardened runtime plus an ad-hoc signature is fatal to Electron: it + # validates its helper processes' code signature, that validation needs the + # task port, and hardened runtime is what denies it. The app logs + # + # codesign_util.cc] task_name_for_pid: (os/kern) failure (5) + # + # and exits before a window appears. That is what v0.0.1 did — + # `flags=0x10002(adhoc,runtime)`. The dev build, which runs perfectly, is + # `flags=0x20002(adhoc,linker-signed)`: same ad-hoc, no hardened runtime. + # + # Hardened runtime is mandatory for notarization and buys nothing without a + # real identity, so it belongs on the signed path only. TCC is unaffected — + # grants key to a stable signature, which this still produces, not to + # hardened runtime. + # + # Both arches on purpose: 26.8.1 only fell back on arm64, which left Intel + # DMGs unsigned for their whole existence. - name: Ad-hoc sign the .app if: steps.signing.outputs.enabled != 'true' run: | codesign --force --deep --sign - \ - --options runtime \ --entitlements macos.entitlements \ "${{ steps.find_app.outputs.app_bundle }}" + # Fail the build rather than ship another DMG nobody can open. + if codesign -d --verbose=2 "${{ steps.find_app.outputs.app_bundle }}" 2>&1 | grep -q 'flags=.*runtime'; then + echo "::error::the ad-hoc signature carries hardened runtime; the app will not launch" + exit 1 + fi # UNCONDITIONAL. Gated on `enabled == 'true'`, this step never ran for the # RC builds — the only ones that could be unsigned — so the regression