From 50e0acf2b39592df04a8a95c472341c2a19eb27b Mon Sep 17 00:00:00 2001 From: Obada Qawwas Date: Mon, 21 Sep 2026 04:00:09 +0300 Subject: [PATCH 1/2] [FIX] Re-sign the development Electron so macOS stops deleting it - Launching the stock dev binary (node_modules/electron/dist/Electron.app) makes macOS 26 report "Electron.app was not opened because it contains malware" and delete it, so `yarn workspace @subzilla/mac dev` could not start. The download is authentic: it matches Electron's published SHA-256. - scripts/sign-dev-electron.js re-signs it ad hoc under its own identifier before `dev` and `start` launch it, and reinstalls the binary first if macOS has already removed it. Idempotent. - Verified: the same binary that was flagged twice runs and stays on disk once re-signed; `yarn start` boots the app with a clean log. Co-Authored-By: Claude Fable 5.1 --- packages/mac/package.json | 4 +-- packages/mac/scripts/sign-dev-electron.js | 40 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/mac/scripts/sign-dev-electron.js diff --git a/packages/mac/package.json b/packages/mac/package.json index 163a017..40576b3 100644 --- a/packages/mac/package.json +++ b/packages/mac/package.json @@ -5,8 +5,8 @@ "main": "dist/main/index.js", "scripts": { "build": "tsc && yarn run copy-renderer && electron-builder", - "dev": "tsc && yarn run copy-renderer && electron .", - "start": "electron dist/main/index.js", + "dev": "tsc && yarn run copy-renderer && node scripts/sign-dev-electron.js && electron .", + "start": "node scripts/sign-dev-electron.js && electron dist/main/index.js", "copy-renderer": "mkdir -p dist/renderer && cp -r src/renderer/* dist/renderer/", "icon": "node scripts/gen-icon.mjs", "dist": "electron-builder", diff --git a/packages/mac/scripts/sign-dev-electron.js b/packages/mac/scripts/sign-dev-electron.js new file mode 100644 index 0000000..6943d86 --- /dev/null +++ b/packages/mac/scripts/sign-dev-electron.js @@ -0,0 +1,40 @@ +// Re-sign the development Electron binary ad hoc before `electron .` runs. +// +// Observed on macOS 26 with Electron 31.7.7: launching the STOCK Electron binary +// makes macOS report "Electron.app was not opened because it contains malware" +// and delete it. The download is authentic (it matches Electron's published +// SHA-256). Stock 31.0.0 is not flagged, and any re-signed 31.7.7 runs fine. +// The consistent discriminator is the executable's code hash (CDHash): re-signing +// gives the bundle a new one. Most likely Apple blocklists that stock hash because +// real malware ships inside unmodified Electron binaries; that part is inference. +// +// Idempotent and quick; also restores the binary if macOS already removed it. +const { execFileSync, spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +if (process.platform !== 'darwin') process.exit(0); + +const electronDir = path.dirname(require.resolve('electron/package.json')); +const appPath = path.join(electronDir, 'dist', 'Electron.app'); +const binary = path.join(appPath, 'Contents', 'MacOS', 'Electron'); + +if (!fs.existsSync(binary)) { + console.log(' • dev Electron binary is missing (macOS may have removed it) - reinstalling'); + fs.rmSync(path.join(electronDir, 'dist'), { recursive: true, force: true }); + fs.rmSync(path.join(electronDir, 'path.txt'), { force: true }); + execFileSync(process.execPath, [path.join(electronDir, 'install.js')], { stdio: 'inherit' }); +} + +// `codesign -dv` reports on stderr +const info = spawnSync('codesign', ['-dv', appPath], { encoding: 'utf8' }).stderr || ''; + +if (/^Identifier=net\.onyxdev\.subzilla\.dev$/m.test(info)) { + process.exit(0); // already re-signed +} + +execFileSync('codesign', ['--force', '--deep', '--sign', '-', '--identifier', 'net.onyxdev.subzilla.dev', appPath], { + stdio: 'inherit', +}); +execFileSync('codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'inherit' }); +console.log(' • dev Electron re-signed ad hoc (net.onyxdev.subzilla.dev)'); From 5fadb7faa4832a236f8a29c22d2e61eb48dad4bc Mon Sep 17 00:00:00 2001 From: Obada Qawwas Date: Mon, 21 Sep 2026 04:00:11 +0300 Subject: [PATCH 2/2] [DOCS] Correct the recorded cause of the macOS "contains malware" flag - #19 blamed an invalid code signature. That was wrong. `codesign --verify` on STOCK Electron always says "code has no resources but signature indicates they must be present" - including 31.0.0, which ran all along - so it cannot be the cause. - What the evidence supports: macOS flags anything still carrying stock Electron 31.7.7's executable code hash (the packaged app once, a pristine checksum-verified Electron.app twice), never stock 31.0.0, and never a re-signed 31.7.7 (7 of 7 launches). Re-signing replaces the CDHash. That Apple blocklists the stock hash because malware ships inside unmodified Electron binaries is inference; the rest is measured. - The fix from #19 (ad-hoc re-signing in afterPack) is still correct and effective; only its stated reason changes. Updated the hook's comment, CLAUDE.md, the mac rule and both skills. - verify-mac-bundle.sh and the release workflow now also fail a bundle that is validly signed but still identifies as stock Electron. Checked with a negative control. Co-Authored-By: Claude Fable 5.1 --- .claude/rules/mac-app.md | 1 + .claude/skills/releasing-mac-app/SKILL.md | 2 +- .../scripts/verify-mac-bundle.sh | 12 ++++---- .../reviewing-dependency-updates/SKILL.md | 4 ++- .github/workflows/release.yml | 4 ++- CLAUDE.md | 2 +- packages/mac/scripts/adhoc-sign.js | 28 +++++++++++-------- 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.claude/rules/mac-app.md b/.claude/rules/mac-app.md index 8b3858a..4ee5088 100644 --- a/.claude/rules/mac-app.md +++ b/.claude/rules/mac-app.md @@ -12,5 +12,6 @@ paths: - Formatting presets exist in THREE places that must match in name and order: `getFormattingPresets()` in `main/preferences.ts`, and twice in `renderer/js/preferences.js` (`applyPreset`, `updatePresetButtons`), plus the buttons in `preferences.html`. A test enforces this, and that no two presets are identical (a duplicate can never show as selected). - Dropped paths come from `window.subzilla.getPathForFile(file)` (`File.path` is gone in newer Electron). Folder expansion lives in `main/files.ts`: skips hidden entries, symlinks, `.subzilla.` outputs, VobSub `.sub`+`.idx` pairs, and `.txt` inside folders. - Renderer behaviour is tested by executing the real script against a stub DOM (`__tests__/renderer/`), not by grepping source. +- Launch the dev app only through `yarn workspace @subzilla/mac dev` / `start`: they run `scripts/sign-dev-electron.js` first. The stock dev binary gets flagged as malware and deleted by macOS (see CLAUDE.md). - Packaging: config is `electron-builder.yml` only; `scripts/adhoc-sign.js` must stay wired as `afterPack`. `electron-store` must stay < 9 (9+ is ESM-only; main is CommonJS) and `electron-builder` stays on 24 until migrated deliberately. - Releasing is a manual procedure: use the `/releasing-mac-app` skill. diff --git a/.claude/skills/releasing-mac-app/SKILL.md b/.claude/skills/releasing-mac-app/SKILL.md index bd6973b..7fb8ac8 100644 --- a/.claude/skills/releasing-mac-app/SKILL.md +++ b/.claude/skills/releasing-mac-app/SKILL.md @@ -36,7 +36,7 @@ Copy this checklist and tick items as they complete. Stop and report at the firs ## Rules -- Step 4 must print `RESULT: OK`. A bundle with an invalid signature is reported by macOS as "contains malware" and deleted on launch. Never publish or launch a build that fails it. +- Step 4 must print `RESULT: OK`. A bundle that still carries stock Electron's signature (`Identifier=Electron`) is reported by macOS as "contains malware" and deleted on launch. Never publish or launch a build that fails it. - Steps 7 and 10 are outward-facing and hard to undo: get explicit confirmation each time. Never publish the draft yourself. - Do not claim the app "works" from packaging alone. Only step 5 (a human launching it) establishes that. - The app is ad-hoc signed, not notarized. Release notes must tell users to approve it once under System Settings → Privacy & Security, and that auto-update cannot install unsigned updates. diff --git a/.claude/skills/releasing-mac-app/scripts/verify-mac-bundle.sh b/.claude/skills/releasing-mac-app/scripts/verify-mac-bundle.sh index d47d708..ef72297 100755 --- a/.claude/skills/releasing-mac-app/scripts/verify-mac-bundle.sh +++ b/.claude/skills/releasing-mac-app/scripts/verify-mac-bundle.sh @@ -17,12 +17,14 @@ apps=(); for d in "$dist"/mac*/Subzilla.app; do [ -d "$d" ] && apps+=("$d"); don echo "== 1. code signatures" for app in "${apps[@]}"; do - if out="$(codesign --verify --deep --strict "$app" 2>&1)"; then - echo " ok ${app#$dist/} ($(codesign -dv "$app" 2>&1 | sed -n 's/^Identifier=//p'))" + id="$(codesign -dv "$app" 2>&1 | sed -n 's/^Identifier=//p')" + if ! out="$(codesign --verify --deep --strict "$app" 2>&1)"; then + echo " FAIL ${app#$dist/}: $out"; fail=1 + elif [ "$id" = "Electron" ]; then + # Stock Electron's code hash: macOS reports it as 'contains malware' and deletes the app + echo " FAIL ${app#$dist/}: still signed as stock Electron. Is scripts/adhoc-sign.js wired as afterPack?"; fail=1 else - echo " FAIL ${app#$dist/}: $out" - echo " macOS reports such a bundle as 'contains malware' and deletes it. Is scripts/adhoc-sign.js still wired as afterPack?" - fail=1 + echo " ok ${app#$dist/} ($id)" fi done diff --git a/.claude/skills/reviewing-dependency-updates/SKILL.md b/.claude/skills/reviewing-dependency-updates/SKILL.md index 6283578..332987c 100644 --- a/.claude/skills/reviewing-dependency-updates/SKILL.md +++ b/.claude/skills/reviewing-dependency-updates/SKILL.md @@ -52,7 +52,9 @@ yarn workspace @subzilla/mac build It checks the code signature, the Electron download against Electron's official SHA-256, and every bundled package against its npm tarball, without launching anything. `RESULT: OK` is required before anyone launches the build. -If macOS ever reports the app as malware: do not launch anything else. Run the script first. An invalid signature (not a compromised package) caused this once already; the script distinguishes the two. +If macOS ever reports the app (or `Electron.app`) as malware: do not launch anything else, and run the script first. It has happened here, and it was NOT a compromised package: macOS flags anything still carrying stock Electron 31.7.7's code hash, and re-signing fixes it. The script tells the two apart (authentic download + identical-to-npm packages + `Identifier=Electron` means "not re-signed", not "tampered"). + +An Electron version bump changes that stock hash. After one, confirm both the packaged app and `yarn workspace @subzilla/mac dev` still launch. ## 5. Report diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdd7a5d..8956574 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,9 +84,11 @@ jobs: - name: Verify the code signatures working-directory: packages/mac/dist-electron run: | - # An invalid signature makes macOS report the app as malware and delete it + # The bundle must be RE-SIGNED: one still carrying stock Electron's code hash + # is reported by macOS as malware and deleted (see scripts/adhoc-sign.js) for app in mac/Subzilla.app mac-arm64/Subzilla.app; do codesign --verify --deep --strict --verbose=2 "$app" + codesign -dv "$app" 2>&1 | grep -q '^Identifier=net.onyxdev.subzilla$' || { echo "::error::$app still has stock Electron's signature"; exit 1; } done - name: Verify the bundles diff --git a/CLAUDE.md b/CLAUDE.md index 3db2803..46866c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,5 +50,5 @@ yarn workspace @subzilla/mac build # package the a - Adding a strip option touches ~10 places: `IStripOptions`, Zod schema, `ConfigManager` (`KNOWN_PROPERTIES` + defaults), CLI `options.ts` + `strip-options.ts` + `IStripCommandOptions`, mac `preferences.ts` (schema, defaults, presets), `preferences.html`, `preferences.js` (element, listener list, load, save, presets ×2), READMEs. `git grep -n bidiControl` lists them all. - In the mac app, electron-store defaults override `.subzillarc` values (open product question — do not "fix" silently). - `packages/mac/electron-builder.yml` is the only builder config. Never re-add a `"build"` field to `packages/mac/package.json`: it silently shadows the yml. -- The app is ad-hoc signed by `scripts/adhoc-sign.js`. An invalid signature makes macOS report "contains malware" and delete the bundle. Before launching any build: `codesign --verify --deep --strict `. +- macOS reports anything carrying STOCK Electron 31.7.7's code hash as "contains malware" and deletes it — even a pristine, checksum-verified `Electron.app`. Re-signing ad hoc gives it a new hash and it runs. So the packaged app is re-signed by `scripts/adhoc-sign.js` (afterPack) and the dev binary by `scripts/sign-dev-electron.js` (runs before `dev`/`start`). Never launch `node_modules/electron/dist/Electron.app` directly. Before launching any build: `codesign -dv ` must NOT say `Identifier=Electron`. (The "code has no resources…" message from `codesign --verify` on stock Electron is normal and was a red herring.) - macOS has no `timeout` command. When checking that a launched process is alive, use its PID (`kill -0 $PID`); `pgrep -f ` matches your own shell command. diff --git a/packages/mac/scripts/adhoc-sign.js b/packages/mac/scripts/adhoc-sign.js index 05307c5..6451b01 100644 --- a/packages/mac/scripts/adhoc-sign.js +++ b/packages/mac/scripts/adhoc-sign.js @@ -1,19 +1,23 @@ -// electron-builder "afterPack" hook. +// electron-builder "afterPack" hook: re-sign the bundle ad hoc. // -// Without a Developer ID certificate electron-builder skips code signing and -// leaves Electron's ORIGINAL ad-hoc signature on a bundle whose contents it has -// just changed (Info.plist, icon, app.asar). That signature no longer matches: +// Without a Developer ID certificate electron-builder skips code signing, so the +// app ships with STOCK Electron's executable and signature (Identifier=Electron). // -// codesign --verify: "code has no resources but signature indicates they must be present" +// Observed on macOS 26: a bundle carrying stock Electron 31.7.7's code hash is +// reported as "was not opened because it contains malware" and deleted - even a +// pristine Electron.app whose download matches Electron's published SHA-256. +// Stock 31.0.0 is not flagged; any re-signed 31.7.7 runs fine (7 of 7 launches). +// The discriminator is the executable's CDHash, which re-signing replaces. Most +// likely Apple blocklists that stock hash because real malware ships inside +// unmodified Electron binaries - that last step is inference, the rest is measured. // -// Apple Silicon refuses to run code with an invalid signature, and macOS reports -// it as: "Subzilla.app was not opened because it contains malware" — then deletes -// the app. Re-signing ad hoc ("-") makes the signature valid again. It is not a -// substitute for a Developer ID (Gatekeeper still asks users to approve the app -// once), but the app is no longer treated as tampered. +// NOT the cause, despite looking like it: `codesign --verify` on stock Electron +// says "code has no resources but signature indicates they must be present". Every +// stock Electron ships like that, including versions that run without complaint. // -// When a real certificate is configured (CSC_LINK / CSC_NAME), electron-builder -// signs after this hook and simply replaces the ad-hoc signature. +// Re-signing is not a substitute for a Developer ID (Gatekeeper still asks users +// to approve the app once). With a real certificate configured (CSC_LINK / +// CSC_NAME), electron-builder signs after this hook and replaces the signature. const { execFileSync } = require('child_process'); const path = require('path');