Notarize 1.0
First public release.
Notarize is a native macOS GUI for Apple's notarization workflow. Drop an app or an installer package on it, pick a credential profile, and click Notarize: it copies, signs, submits to Apple's notary service, waits for the verdict, staples the ticket, and runs the Gatekeeper assessment. The four main stages can also be run one at a time. Notary credentials are set up once through a guided wizard and reused across every target.
Notarize ships none of the Apple tools and reimplements none of them. It drives the ones already on your Mac (codesign, productsign, pkgutil, xar, ditto, spctl, security, xcrun notarytool, xcrun stapler) and streams their output into the log.
Requirements
| Requirement | Notes |
|---|---|
| macOS 14.6 (Sonoma) or later | Enforced at launch |
| Xcode Command Line Tools | Provides notarytool and stapler. Install with xcode-select --install, or install Xcode. |
| App Store Connect credentials | An Apple ID with an app-specific password and a Team ID, or an App Store Connect API key. Stored once as a notary keychain profile. |
| Developer ID Application certificate | Optional. Needed only if you want Notarize to sign apps for you. |
| Developer ID Installer certificate | Optional. Needed only if you want Notarize to sign installer packages for you. |
A target that was already signed elsewhere can be notarized here without any certificate in your keychain.
What is in 1.0
Apps and installer packages, both handled correctly
Drop a .app bundle or a flat installer package, .pkg or .mpkg. The two kinds take the same route through the window, and Notarize picks the right tool at every step that touches a signature or an upload:
| Application bundle | Installer package | |
|---|---|---|
| Certificate | Developer ID Application | Developer ID Installer |
| Signed with | codesign, hardened runtime, entitlements |
productsign |
| Uploaded as | a ditto zip |
the package itself |
| Signature check | codesign -dv, codesign --verify |
pkgutil --check-signature |
| Gatekeeper check | spctl --type execute |
spctl --type install |
The two kinds keep separate default signing identities, because a Developer ID Application certificate and a Developer ID Installer certificate are not interchangeable.
The full pipeline in one click
The Notarize button runs, in order: copy to the output folder when one is set, sign, check nested code, prepare the upload, submit with notarytool submit --wait, fetch the notary log on failure, staple the ticket, and assess with spctl. The nested-code check runs even when the signing stage was skipped, so nothing reaches the notary service unexamined.
The step rail shows the live status of each of its four stages, so a failed run shows where it stopped. The log streams as it goes, and Stop cancels a full run or a submission in progress.
Submissions are capped at --timeout 30m, so a stalled notary queue ends the step rather than hanging the window.
Entitlements survive re-signing
Signing replaces a signature rather than amending it, so re-signing an app with an empty entitlements field would strip every entitlement it had. A sandboxed app would come out unsandboxed, correctly signed, and accepted by the notary service, broken in a way nothing downstream flags.
Notarize reads the existing entitlements out of the signature and re-applies them, and logs how many it carried over. The same holds inside the bundle: nested frameworks, helper tools and XPC services are re-signed and each keeps its own entitlements. The entitlements field governs the outer bundle only.
When a .entitlements file sits next to the app, the field is filled in automatically, so you can see what will be applied before anything is signed. What is in that field is exactly what gets used; nothing hidden is ever picked up from the surrounding folder.
It does not re-sign what is already signed
Before signing, the pipeline compares the existing signature against the window. For an app that means the certificate, the secure timestamp, the hardened runtime, and the entitlements; for a package, which has none of the latter two, it means the certificate, the trusted timestamp, and whether the package is already distribution-signed. If everything matches, signing is skipped and the log says so. Anything that differs is named as the reason, and the target is signed.
For an app the comparison covers nested code, not just the outer bundle. A valid outer signature says nothing about a helper signed ad-hoc or an unsigned .dylib sitting in Resources, so Notarize checks the same set it would sign.
Sign for Release, run on its own, always signs regardless of that comparison.
You can decline signing
The signing identity picker ends with a Don't Code-sign row, which sends the target to the notary service exactly as it arrived. Re-signing is not always harmless: an app assembled by a build system this window cannot reproduce, with nested code carrying entitlements of its own or a designated requirement that has to survive, can come out of a re-sign subtly different from what was tested. That call belongs to you.
With no certificate of the required class in your keychain, the picker opens on Don't Code-sign and the pipeline checks the existing signature, carrying on if the notary service would accept it and stopping with an explanation if it would not. The log distinguishes the two cases: that you asked for it, or that there was no certificate to use.
The choice is remembered for that target alone and is never adopted as a global default.
Gatekeeper preflight
A notarized and stapled app can still be rejected by Gatekeeper. Stale ad-hoc-signed or unsigned files sitting in nested-code locations pass codesign --verify --deep --strict, the notary service, and stapler validate, and are then rejected by spctl with a bare rejected and no reason.
The Notarize pipeline refuses to submit a bundle in that state, and names the offending items, so you find out from a local directory walk rather than after an upload, a wait, a staple, and a rejection in the field. Inspect Signature reports the same thing on demand. Gatekeeper rejections in the log explain themselves: which nested items are at fault, or a missing ticket, or a broken signature.
Credentials set up once
The Credentials button opens a guided wizard that creates and validates a notary keychain profile with notarytool store-credentials. Three paths: an Apple ID with an app-specific password, an App Store Connect API key, or registering a profile you already created yourself. The first two link out to the page the credentials come from, and the Team ID is prefilled from your default signing identity. Test Profile checks a profile without storing any credentials; Save and Validate checks the credentials with Apple first and stores them only if that succeeds. The wizard does not require an app to be loaded.
An app-specific password is piped over stdin and never appears on a command line.
An existing profile is never replaced without asking
notarytool store-credentials treats its profile name as one "to create or update". It overwrites a matching profile in place with no prompt, and what it replaces cannot be read back out of the keychain by anything, including this app.
That is easy to walk into, because the wizard suggests a name and the suggestion survives backing out and choosing a different authentication method, so an API-key save can land on the name an Apple ID profile is already using. Notarize flags a name it already knows as you type, and on Save confirms with a "Replace credentials" or "Cancel" alert before writing over anything. Cancel is on the key Escape reaches; canceling leaves the stored credentials exactly as they were.
Profiles created outside this app are checked too. They cannot be listed, because notarytool keeps them in Apple's data protection keychain where security find-generic-password cannot see them, so the wizard asks notarytool about the one name you typed. A name it has never registered and notarytool reports as absent is treated as free; anything else, including an expired profile or an unreachable notary service, is treated as in use. Being wrong in that direction costs a confirmation you did not need. Being wrong the other way destroys credentials without asking.
Profile names that cannot work, meaning empty, whitespace-only, containing a line break, or starting with a dash, are rejected up front instead of surfacing later as a cryptic failure.
Settings that follow the product
Per-target settings, the signing identity, entitlements, credential profile, and output folder, are remembered between runs. They are keyed by bundle identifier for an app and by package identifier for a package, so they follow the product rather than a file name carrying a version number. When neither identifier can be read, the file name is the fallback key. Signing identity and credential profile also have global defaults, with a per-target override only when your selection differs from the default.
A package has no entitlements to apply, so that field is disabled rather than hidden for one.
Your development copy is left alone
When an output folder is set, the target is copied there before signing, so release signing never happens on your working copy.
Signing a package replaces it atomically: productsign writes a hidden neighbor that is renamed into place, so an interrupted run leaves either the old package or the new one, never a half-written file.
One window per target
A target arrives one of two ways: dropped on the Notarize icon in the Finder, or opened through the Open panel. Either opens a new window, bound to that target for as long as the window is open. Opening a second target opens a second window.
Individual steps and diagnostics
Each of the four step-rail stages is also a button that runs just that stage: Sign for Release, Submit and Wait, Staple Ticket, Validate Result. Alongside them, Inspect Signature reports signature, nested-code, staple and Gatekeeper status, and Fetch Log retrieves the notary service log for the last submission. Reveal in Finder appears once a ticket has been stapled, and shows the finished result.
Known limitations
- Flat packages only. An old bundle-style
.pkgor.mpkg, which is a folder, is not accepted; the window opens empty rather than loading it. The notary service does not accept that form either. Flat packages are whatproductbuildproduces. - Stop covers the full run and a submission, the two things worth interrupting. It stays disabled while any other single stage runs on its own.
- No distribution archive is built for you. Creating the zip or disk image you ship is left to your own tooling.
ditto -c -k --keepParentwithout--sequesterRsrcstores extended attributes as literal._AppleDouble files inside the bundle, which breaks the signature for anyone who unzips with the Finder, so Notarize does not do it for you. - The automated test suite runs against stand-ins for the Apple tools. It does not cover visual layout, and it cannot prove that the real
codesignbehaves exactly as the fakes model it.
Architecture
Notarize is an OMC 5.1 applet. The OMC framework handles the app lifecycle, the per-target document window, file dialogs, and the credential sheet. The interface is defined declaratively in ActionUI JSON. All business logic runs as /bin/sh scripts in Contents/Resources/Scripts/, with shared functions in lib.notarize.sh and command routing declared in Contents/Resources/Command.json.
Everything that differs between an app and a package is decided by one function, target_kind_of, which returns app or pkg for a path; the handlers branch on that rather than each guessing from the extension.
Signing delegates to a bundled copy of OMC's canonical codesign_applet.sh, kept byte-identical to the copy at the repository root.
License
Apache License 2.0. See LICENSE.
Built with OMC, a low-code app builder for macOS, and ActionUI, a JSON to SwiftUI interface builder.
And yes, you can notarize Notarize with itself.