Journal · Guide · 8 min

Notarisation and the six things that get a Mac app rejected

By Ziyarex ·

Notarisation gets talked about as if it were a review. It isn't. There's no person, no judgement, no appeal, it's an automated service that checks your app is signed correctly and contains no known malware, and it either passes or hands you a JSON file naming exactly what's wrong.

Which is good news: every failure has a precise cause. Here are the six that account for nearly all of them, and the pipeline that avoids them.

The pipeline, in order

build  →  sign (inside-out)  →  notarize  →  staple  →  verify

Get the order wrong and the later steps fail in ways that look unrelated to the actual mistake. That's the source of most of the confusion in this process.

1. Signed with the wrong identity

For distribution outside the App Store you need a Developer ID Application certificate. Not "Apple Development", not "Apple Distribution", not "Mac App Distribution", those are for other channels and notarisation will reject them.

security find-identity -v -p codesigning

If you don't see a Developer ID Application line, that's your problem, and it's an Xcode → Settings → Accounts → Manage Certificates fix.

2. Hardened runtime not enabled

Notarisation requires it. Every executable in your bundle needs --options runtime:

codesign --force --options runtime --timestamp \
  --sign "Developer ID Application: Your Name (TEAMID)" \
  MyApp.app

In Xcode it's the "Hardened Runtime" capability. Turning it on will often break things that used to work (unsigned plugins, JIT, DYLD environment overrides) and each of those has a specific entitlement to re-enable it deliberately. That's the point of it.

3. Missing secure timestamp

--timestamp is not optional. Without it, notarisation rejects the submission, and the error text doesn't obviously say "timestamp."

It's also the reason a build that worked offline fails in CI, or vice versa: signing with a timestamp requires reaching Apple's timestamp server. A flaky network produces a signature that's structurally fine and unnotarisable.

4. Nested code signed in the wrong order

This is the big one, the failure that consumes an afternoon.

Everything executable inside your bundle must be signed: frameworks, embedded helpers, XPC services, dylibs, command-line tools in Resources, Sparkle's updater, anything vendored. And they must be signed before the outer app, because signing the app seals its contents. Sign the app first and then touch anything inside it, and the outer signature is now invalid.

# inside-out: deepest first, app bundle last
codesign --force --options runtime --timestamp -s "$ID" MyApp.app/Contents/Frameworks/*.framework
codesign --force --options runtime --timestamp -s "$ID" MyApp.app/Contents/Helpers/*
codesign --force --options runtime --timestamp -s "$ID" MyApp.app

Apple's own guidance is to avoid --deep. It signs everything with the same options and entitlements, which is wrong the moment a helper needs different ones, and it silently produces bundles that pass codesign --verify and fail notarisation.

Verify properly before submitting:

codesign --verify --deep --strict --verbose=2 MyApp.app

5. A disallowed entitlement: usually get-task-allow

com.apple.security.get-task-allow lets a debugger attach. Xcode adds it to debug builds. Notarisation rejects any binary carrying it.

The classic version of this bug: the app is fine, but one vendored helper binary was built in debug and still has it. Check what you're actually shipping:

codesign -d --entitlements - MyApp.app

Do this for the nested binaries too, not just the app. That's where it hides.

6. Stapling skipped

Notarisation succeeding is not the end. The result lives on Apple's servers until you staple the ticket into the app:

xcrun stapler staple MyApp.app
xcrun stapler validate MyApp.app

Skip it and everything works on your machine and on any machine with a live connection, because Gatekeeper checks online. The first user who opens it offline, or behind a restrictive network, gets a warning. This is the failure that reaches customers rather than CI, which makes it the worst one on the list.

Staple the DMG or PKG too if that's what you ship, and sign the DMG itself after stapling the app inside it.

The submission, end to end

Store credentials once:

xcrun notarytool store-credentials "AC_NOTARY" \
  --apple-id "you@example.com" \
  --team-id "TEAMID" \
  --password "app-specific-password"

Then, per build:

ditto -c -k --keepParent MyApp.app MyApp.zip
xcrun notarytool submit MyApp.zip --keychain-profile "AC_NOTARY" --wait
xcrun stapler staple MyApp.app

Use ditto, not Finder's Compress and not zip, those can mangle symlinks inside frameworks and produce a submission that fails for reasons that have nothing to do with your signing.

When it fails, the log tells you exactly which file and why:

xcrun notarytool log <submission-id> --keychain-profile "AC_NOTARY"

Read that file before changing anything. It names the path of the offending binary and the specific check it failed, and nearly every hour lost to notarisation is an hour spent guessing instead of reading it.

Final check, as a user would see it

spctl -a -vvv -t install MyApp.app

You want accepted and source=Notarized Developer ID. Anything else means a user will see a warning.

Best test: copy the built app to a Mac that has never seen your project, unplug the network, and open it. That's the scenario stapling exists for, and it's the only way to find out you skipped it before someone else does.

A note on App Store rejections, which are a different thing

If you're shipping through the Mac App Store instead, notarisation is handled for you and your rejections come from human review, most commonly incomplete metadata, a missing demo account, a privacy declaration that doesn't match what the app does, or App Sandbox entitlements you requested without justifying.

Different process, different fixes, and worth not conflating the two when you're searching for an error message at midnight.

Related: every app icon size and the screenshot dimensions, the other two things that block a release for stupid reasons.