Desktop App Distribution skill for Codex
No Gatekeeper or SmartScreen surprises. Codex signs nested binaries inside-out with hardened runtime and timestamps, notarizes and staples the DMG, and signs Windows executables through an HSM-backed provider since key files are no longer issued. It picks installer formats per platform, sets up beta and stable update feeds with staged rollout and separately backed-up update keys, and asks before each publish.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- desktop-app-distribution Installs to ~/.agents/skills/desktop-app-distribution/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install desktop-app-distribution The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project desktop-app-distribution Installs to .agents/skills/desktop-app-distribution/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/desktop-app-distribution.zipunzip -o desktop-app-distribution.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) desktop-app-distribution PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $desktop-app-distribution, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Ships desktop apps to users on macOS, Windows and Linux - Developer ID signing with hardened runtime, notarytool and stapling for apps, DMGs and pkgs, Windows Authenticode with HSM or cloud signing and SmartScreen reputation, MSIX, MSI and NSIS installers, AppImage, deb, rpm and Flatpak, signed auto-update feeds (Sparkle, electron-updater, Tauri updater, Squirrel), beta and stable channels, staged rollouts and rollback. Use when an app is blocked by Gatekeeper or SmartScreen, when setting up release signing in CI, choosing installer formats, or adding update channels.
Use it when
- Gatekeeper or SmartScreen blocks or warns on your installer
- Setting up release signing and notarization in CI
- Choosing DMG, pkg, MSI, MSIX, NSIS, AppImage, deb or Flatpak
- Adding update feeds, channels or a rollback plan
Use something else when
- Shipping an iOS app through TestFlight and the App Store: use ios-app-store-release
- Electron security and IPC design rather than release packaging: use electron-apps
How to check the result
stapler validate, spctl and signtool verify pass in CI, and installers downloaded through a browser open without warnings on clean macOS and Windows VMs.
The mistake it prevents
Testing a locally built or scp-copied app, which never gets the quarantine flag, so missing notarization goes unnoticed until users download it.
Done well means: a user downloads the installer in a browser and opens it on a clean machine with no security warning beyond the normal first-launch prompt, every binary carries a timestamped signature, updates are signed with a key separate from the OS certificate, and a bad release can be halted before it reaches everyone.
Before changing anything
- Read
AGENTS.md, then the packaging config (electron-builder.yml,forge.config.*,tauri.conf.json, Xcode export options, WiX or NSIS scripts) and the release workflow. - Inventory signing identities: Apple Developer team and its Developer ID Application (and Installer, for pkg) certificates; Windows code signing provider and where the key lives; update signing keys (Sparkle EdDSA, Tauri minisign, electron-builder feeds).
- Inventory the current release path: where artifacts are hosted, how the update feed is generated, how many users are on each version.
- Releasing reaches every installed user, and certificate actions (revoking, re-issuing) can break existing installs. Confirm each publish, feed update and certificate change with the user explicitly.
- Notarization, timestamp servers and cloud signing need network; signing steps usually run in CI, not in a sandboxed local session.
macOS: sign, notarize, staple
Order matters: sign nested code inside-out, then the app, then the container, then notarize the container and staple.
# 1. Sign (Xcode export with method developer-id does this; manual form for other toolchains)
codesign --force --options runtime --timestamp \
--entitlements build/entitlements.plist \
--sign "Developer ID Application: Example Ltd (ABCDE12345)" "build/Example.app"
# 2. Package
hdiutil create -volname Example -srcfolder build/Example.app -ov -format UDZO build/Example.dmg
codesign --timestamp --sign "Developer ID Application: Example Ltd (ABCDE12345)" build/Example.dmg
# 3. Notarize with an App Store Connect API key, then staple
xcrun notarytool submit build/Example.dmg --key "$ASC_KEY_PATH" --key-id "$ASC_KEY_ID" --issuer "$ASC_ISSUER_ID" --wait
xcrun stapler staple build/Example.dmg
# 4. Check as Gatekeeper would
xcrun stapler validate build/Example.dmg
spctl -a -vvv -t open --context context:primary-signature build/Example.dmg
codesign --verify --deep --strict --verbose=2 build/Example.app
--options runtime(hardened runtime) and--timestampare required for notarization.- Do not sign with
--deep; it applies one set of entitlements to everything. Sign each helper, framework and login item individually (Electron and Tauri tooling does this). - On rejection, read
xcrun notarytool log <id> ...; the usual causes are an unsigned nested binary, a missing timestamp or a missing hardened runtime. - A
.pkgneeds the Developer ID Installer certificate:productbuild --sign "Developer ID Installer: ...".
Windows: Authenticode
Since mid-2023, publicly trusted code signing keys must live in hardware (HSM, token or
cloud HSM); .pfx files on build agents are no longer issued.
| Option | Fits |
|---|---|
| Microsoft’s managed service (Azure Trusted Signing, being renamed Artifact Signing) | Organizations eligible for it; integrates with signtool via a dlib |
| CA cloud signing (DigiCert KeyLocker, SSL.com eSigner, others) or Azure Key Vault + AzureSignTool | Existing OV/EV certificate |
| Hardware token on a dedicated machine | Small teams with low release volume; poor for CI |
| Microsoft Store (MSIX submitted to the Store) | Store signs the package; no certificate needed for Store delivery |
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /sha1 <cert-thumbprint> build\ExampleSetup.exe
signtool verify /pa /v build\ExampleSetup.exe
- Sign every executable and DLL you ship, and the installer itself.
- SmartScreen warnings fade as a certificate builds reputation through clean downloads; EV certificates no longer skip that process. Keep the same certificate subject across renewals and warn the user that a brand-new certificate shows warnings at first.
- Always timestamp; without it, signatures become invalid when the certificate expires.
Installer formats
| Platform | Default | When else |
|---|---|---|
| macOS | Signed, notarized DMG with the app | .pkg when installing system components, launch daemons or for MDM deployment |
| Windows | NSIS (per-user, no admin) or Squirrel via your framework | MSI for enterprise GPO/Intune; MSIX for Store and clean install/uninstall |
| Linux | AppImage for direct download + deb/rpm | Flatpak on Flathub for desktop users; Snap if Ubuntu-centric |
Also publish to WinGet (manifest PR to microsoft/winget-pkgs) and Homebrew (cask) when
the audience is developers; both pick up updates from your release URLs.
Update feeds and channels
| Stack | Mechanism | Signing |
|---|---|---|
| Native macOS | Sparkle 2 appcast | EdDSA (generate_keys, sign_update) plus Developer ID |
| Electron | electron-updater (builder) or Squirrel / update.electronjs.org (Forge) | OS code signing; builder also checks publisher on Windows |
| Tauri | tauri-plugin-updater with latest.json | Tauri signer key plus OS signing |
| Linux packages | Signed apt/yum repository, Flathub | GPG repo key |
- Run at least two channels:
beta(opt-in, staff and volunteers) andstable. - Use staged rollout where the mechanism supports it (Sparkle phased rollout,
electron-updater
stagingPercentage, or serving the new feed to a percentage of clients from your own endpoint). Watch crash and error rates before widening. - Rollback: clients rarely downgrade automatically. Halt by reverting the feed to the previous version, then ship a fixed N+1 with a higher version number.
- Update signing keys are as critical as certificates: lose one and existing installs can never update. Keep them in a secret manager with an offline backup and named owners.
- Keep the update feed and artifacts on HTTPS with immutable versioned URLs.
Release workflow
- Version bumped consistently (app, installer metadata, feed)
- Build each OS on its own runner; collect artifacts
- Sign macOS app, package, notarize, staple; sign Windows binaries and installer
- Verify signatures (commands above) in CI; fail the job on any error
- Smoke test installers on clean VMs (no developer certificates installed)
- Ask the user to confirm, then publish artifacts and the beta feed
- After the beta soak period and a second confirmation, publish to stable with staged rollout
- Record checksums (SHA-256) on the release page
Gotchas
- Downloading from a browser adds the quarantine attribute; testing an app copied over
scpor built locally skips Gatekeeper entirely and proves nothing. - Apple Developer ID certificates last five years; set calendar reminders well before expiry and before the Apple Developer membership renewal date.
- Changing the macOS bundle identifier or Windows AppUserModelID breaks updates, notifications and stored settings.
- Antivirus false positives hit unsigned or newly signed NSIS installers; submit samples to the major vendors before a large launch.
- Universal macOS builds need every native dependency for both arm64 and x86_64; check with
lipo -archs.
Output
**Release:** <version> channels: beta -> stable (rollout %)
**macOS:** signed (identity), notarization id + status, stapled
**Windows:** signing provider, signtool verify result
**Linux:** formats, repo or store
**Feed:** <URL, signature key owner, previous version kept for rollback>
**Smoke tests:** <OS, clean VM, result> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Desktop App Distribution"
short_description: "No Gatekeeper or SmartScreen surprises"
default_prompt: "$desktop-app-distribution Users get an unidentified developer warning when opening our Mac DMG" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.