# Distributing CodeBurn Desktop This document describes how to produce distributable macOS, Windows, and Linux builds of the Electron desktop app. Release macOS builds are signed with a Developer ID Application certificate and notarized by Apple (see "macOS code signing and notarization" below); the Windows and Linux builds are **unsigned**. Windows NSIS packages are built and checked by the `Build Windows installer` GitHub Actions workflow; the other desktop packages are still produced by hand. All three targets are produced by `electron-builder`. ## The bundled CLI (no install prerequisite) The packaged app ships its own copy of the `codeburn` CLI and needs nothing installed on the target machine. Packaging stages a self-contained copy of the root repo's CLI into `app/build/cli` (see `scripts/stage-cli.mjs`) — the tsup bundle (`dist/main.js`), its Node-version launcher (`dist/cli.js`), the root `package.json` (the bundle reads its `version`), plus the CLI's production `node_modules` closure. An `afterPack` hook (`scripts/after-pack.cjs`) copies that tree into the app at `Contents/Resources/cli/` after packaging and before signing, so it lands inside the code signature. At runtime a packaged build spawns the bundled CLI with Electron's own binary acting as Node (`ELECTRON_RUN_AS_NODE=1`), so **no Node install is required** — the app is version-matched to itself. `main.ts` sets `CODEBURN_BUNDLED_CLI` to `Resources/cli/dist/launch.js` (a small shim that preserves the argument shape expected by the resident and one-shot CLI paths, then hands off to `main.js`), and `electron/cli.ts` resolves it ahead of any persisted path or `PATH` lookup. A user-installed CLI is only consulted **outside** a packaged build (dev via the Vite dev server) or when explicitly overridden — `CODEBURN_BIN=/abs/path`, or a persisted path file (`Application Support/CodeBurn/codeburn-cli-path.v1`). The full resolution order in `electron/cli.ts` is: `CODEBURN_BIN` → dev repo CLI (Vite) → bundled CLI → persisted path → `PATH`/Homebrew/nvm/volta/asdf. ### Freshness Packaging **always rebuilds the root CLI** before staging: the app `stage-cli` script runs the root `build:cli` (tsup only — no dashboard build, no network `bundle-litellm` fetch), so `dist/main.js` and `dist/cli.js` are regenerated from current `src/` on every `package*` run. A stale global `codeburn` can never ship, and the app can never be older than the JSON surfaces it calls. The bundled CLI's deps are pure JS (no native bindings), so the same staged tree is valid for every arch. ### Why `afterPack`, not `extraResources` electron-builder routes every `node_modules` directory it copies through its production-dependency filter, which keeps only the *app's* own deps — so the bundled CLI's dependency tree gets stripped out of an `extraResources` copy (`filter: ["**/*"]` does not defeat this). `afterPack` copies the staged tree in verbatim, which is the electron-builder-recommended mechanism for adding unpacked files that must not be ASAR-archived. ## The bundled tray app and Capacity Dock (Windows only) The Windows desktop app also carries the Tauri tray app from `windows/`, so installing the desktop app gives the user the tray icon and the Capacity Dock without their having to know that `codeburn menubar` exists. Two switches in the sidebar's bottom-left corner turn each surface off ("Menu bar" is the tray process, "Sidebar" is the dock rail); both default on, and neither appears on macOS or Linux, or in a build with nothing staged. `scripts/stage-menubar.mjs` puts the tray app into `app/build/menubar`, which `build.win.extraResources` ships as `resources/menubar`. It copies rather than downloads, exactly like `stage-cli.mjs`: with no `--from` it takes the local `windows/src-tauri/target/release` output, and CI downloads the `windows-v*` release assets in its own step and passes `--from `. The two routes need different things staged, which is why there are two scripts: - `npm run stage-menubar` (run by `package:win`) stages `CodeBurn.Menubar__x64_en-US.msi` and its `.sha256`. The digest is copied from the release when it is there and computed when it is not, so a local `cargo tauri build` stages the same shape a release does. - `npm run stage-menubar:store` (run by `package:store`) stages `codeburn-menubar.exe` itself, plus any sidecar DLL beside it. **The staged tray app is not the released one, even at the same version.** `build-windows-installer.yml` runs `npm run tauri build` itself and stages the MSI that run produced. `release-menubar-windows.yml` runs its own `npm run tauri build` on a `windows-v*` tag and uploads that MSI as the release asset. Two builds, two binaries: a Tauri MSI is not byte-reproducible (build timestamps, the package GUID and the PDB signature differ per run), so the `CodeBurn.Menubar__x64_en-US.msi` inside a desktop installer and the `windows-v` release asset of the same name are different files with different `.sha256` digests, even off the same commit. Nothing in the install path depends on them matching: the CLI's installer verifies the digest of the file it was handed, and the upgrade rules go by the `DisplayVersion` in the uninstall registry rather than by a hash. What it does mean is that a version number is not a statement about which binary is on a machine, and the release asset is not a way to reproduce or verify what a desktop installer carries. So cut both from one commit when releasing, and say "built by the desktop installer" or "the windows-v asset" rather than treating the version as an identifier for either. ### The NSIS route: install through the CLI, never downgrade `app/electron/menubar.ts` runs at every launch (which is also what covers a desktop-app update, since the staged MSI travels with the app) and hands the staged file to the CLI's own Windows installer by spawning `codeburn menubar --staged-msi `. The path is an argument of the command rather than an environment variable, so nothing a variable in the inherited environment happens to name can reach msiexec; the `CODEBURN_MENUBAR_MSI` variable this replaced is now rejected by the CLI. The install itself is not reimplemented on the Electron side: `src/menubar-installer.ts` owns the uninstall-registry read, the checksum verification, the `msiexec /i ... /passive /norestart` call and the rules that go with a bundled copy. It prints one `CODEBURN_MENUBAR_RESULT ` line that the desktop app reads. The spawn itself is not made at every launch. `msiexec /passive` puts an admin prompt in front of the user, so `companion.v1.json` records the version of the tray app that is on disk and the version of any staged MSI whose install the user declined at that prompt (exit 1602). A launch skips the probe when the recorded tray app is the staged version and is still there, and skips it when this exact staged version was already refused. A newer staged version is always offered, and so is turning the "Menu bar" switch on by hand, which clears the refusal. Only a refusal is remembered. A spawn that timed out, a CLI that was not there and a run that died before printing its result are all transient, so nothing is written down and the install is attempted again at the next launch; recording them would turn one bad launch into a tray app that is never installed. When msiexec exits 3010 the install is real but unfinished: Windows could not replace a file that was in use and has deferred it to the next restart, so the binary on disk is still the previous tray app while the uninstall registry already reports the new version. The desktop app starts nothing in that state and the sidebar corner says "Restart Windows to finish installing the menu bar app." instead. `BundledInstallResult` carries no field for this, so the desktop side reads the installer's own notice line; the optional `rebootRequired` in `MenubarInstallResult` is believed first, for when the CLI starts sending it. Two of those rules matter here: - **Deduplication.** The MSI carries a fixed WiX upgrade code (`windows/src-tauri/tauri.conf.json`, `bundle.windows.wix.upgradeCode`), which is the value Tauri already derived from the product name, so installing the bundled copy over a manual `codeburn menubar` install is an in-place upgrade rather than a second entry in Programs and Features. - **Never downgrade.** If the installed `DisplayVersion` is newer than the bundled one, nothing is installed and the existing app is left alone. **The marker.** The installer writes `%LOCALAPPDATA%\codeburn-menubar\installed-by.json`, crediting `desktop` only when it installed onto a machine that had no tray app, and `manual` when it upgraded one that was already there. An existing marker's verdict is never rewritten. `build/installer.nsh` (wired as `nsis.include`) reads it in the uninstaller and removes the tray app only when the marker says `desktop`, so a tray app the user installed by hand survives an uninstall of the desktop app. Launch at login on this route is a single `HKCU\...\CurrentVersion\Run` value named `CodeBurn`, the same value the tray app's own settings toggle writes (`windows/src-tauri/src/autostart.rs`), so the two can never leave two entries. ### The Store route: no msiexec A packaged app cannot run `msiexec` and would be fighting the package manager if it could, so the AppX build ships `codeburn-menubar.exe` inside the package and launches it from `app\resources\menubar\`. Launch at login is a `windows.startupTask` extension (`build/appx-extensions.xml`, wired as `appx.customExtensionsPath`), which also lets the user turn it off in **Settings > Apps > Startup**; no Run value is written. The tray app detects the package with `GetCurrentPackageFullName` and skips its own update checker entirely, since Store updates cover it. That is why the Store is the recommended Windows install: it is the only Windows route with a signed artifact (Microsoft signs the package at submission) and a real update path. The GitHub downloads stay a developer preview, unsigned and without self-update, until an Authenticode certificate exists. The startup task is Windows' to set, not this app's: reaching one would take the WinRT `Windows.ApplicationModel.StartupTask` API, which Electron has no binding for and which this app ships no native code to reach, and a Run value written beside it would start the tray app twice. So `trayPrefs()` reports `launchAtLoginManaged: true` on this route and the Menu bar pane renders that row as text plus a button that opens `ms-settings:startupapps`, rather than a switch that would silently move nothing. That URL is the one exception in the main process's external-open guard, which is otherwise http(s) only. ### Talking to a running tray app The tray app has no window of its own, so its argv is the control channel: the Tauri single-instance plugin hands a running instance whatever a second launch was started with (`windows/src-tauri/src/lib.rs`). `--quit` exits and `--reload-settings` re-reads the preference files. Turning "Sidebar" off writes `enabled: false` into `~/.config/codeburn/windows-dock.json` and then sends `--reload-settings`, so a running rail disappears at once rather than at the next launch. ## Versioning `app/package.json`'s `version` tracks the CLI's version (root `package.json`) — one CodeBurn version across CLI, menubar, and desktop. Bump it in the same change that bumps the root version; the splash, the About dialog, and the artifact filenames all read it from there. ## Build ```sh npm --prefix app install npm --prefix app run package # macOS, both arm64 and x64 npm --prefix app run package:arm64 # macOS arm64 only (faster on Apple Silicon) npm --prefix app run package:x64 # macOS x64 only npm --prefix app run package:win # Windows NSIS installer, x64 npm --prefix app run package:store # Microsoft Store AppX, x64 (Windows host only) npm --prefix app run package:linux # Linux AppImage, x64 ``` The Linux targets and arches can be widened by calling `electron-builder` directly, all from the same macOS host: ```sh cd app npx electron-builder --linux AppImage deb # x64 AppImage + deb npx electron-builder --linux AppImage deb --x64 # force x64 on an arm64 mac npx electron-builder --linux AppImage deb --arm64 npx electron-builder --linux rpm --x64 # rpm; needs `brew install rpm` (rpmbuild) ``` The `rpm` target is the only one with an extra prerequisite — `fpm` (bundled by electron-builder) shells out to `rpmbuild`, so `brew install rpm` must be present or the build fails with "executable rpmbuild is required". It emits `codeburn-desktop-.x86_64.rpm`, the name the website's Fedora/RHEL download links. `package` runs `npm run stage-cli` (rebuilds the root CLI and stages the self-contained bundle into `app/build/cli`; see "The bundled CLI" above), then `npm run build` (compiles `electron/` with `tsc`, builds the renderer with `vite`), then `electron-builder --mac` (whose `afterPack` hook copies the staged CLI into the app). `package:win` and `package:linux` mirror it exactly, swapping the final flag for `electron-builder --win` and `electron-builder --linux`. Developers can run all three locally on the same macOS host — electron-builder downloads the NSIS and AppImage tooling on first use. Release Windows installers are built by the `windows-latest` workflow described below. ### Artifacts electron-builder writes to `app/release/` (gitignored, like `dist/`): - `CodeBurn--arm64.dmg`, `CodeBurn-.dmg` — installer images - `CodeBurn--arm64-mac.zip`, `CodeBurn--mac.zip` — zipped `.app` bundles - `release/mac-arm64/CodeBurn.app`, `release/mac/CodeBurn.app` — the raw unpacked bundles (arm64 and x64 respectively) - `.blockmap` files alongside each zip/dmg (electron-updater uses the zip blockmaps for differential downloads) - `latest-mac.yml` — the update metadata for both zips, written because both arches build in one run (see "Auto-update" below) Both `dmg` and `zip` targets are built for both `arm64` and `x64` — four artifacts total, not a universal binary. This keeps each download roughly half the size of a universal build. Pick the zip if you just want to unpack and drag to `/Applications`; the dmg gives users the familiar drag-to-Applications installer window. ### Build configuration The `build` block lives in `app/package.json` (small enough not to warrant a separate `electron-builder.yml`): - `appId: "org.agentseal.codeburn-desktop"` — reuses the `org.agentseal.*` prefix from the menubar app's bundle id (`org.agentseal.codeburn-menubar`, see `mac/Scripts/package-app.sh`); there is no `com.codeburn.*` bundle id anywhere in the codebase, so `org.agentseal.*` is the actual house convention. - `productName: "CodeBurn"`. - `files`: only `dist/electron/**/*`, `dist/renderer/**/*`, and `package.json`. electron-builder still adds the production `dependencies` to `app.asar`; the main process needs only `electron-updater` from them (the renderer is a single Vite bundle). (The *bundled CLI* has its own `node_modules`, added to `Resources/cli/` by the `afterPack` hook — see "The bundled CLI" above.) - `afterPack: "./scripts/after-pack.cjs"` — copies the staged CLI bundle (`app/build/cli`) into `Contents/Resources/cli` after packaging and before signing. - `mac.identity: "-"` — forces ad-hoc signing by default. **`identity: null` does NOT ad-hoc sign — it skips signing entirely**, which produces a bundle with a broken/absent seal (`codesign --verify --deep --strict` fails with `code has no resources but signature indicates they must be present`, and Apple Silicon refuses to run it at all). `"-"` is the same ad-hoc identity `mac/Scripts/package-app.sh` falls back to for the menubar app's local builds. This is the local/dev default; signed release builds pass the real Developer ID identity as a CLI override instead of changing this file (see "macOS code signing and notarization" below) — worth baking into `build.mac` directly so a release can't accidentally ship ad-hoc. - `mac.hardenedRuntime: false` — matches the ad-hoc default above (hardened runtime needs a Developer ID signature). Signed release builds turn this on via the same CLI override. - `mac.gatekeeperAssess: false` — skips electron-builder's post-sign `spctl` check, which always fails for an ad-hoc/unnotarized build. A signed, notarized release build passes Gatekeeper's real check regardless of this setting. - `icon: build/icon.png` — a pre-existing 1024x1024 PNG at `app/build/icon.png`. No `.icns` exists in the repo; electron-builder generates one from the PNG at build time. This is the same source PNG used for the app icon; the menubar app has its own separate icon (`assets/menubar-logo.png`, converted to `.icns` in `package-app.sh`). - `directories.output: "release"` — electron-builder's default output dir is `dist`, which collides with this app's existing `tsc`/`vite` build output (`app/dist/electron`, `app/dist/renderer`) that `files` reads from. Using a separate `release/` directory keeps build inputs and packaging outputs apart. ## Windows and Linux builds Developers can cross-build both locally from the same macOS host used for the mac build — no Windows or Linux machine, and no `wine`, is required. Release-authoritative Windows NSIS installers are instead built by the `Build Windows installer` workflow on `windows-latest`. electron-builder 26 embeds the Windows executable's icon/version resources natively and downloads the NSIS and AppImage tooling on first run. ### Windows (`package:win`) `electron-builder --win` produces a single installer in `app/release/`: - **`CodeBurn-Setup-0.9.15.exe`** — the NSIS installer (the version number tracks `package.json`). A `.exe.blockmap` and `latest.yml` are written alongside it; they stay unused while Windows auto-update is off (see "Auto-update" below). Config (`build.win` + `build.nsis`): - `win.target: nsis`, `arch: x64`. - `win.icon: build/icon.png` — electron-builder converts the 1024x1024 PNG to a multi-resolution `.ico` at build time (same source PNG as the mac icon). - `win.extraResources: build/menubar -> menubar`: the staged tray app (see "The bundled tray app and Capacity Dock" above). No `node_modules` is involved, so unlike the CLI this one can use `extraResources`. - `nsis.include: build/installer.nsh`: the uninstall macro that removes the tray app only when the marker says the desktop app installed it. - `nsis.oneClick: false` — an assisted installer with a wizard, so users get an **install-directory choice** instead of a silent one-click install. - `nsis.perMachine: false` — installs per-user (into the user's `AppData`), so **no administrator/UAC elevation** is needed. **The build is UNSIGNED** (no Authenticode certificate). electron-builder logs `signing with signtool.exe`, but with no certificate configured that step is a no-op — the `.exe` ships without a signature. On first run, Windows SmartScreen shows **"Windows protected your PC"**. Users click **"More info" → "Run anyway"** to launch it. This is expected for an unsigned build; the only fix is a purchased code-signing (Authenticode/EV) certificate. **ARM64 builds.** `npm --prefix app run package:win:arm64` builds the installer for ARM64 Windows. It sets `ELECTRON_BUILDER_7Z_FILTER=BCJ`, and that is not optional: 7-Zip compresses ARM64 executables with its ARM64 branch filter by default, the 7-Zip decoder inside the NSIS installer predates that filter, and the installer then silently drops every .exe and .dll while the data files land, leaving an install with no program in it. The x64 build is unaffected because its default filter is one the decoder knows. The per-architecture archives inside a universal installer get the same treatment, so an installer built with both `--x64` and `--arm64` needs the variable too. **The ARM64 installer carries the x64 tray app.** `package:win:arm64` runs the same `npm run stage-menubar` as `package:win`, and there is no ARM64 tray app for it to stage: - `tauri build` builds for the host, so an ARM64 Windows machine produces `CodeBurn Menubar__arm64_en-US.msi`, and `stage-menubar.mjs` matches only `CodeBurn[ .]Menubar__x64_en-US.msi`. Staging on an ARM64 host therefore fails outright with `no CodeBurn.Menubar__x64_en-US.msi` rather than staging the arm64 bundle. What `package:win:arm64` actually ships is the x64 MSI, from an x64 host build or from the `windows-v*` release assets passed with `--from`. - Teaching the staging script the arm64 name would not help on its own. The CLI is what runs the install, and `src/menubar-installer.ts` accepts only the x64 name: its release-asset pattern, `parseWindowsMsiVersion` and the staged-install path all require `_x64_en-US.msi`, and a file named anything else is rejected before msiexec sees it. An ARM64 tray app would need a matching change there and in the tray's own updater, which is a larger piece of work than the packaging script. - What it means in practice: on ARM64 Windows the desktop app installs the x64 tray app, and Windows runs it under x64 emulation. It works, and it is slower and heavier than a native build would be. Nothing misreports its architecture, because the tray app's version and the uninstall-registry entry are both the x64 MSI's own. `build-windows-installer.yml` builds `package:win` on `windows-latest` only, so no released artifact is affected: `package:win:arm64` is a local build today. **Distribution policy.** The Microsoft Store build (Store ID `9P0R4ZL5XMB8`) is the recommended Windows install. This NSIS setup `.exe` and the tray `.msi` under the `windows-v*` releases are a developer preview: unsigned, SmartScreen warns on first run. The desktop installer does not update itself yet (see "Auto-update" below). The tray app installs a newer `.msi` in one click once its updater key is set (`RELEASING.md`); until then it points at the GitHub release and `codeburn menubar --force`. ### Microsoft Store (`package:store`) The Store build is a separate AppX target so the GitHub NSIS installer remains unchanged. AppX packaging requires Windows 10 or newer and is built by the manual `Build Windows Store package` GitHub Actions workflow on `windows-latest`. Download its `CodeBurn-Microsoft-Store` workflow artifact and upload the contained `CodeBurn-Store--x64.appx` file in Partner Center. The manifest identity must exactly match the reserved Partner Center product: - Identity name: `Codeburn.CodeBurn` - Publisher: `CN=3EFA3336-87E1-46F2-9DFA-2EB5A7693F89` - Publisher display name: `Codeburn` - Store ID: `9P0R4ZL5XMB8` The Store package is intentionally unsigned: Microsoft signs it during Store submission. Direct sideloading requires a separate trusted or development certificate. The AppX declares `runFullTrust` (electron-builder's required default for Electron apps), so CodeBurn retains access to the user's local provider session files rather than running in a UWP application sandbox. The tray app ships inside this package too (`app\resources\menubar\`), with a `windows.startupTask` extension for launch at login. `msiexec` is never run on this route. See "The Store route" above. ### Linux (`package:linux`) `electron-builder --linux` produces a single artifact in `app/release/`: - **`CodeBurn-0.9.15.AppImage`** — a self-contained AppImage (no install step, no package manager). Config (`build.linux`): - `linux.target: AppImage`, `arch: x64`. - `linux.category: "Development"` — the freedesktop menu category. - `linux.icon: build/icon.png` — reuses the same source PNG. - `linux.maintainer: "AgentSeal "` — matches the root `package.json` author. - `linux.executableName: "codeburn"` — the binary name inside the AppImage (lowercase, no spaces), distinct from the `CodeBurn` product name. After downloading, the AppImage must be made executable before it will run: ```sh chmod +x CodeBurn-0.9.15.AppImage ./CodeBurn-0.9.15.AppImage ``` The build logs one benign warning — `desktopName is not set` — which only affects how some desktop environments group the app's windows in the taskbar/dock; it does not affect packaging or launch. ## Releases When a maintainer cuts a desktop release, the GitHub tag convention is: ``` desktop-v # e.g. desktop-v0.9.15 ``` This mirrors the menubar's `mac-v` convention (see `../RELEASING.md`) and keeps the desktop app's tags in their own namespace, separate from the CLI (`v`) and the menubar (`mac-v`). Pushing a `desktop-v` tag runs the `Build Windows installer` workflow on `windows-latest`. The workflow requires the tag version, root package version, and app package version to agree, and it fails unless the build emits exactly one `CodeBurn-Setup-.exe` and one matching `.exe.blockmap` at the top level of `app/release/`. It uploads those exact top-level filenames as the `CodeBurn-Windows-Installer` Actions artifact. The workflow has read-only repository permissions and does **not** publish release assets automatically. Artifacts are retained for 30 days. Before publishing the GitHub Release, the release owner must download that workflow artifact and manually upload both Windows files along with the four macOS `.dmg`/`.zip` files, `CodeBurn-.AppImage`, `codeburn-desktop__amd64.deb`, and `codeburn-desktop-.x86_64.rpm`, plus `latest-mac.yml` (from the `CodeBurn-macOS` artifact) and `latest-linux.yml` (from the Linux build). Confirm the live release contains every required platform asset before announcing it. The website's download links **pin that tag** in their URLs, so a release with a missing installer is broken even when another Windows distribution channel is available. The Windows installer uses an explicit `nsis.artifactName` of `CodeBurn-Setup-${version}.${ext}`. Publishing the Release triggers a read-only live-asset check. If the files are uploaded afterward, rerun the workflow manually with `release_tag` set to the existing `desktop-v` tag and require the verification job to pass. ## Auto-update The app uses electron-updater with the `generic` provider pointed at the fixed `update-feeds` release (`build.publish` in `package.json`), not GitHub's `/releases/latest`, which the CLI, menubar, tray and desktop lines share. The `publish-update-feeds` job in `build-windows-installer.yml` rewrites the file names in `latest*.yml` to absolute `desktop-v` URLs and uploads them there, after the live-asset check passes. Release order is in `RELEASING.md`. Who updates itself (`app/electron/auto-update.ts`): - macOS, signed release builds: Update, then Restart to update. Ad-hoc local builds fail the Squirrel signature check and fall back to the download link. - Linux AppImage only (`APPIMAGE` is set). deb, rpm, snap and Flathub keep the link banner or their store. - Windows NSIS: off behind `WINDOWS_AUTO_UPDATE` until a trust model is chosen (`RELEASING.md`, "Windows NSIS auto-update"). The Store build never checks. ## Verifying a build ```sh codesign -dv --verbose=2 app/release/mac-arm64/CodeBurn.app codesign --verify --deep --strict app/release/mac-arm64/CodeBurn.app spctl --assess --type execute --verbose app/release/mac-arm64/CodeBurn.app ``` A local ad-hoc build shows `Signature=adhoc`; a signed release build shows `Authority=Developer ID Application: Resham Joshi (XRVP7P7F9M)` and `spctl` reports `source=Notarized Developer ID` once the notarization ticket is stapled. Either way expect a real `Identifier=org.agentseal.codeburn-desktop` and `Sealed Resources` present, and the deep-verify command should exit 0. To smoke-test that the packaged renderer actually loads (the classic failure is a white screen from a wrong `loadFile` path once assets are behind `app.asar`), launch the built binary directly and confirm the process tree stays up and the main process logs no `did-fail-load` errors: ```sh "app/release/mac-arm64/CodeBurn.app/Contents/MacOS/CodeBurn" --user-data-dir=/tmp/codeburn-smoke ``` A healthy launch spawns `CodeBurn`, `CodeBurn Helper` (gpu-process), `CodeBurn Helper` (utility/network), and `CodeBurn Helper (Renderer)` processes and keeps running with no stderr output. `main.ts`'s `did-fail-load` handler (`console.error('Renderer failed to load ...')`) prints to that same stderr if the packaged `loadFile(path.join(__dirname, '..', 'renderer', 'index.html'))` path is ever wrong. ## macOS code signing and notarization Release macOS builds — the desktop `.dmg`/`.zip` files and the `CodeBurnMenubar-.zip` menubar app — are signed with the Developer ID Application certificate **"Developer ID Application: Resham Joshi (XRVP7P7F9M)"** (team `XRVP7P7F9M`), built with hardened runtime **on**, and notarized by Apple via `xcrun notarytool` (using the `codeburn-notary` keychain profile), then stapled. `spctl --assess` reports `source=Notarized Developer ID`, and the app opens with **no Gatekeeper warning at all** — no right-click → Open, no quarantine removal, no "unidentified developer" dialog. ### Entitlements Hardened runtime needs a handful of entitlements or Electron's JIT-compiled renderer fails to start: - `com.apple.security.cs.allow-jit` - `com.apple.security.cs.allow-unsigned-executable-memory` - `com.apple.security.cs.allow-dyld-environment-variables` - `com.apple.security.cs.disable-library-validation` ### Two gotchas - **electron-builder rejects the `Developer ID Application:` prefix in the identity argument.** Pass the bare certificate common name — `"Resham Joshi (XRVP7P7F9M)"` — not the full `"Developer ID Application: Resham Joshi (XRVP7P7F9M)"` string `codesign` itself accepts. - **electron-builder notarizes the `.app`, not the `.dmg`.** Once `electron-builder --mac` finishes signing and notarizing the app bundle, the `.dmg` it produces around that app still needs its own codesign + notarize + staple pass: ```sh codesign --force --sign "Resham Joshi (XRVP7P7F9M)" CodeBurn--arm64.dmg xcrun notarytool submit CodeBurn--arm64.dmg --keychain-profile codeburn-notary --wait xcrun stapler staple CodeBurn--arm64.dmg ``` (repeat for the `x64` dmg). ### CI build `.github/workflows/build-desktop-mac.yml` runs on every `desktop-v*` tag: it builds both arches in one run, signs with the Developer ID from `MACOS_CERT_P12_BASE64`, notarizes the apps and the dmgs with the App Store Connect API key, verifies them, and uploads a `CodeBurn-macOS` artifact (dmgs, zips, blockmaps, `latest-mac.yml`) for the release owner to attach. `build.mac` carries the entitlements; the workflow passes the identity and hardened runtime as overrides. ### Release builds vs. the committed config `app/package.json`'s committed `build.mac` still declares `identity: "-"` and `hardenedRuntime: false` — the ad-hoc/local-dev default described in "Build configuration" above. A signed release build passes the real Developer ID settings as `electron-builder` CLI overrides instead of changing that file: ```sh npx electron-builder --mac \ -c.mac.identity="Resham Joshi (XRVP7P7F9M)" \ -c.mac.hardenedRuntime=true \ -c.mac.entitlements=build/entitlements.mac.plist \ -c.mac.entitlementsInherit=build/entitlements.mac.plist \ -c.mac.gatekeeperAssess=true ``` This works, but it is easy to forget on a manual release — baking these settings into `build.mac` directly (falling back to ad-hoc only when no Developer ID identity is present in the keychain) would remove the risk of a release accidentally shipping unsigned. The menubar app is signed, notarized and stapled in CI by `.github/workflows/release-menubar.yml` (see RELEASING.md for the secrets). For a local signed build, `mac/Scripts/package-app.sh` takes the identity in `CODESIGN_IDENTITY` and notarizes and staples the `.app` before zipping when `NOTARY_KEY_PATH`, `NOTARY_KEY_ID` and `NOTARY_ISSUER_ID` are also set. ### Ad-hoc is still the fallback Building locally with no Developer ID identity in the keychain (or in CI, where none is configured) falls back to ad-hoc signing, same as before: enough to satisfy the kernel's code-signing check on Apple Silicon, but not Gatekeeper. A quarantined ad-hoc build still shows the "Apple could not verify..." / "is damaged and can't be opened" dialogs, and stripping the quarantine attribute (`xattr -cr /Applications/CodeBurn.app`) is still the workaround for a build tested this way. This path is for local development only — the distributed release builds are signed and notarized and never hit it. ### Folder-access prompts CodeBurn requests access to folders like Documents, Desktop, and Downloads (via `mac.extendInfo` in `app/package.json`) to read local AI coding tool session logs. Signed release builds use the stable Developer ID identity above, so macOS TCC treats every release build as the same app and folder grants persist across updates. This only regresses for an ad-hoc/local-dev build: with no stable Developer ID, TCC treats each rebuild as a new app identity and re-prompts for folder access every time.