macos-notarize
v1.0.0Notarize and staple a Developer-ID-signed macOS app or .dmg for distribution outside the App Store; submit with notarytool --wait using a stored keychain credential profile, staple the ticket, verify with stapler/spctl, and read the notary log on rejection. macOS + Xcode only.
by VladoIvankovic · 0 installs · updated 6/16/2026
Install
/skills install VladoIvankovic/macos-notarizeRuns in any Codeep session (TUI, Zed, or VS Code extension). Writes to .codeep/skills/macos-notarize/SKILL.md in your project.
SKILL.md
---
name: macos-notarize
description: Notarize and staple a Developer-ID-signed macOS app or .dmg for distribution outside the App Store; submit with notarytool --wait using a stored keychain credential profile, staple the ticket, verify with stapler/spctl, and read the notary log on rejection. macOS + Xcode only.
allowed-tools: [execute_command, read_file, list_files, search_code]
triggers: ["notarize the app", "notarytool submit", "staple the dmg", "Developer ID notarization", "gatekeeper rejects my build", "notary log invalid"]
version: 1.0.0
author: Codeep
---
Notarize and staple a Developer-ID-signed macOS app or `.dmg` so Gatekeeper opens it without warnings off the App Store. macOS + Xcode/command-line-tools required. Run every command via `execute_command` (it allows `xcrun`/`xcodebuild`/`codesign`). Operate on absolute paths.
## Prerequisites (verify first)
1. A **Developer ID Application** cert is installed: `security find-identity -v -p codesigning` must list one. If absent, stop — the user must create it in Xcode ▸ Settings ▸ Accounts ▸ Manage Certificates. You cannot notarize an ad-hoc or Apple-Development signature.
2. A **notarytool credential profile** exists in the keychain. Find the profile name (this repo uses `codeep-notary` — grep for it: `search_code "notarytool"` / `search_code "keychain-profile"`). If none exists, the user runs once: `xcrun notarytool store-credentials <profile> --apple-id <id> --team-id <TEAMID> --password <app-specific-password>` (use an app-specific password from appleid.apple.com, not the Apple ID password).
3. The artifact is **already Developer-ID signed with a hardened runtime** and a secure timestamp. Confirm: `codesign -dvvv --verbose=4 <app>` should show `runtime` in `flags=` and `Authority=Developer ID Application`. If not, re-sign: `codesign --force --options runtime --timestamp --sign "Developer ID Application" <app>` (sign nested frameworks/helpers inside-out first).
## Steps
1. **Package for upload.** notarytool needs a `.zip`, `.dmg`, or `.pkg` — never a bare `.app`. Zip an app with: `/usr/bin/ditto -c -k --keepParent <app> <out.zip>`. A `.dmg`/`.pkg` is submitted as-is.
2. **Submit and wait** for the verdict (one call, blocks until done): `xcrun notarytool submit <artifact> --keychain-profile <profile> --wait`. Read its output: status must be `Accepted`. Capture the `id` (submission UUID) for step 4.
3. **Staple the ticket** onto the real artifact (the `.app` and/or `.dmg`, not the throwaway zip): `xcrun stapler staple <app-or-dmg>`. For a `.dmg`, sign it (`codesign --force --timestamp --sign "Developer ID Application" <dmg>`), notarize the `.dmg`, then staple the `.dmg`.
4. **Verify** offline-validity: `xcrun stapler validate <artifact>` (expect `The validate action worked`) and `spctl -a -vvv --type execute <app>` (expect `accepted` + `source=Notarized Developer ID`). For a `.dmg` use `spctl -a -t open --context context:primary-signature -vvv <dmg>`.
## On failure
- Status `Invalid` or `Rejected`: pull the detailed log with the UUID from step 2: `xcrun notarytool log <submission-id> --keychain-profile <profile>`. Read the JSON `issues[]` — each entry names the offending path + reason.
- Common fixes: `The binary is not signed with a valid Developer ID` → re-sign with the Developer ID cert; `does not have a secure timestamp` → add `--timestamp`; `not signed with a hardened runtime` → add `--options runtime`; nested binary unsigned → sign every `.dylib`/framework/helper inside-out before re-zipping and resubmitting.
- `Error: HTTP status code: 403` / auth failures → the credential profile is wrong or the app-specific password was revoked; re-run `store-credentials`.
- `stapler staple` → `Error 65 / could not find ticket`: notarization wasn't actually `Accepted`, or you stapled before submitting — re-check step 2.
## Notes
- This repo already ships the full pipeline at `Codeep-MacOs/scripts/notarize-developer-id.sh` (archive → export → notarize app → notarize dmg → staple → Sparkle appcast). Prefer running it via `execute_command` over hand-rolling steps; `read_file` it first to reuse `NOTARY_PROFILE`, the export plist, and `make-dmg.sh`.
- Stapling embeds the ticket so Gatekeeper passes **offline**. Always staple before distributing; an un-stapled but notarized app only passes when the machine can reach Apple.
- Use `--wait` so the agent gets a synchronous verdict; without it you must poll `xcrun notarytool info <id> --keychain-profile <profile>`.
## Done when
`xcrun stapler validate` reports success AND `spctl -a -vvv` reports `accepted` / `source=Notarized Developer ID` on the final distributable artifact.