The compiled macOS omp binaries shipped on GitHub Releases are signed with a Developer ID Application certificate and notarized by Apple. This makes them Gatekeeper-acceptable and is the prerequisite for an official Homebrew submission (see #776).

Signing happens in CI, in the release_binary job’s darwin matrix legs (.github/workflows/ci.yml), via scripts/ci-macos-sign.sh. It auto-skips until the APPLE_* repository secrets below are configured, so releases keep working (ad-hoc signed, as before) in the meantime.

How it works

  1. ci:release:build-binaries builds and ad-hoc signs the binary (so it can run on the build runner).
  2. scripts/ci-macos-sign.sh then:
    • imports the Developer ID cert into a throwaway keychain;
    • re-signs with --options runtime --timestamp (hardened runtime + secure timestamp) and --entitlements scripts/macos-entitlements.plist;
    • runs --version and --smoke-test under the new signature to fail fast;
    • notarizes the binary via notarytool submit --wait.
  3. release_github_verify re-downloads the published arm64 asset and asserts it is not ad-hoc, passes codesign --verify --strict, and boots cleanly.

Why the entitlements are mandatory

The binary is a Bun single-file executable, so the hardened runtime needs:

EntitlementReason
com.apple.security.cs.allow-jitJavaScriptCore JITs at runtime.
com.apple.security.cs.allow-unsigned-executable-memoryJSC executable memory pages.
com.apple.security.cs.disable-library-validationomp extracts its native addon (pi_natives.<triple>.node) and other optional dylibs to a runtime cache and dlopen()s them. They do not share the main binary’s Team ID, so without this the hardened runtime aborts with β€œmapping process and mapped file have different Team IDs” β€” breaking effectively every command.

Without disable-library-validation, a signed+notarized binary signs and notarizes fine but fails at first real use. scripts/ci-macos-sign.sh runs --smoke-test after signing specifically to catch this before notarizing.

Stapling limitation (important)

A bare Mach-O executable cannot be stapled (stapler only supports .app/.pkg/.dmg). The binary is genuinely notarized β€” notarytool returns Accepted and the ticket exists on Apple’s servers keyed to its cdhash β€” but because there is no stapled ticket, a direct spctl -a -t exec assessment reports rejected / source=Unnotarized Developer ID. This is expected and is not a signing or credential failure.

What this means in practice:

  • curl https://omp.sh/install | sh β€” curl sets no quarantine bit, so Gatekeeper is never consulted; the binary just runs. βœ…
  • Homebrew formula installs β€” Homebrew does not quarantine formula files, so Gatekeeper is never consulted. βœ…
  • Anything that quarantines the binary (a browser download, or a Homebrew cask) and is assessed offline will be blocked, because there is no stapled ticket. For that route, wrap the binary in a stapleable, notarized .pkg or .dmg (xcrun stapler staple works on those). That is a follow-up and is not required for the curl/formula paths.

Required GitHub secrets

Add these under Settings β†’ Secrets and variables β†’ Actions (repo secrets). All five secrets (cert, password, and API key trio) must be present for signing to engage.

SecretWhat it is
APPLE_CERTIFICATE_P12base64 of the exported Developer ID Application .p12 (cert + private key).
APPLE_CERTIFICATE_PASSWORDpassword you set when exporting the .p12.
APPLE_API_KEY_IDApp Store Connect API Key ID.
APPLE_API_ISSUER_IDApp Store Connect API Issuer ID (UUID).
APPLE_API_KEYbase64 of the App Store Connect .p8 private key.

Producing the credential files

Drop these into a working directory (default ~/omp-signing):

FileHow
*.p12Keychain Access β†’ right-click your Developer ID Application: … identity (the entry that expands to a cert with a private key) β†’ Export… β†’ save as .p12 and set a password.
p12-password.txtthe password you just set on the .p12.
AuthKey_<KEYID>.p8App Store Connect β†’ Users and Access β†’ Integrations β†’ App Store Connect API β†’ create a key (Account Holder role also allows API cert creation; Developer is enough for notarization) β†’ download once (non-recoverable).
issuer-id.txtthe Issuer ID (UUID) shown above the keys table.
key-id.txtoptional β€” the Key ID; otherwise read from the .p8 filename.

The App Store Connect API key is the one credential that cannot be minted from a CLI β€” it is the bootstrap credential for the API itself, and the .p8 downloads exactly once. Everything else is local.

Uploading (no value leaves disk)

scripts/ci-macos-upload-secrets.sh validates the files (opens the .p12 with your password, sanity-checks the .p8) and pipes each value to gh secret set over stdin β€” no secret is ever printed to the terminal, argv, or shell history:

scripts/ci-macos-upload-secrets.sh ~/omp-signing --dry-run   # validate first
scripts/ci-macos-upload-secrets.sh ~/omp-signing             # upload all five
gh secret list --repo can1357/oh-my-pi                       # confirm

Re-run it whenever the certificate is renewed.

Finding your signing identity / Team ID (sanity check)

security find-identity -v -p codesigning
# e.g. "Developer ID Application: Your Name (TEAMID1234)"

The script selects the first Developer ID Application identity automatically; you do not need to store the identity string or Team ID as a secret.

Local dry run

You can exercise the full sign+notarize path locally (real cert + API key) by exporting the five env vars and running:

RELEASE_TARGETS=darwin-arm64 bun run ci:release:build-binaries
APPLE_CERTIFICATE_P12=… APPLE_CERTIFICATE_PASSWORD=… \
APPLE_API_KEY_ID=… APPLE_API_ISSUER_ID=… APPLE_API_KEY=… \
  bash scripts/ci-macos-sign.sh packages/coding-agent/binaries/omp-darwin-arm64