It’s 2 AM. Your app compiles. You sign it. You package it in a DMG. You run notarytool submit. Apple says “In Progress”. You wait 5 minutes. 10. 20. An hour. Two hours. The submission is still “In Progress”. You go to bed. The next morning: Invalid.
With no more explanation than “The signature of the binary is invalid”. For both architectures. Thanks, Apple. Very helpful.
Notarization is one of those processes that works perfectly… until it doesn’t. And when it fails, it leaves you with a .dmg that Gatekeeper won’t let open and an error that tells you nothing. After fighting with this for a couple of days with Tokamak (my menu bar app for monitoring Claude quota), I decided to document everything I learned and write a linter so I never have to go through this again.
What notarization is (in plain language)
Imagine the Mac App Store is a mall with a security guard. But you don’t want to sell in the mall — you want to distribute your app directly, with your own DMG. A street vendor.
Apple says: “Fine, you can. But first get past the bouncer.”
That bouncer is notarization. It’s Apple’s automated service that scans your signed app, verifies it doesn’t contain known malware, and if everything’s fine, gives you a ticket. You staple that ticket to your DMG (stapler staple) and from then on, when a user downloads it and tries to open it, Gatekeeper sees the ticket and says “go ahead”.
Without that ticket, the user sees this:
“Tokamak.app” can’t be opened because Apple cannot check it for malicious software.
And your app gets left in the dust.
Why Apple does this
There are two reasons. One legitimate. The other… well.
The legitimate one: protecting users. Before notarization (introduced in macOS 10.14.5, 2019), anyone could distribute a .app signed with Developer ID and macOS would open it without question. Code signing verified the developer’s identity, but didn’t scan the content. If your signed app included a keylogger, it would run signed and all.
Notarization adds a layer: Apple scans the binary for known malware and suspicious behavior before it reaches the user. It’s not a human review like the App Store — it’s an automated system. But it’s something.
The other reason: control. Apple wants you to go through App Store Connect for everything. Direct distribution with Developer ID has always been the second-class citizen. Notarization is one more step on that path of “you can distribute outside the App Store, but we’ll make it uncomfortable for you”.
That said, notarization is mandatory since macOS 10.15 for any app distributed outside the App Store. It’s not optional. Your app goes notarized or it doesn’t open. Period.
The 7 errors that will make you lose hours
After collecting my own errors and those from developer forums, these are the ones that hurt the most:
1. Missing --timestamp
This is the classic. Your codesign works perfectly locally, Gatekeeper doesn’t complain, and notarization returns “The signature of the binary is invalid.”
| |
The secure timestamp proves the signature was made while the certificate was valid. Without it, Apple doesn’t trust it. It’s like signing a contract without a date — technically valid, but nobody’s going to accept it.
2. Wrong certificate
There are three certificates that sound similar and do different things:
| Certificate | What it’s for |
|---|---|
| Apple Development | Debug builds (local development) |
| Apple Distribution | App Store and TestFlight |
| Developer ID Application | Direct distribution + notarization |
If you sign with “Apple Development” and try to notarize, Apple sends you packing. You need Developer ID Application. Seems obvious, but when you’ve been trying for three hours, it’s not.
3. Hardened Runtime disabled
Notarization requires your app to use Hardened Runtime. It’s a protection that prevents your app from doing things like injecting code into other processes or disabling memory protections.
| |
Without --options runtime, the signature is valid but Apple rejects it. It’s like going to a job interview with a perfect resume but no pants.
4. xcodebuild -exportArchive hangs
If you use xcodebuild -exportArchive with method: developer-id, get ready: it hangs. Indefinitely. No output. No error. Just… silence.
The problem is that exportArchive needs Apple ID credentials to contact the distribution servers, and if it doesn’t have them cached (or if there are old corrupt accounts in the Keychain), it waits for interactive authentication that never comes.
The solution: skip exportArchive and do the signing manually.
| |
It’s a hack, yes. But it works. And it doesn’t hang.
5. codesign requests Keychain access… silently
The first time you sign with a new certificate, codesign needs access to the private key stored in the Keychain. macOS shows a popup asking for permission.
The problem: if you run codesign from an IDE, a script, or a tool like Claude Code, the popup might not appear. Or appear behind all windows. And codesign hangs waiting for your response to a popup you can’t see.
Solution: the first time, run codesign directly from the terminal. Click “Always Allow” on the popup. From then on, scripts will work without asking.
6. Notarization takes… and takes… and takes
Apple says “most uploads are processed within minutes.” Reality:
- Normal: 2-15 minutes
- First time with new Developer ID: up to 1 hour
- When Apple has problems: hours. Or days. No warning.
In February 2026, there are reports on Apple forums of submissions stuck “In Progress” for over 16 hours. The service was technically “operational”, but many submissions simply weren’t being processed.
There’s nothing you can do except wait and retry. Welcome to Apple’s developer experience.
7. Extended attributes that break the signature
macOS adds extended attributes to downloaded files (the famous com.apple.quarantine). If your build pipeline copies files that have these attributes, the signature becomes invalid.
| |
It’s a detail nobody tells you until it bites you.
The linter: 25 checks so you’re not exposed
After accumulating all these errors, I wrote a script that verifies 25 notarization conditions — both static (your code and configuration) and dynamic (build artifacts).
It runs like this:
| |
And produces something like:
── CRITICAL ──
✓ PASS C1: export-direct passes --entitlements to codesign
✗ FAIL C2: Release CODE_SIGN_IDENTITY is not Developer ID
✓ PASS C3: ENABLE_HARDENED_RUNTIME = YES in Release
── HIGH ──
✗ FAIL H1: export-direct does NOT run xattr -cr
── MEDIUM ──
⚠ WARN M1: DMG is not signed
✓ PASS M2: LSUIElement declared in project.yml
⚠ WARN M4: Only stapling DMG, not .app
── CERTIFICATES AND KEYCHAIN ──
✓ PASS P1: Developer ID Application certificate in Keychain
✓ PASS P2: notarytool profile 'tokamak-notary' configured
── ARTIFACTS ──
✓ PASS P13: Universal binary (x86_64 + arm64)
✓ PASS C2-V: Export signed with Developer ID Application
✓ PASS C3-V: Hardened runtime active in export signature
── SUMMARY ──
Total: 25 checks
✓ 18 passed
✗ 2 failed
⚠ 3 warnings
⊘ 2 skipped (build artifacts missing)
✗ DO NOT NOTARIZE — there are 2 failures to fix first.
The checks are divided into four levels:
CRITICAL — If any fail, notarization will definitely fail:
--entitlementsincodesign(if you don’t pass them, you lose sandbox and network)CODE_SIGN_IDENTITY= Developer ID in Release- Hardened Runtime enabled
HIGH — Will probably fail or cause problems:
xattr -crbefore signing- No debug dylibs in Release
MEDIUM — Might work, but you’re taking risks:
- DMG signed (not mandatory, but recommended)
LSApplicationCategoryTypefor App Store- Version consistency between
AppInfoandproject.yml
ARTIFACTS — Verifies generated builds:
- Valid signature with Developer ID
- Hardened Runtime active in signature
- Universal binary (x86_64 + arm64)
- No
.DS_Storeor symlinks in bundle - Correct structure (
Contents/{MacOS,Resources,Info.plist})
The script is static for configuration checks (doesn’t need builds), and dynamic for artifacts (you need to have done make archive && make export-direct). So you can run it anytime as a preflight check.
The complete workflow
To give you an idea of the end-to-end process:
flowchart TD
subgraph build[" 🔨 Build "]
direction TB
A["xcodegen generate"] --> B["xcodebuild archive<br/>(Release, signed)"]
end
subgraph sign[" ✍️ Sign "]
direction TB
C["Copy .app from archive"] --> D["xattr -cr<br/>(clean attributes)"]
D --> E["codesign --force<br/>--options runtime<br/>--timestamp<br/>--sign Developer ID"]
end
subgraph package[" 📦 Package "]
direction TB
F["hdiutil create<br/>(DMG UDZO format)"]
end
subgraph notarize[" 🍎 Notarization "]
direction TB
G["notarytool submit<br/>--wait"] --> H{Accepted?}
H -->|Yes| I["stapler staple<br/>(staple ticket)"]
H -->|No| J["notarytool log<br/>(see errors)"]
J --> K["Fix and<br/>sign again"]
end
subgraph lint[" 🔍 Preflight "]
direction TB
L["lint-notarize.sh<br/>(25 checks)"]
end
build --> sign
sign --> lint
lint -->|✓ All OK| package
lint -->|✗ Failures| K
package --> notarize
K --> sign
I --> M["✅ DMG ready<br/>for distribution"]
And in commands:
| |
Lessons learned at 3AM
--timestampalways. Always. No excuse.- First signature, in terminal. So the Keychain popup appears.
- Don’t use
exportArchivefor Developer ID. Copy + manualcodesign. - Be patient with Apple. Notarization can take minutes or hours. There’s no SLA.
- A linter is worth more than a hundred attempts. The 30 seconds the script takes saves you hours of failed submissions.
Notarization is like a driver’s test: uncomfortable, bureaucratic, and probably necessary. But once you have the process mastered, it becomes just another step in the pipeline. A step that no longer wakes you up at 3AM wondering why Apple says “Invalid” without bothering to explain why.
The complete script is in the Tokamak repo.