- Introduction
- Overview
- Prerequisites
- Installation
- One-time Setup
- Setting Up on a Different Mac
- Per-project Setup
- Usage
- Notarizing from Xcode (Alternative)
- How Users Install the dmg
- Notes
- Troubleshooting
- FAQ
- How do I lock the Keychain manually?
- How do I unlock the Keychain manually?
- How do I keep the Keychain unlocked for a long build?
- How do I find my Team ID?
- How do I find my notarytool profile name?
- How do I check whether a DMG is properly notarized?
- How do I check the notarization history for a DMG?
- The script failed but I see no error -- what happened?
- How do I check whether my Developer ID certificate is still valid?
- How do I clean the build directory?
- Storing Secrets Securely on Mac
- Credits
notarize-mac-app.sh is a simple shell script I use to archive, sign, package,
notarize, and verify a macOS app in one step from comamnd line. It reads a
per-project config file (notarize.conf).
Notarization is Apple's process of scanning a macOS app for malware and verifying it is safe to run. Without notarization, macOS blocks an app from opening and shows a warning dialog, and the user then has to manually allow it from System Settings > Privacy & Security
Hope you will find it useful as well.
notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:
- Archive the Xcode project
- Extract and deep-sign the
.appwith a Developer ID certificate - Package it into a DMG with a drag-to-Applications layout (via create-dmg)
- Sign the dmg
- Submit to Apple's notary service and wait for approval (via notarytool)
- Staple the notarization ticket to the dmg
- Verify Gatekeeper acceptance with
spctl
- Apple Developer Program membership ($99/year)
- Xcode installed (provides
xcodebuild,xcrun,codesign,spctl) - Install Homebrew for
create-dmg - create-dmg installed:
brew install create-dmg
git clone https://github.com/muquit/NotarizeMacApp.gitThen either add the cloned directory to your PATH, or create a symlink:
sudo ln -s \
/path/to/NotarizeMacApp/notarize-mac-app.sh \
/usr/local/bin/notarize-mac-app.shThese steps are performed once and apply to all projects.
- Open Xcode -> Settings -> Accounts
- Select your Apple ID -> Manage Certificates
- Click + and choose Developer ID Application
Xcode generates the CSR, requests the certificate, and installs it in your Keychain automatically.
Verify it is present:
security find-identity -v -p codesigning | grep "Developer ID"Expected output:
1) XXXXXXXXXXXX "Developer ID Application: Your Name (TEAMID)"
- Go to appstoreconnect.apple.com
- Navigate to Users and Access -> Integrations -> App Store Connect API
- Click +, give it a name (e.g.
notarytool), set role to Developer - Download the
.p8file -- you can only download it once - Note the Key ID and Issuer ID shown on the page
Store the .p8 file outside any git repository, for example in an encrypted
sparse bundle disk image (see Storing Secrets Securely).
The .p8 key, Key ID, and Issuer ID together grant full API access to App
Store Connect -- treat them like a password.
xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_IDThis only needs to be done once. The credentials are stored securely in your
macOS Keychain. The name notarytool-profile is a label I chose; it is the
default the script expects. If you already have credentials stored under a
different name, set PROFILE=your-profile-name in notarize.conf instead of
re-running this command.
To find the profile name you used previously, open Keychain Access
(Applications > Utilities > Keychain Access), search for notary, and click
the result. The Account field shows a value like
com.apple.gke.notary.tool.saved-creds.your-profile-name. The part after
saved-creds. is your profile name.
The one-time setup is per-machine because the Keychain is not shared between Macs. To use the script on another Mac:
Install Homebrew
- Requires for
create-dmg
Copy from the original Mac:
- The
.p8API key file (e.g.~/.private/AuthKey_KEYID.p8). Copy it securely; do not put it in a git repository.
On the new Mac do the following:
Install Xcode, then sign in under Xcode > Settings > Accounts with your Apple ID. Xcode will automatically sync the Developer ID Application certificate from Apple's servers.
Install
create-dmg:brew install create-dmg
Store the credentials in the new Mac's Keychain using the same
.p8file:xcrun notarytool store-credentials "notarytool-profile" \ --key ~/.private/AuthKey_KEYID.p8 \ --key-id YOUR_KEY_ID \ --issuer YOUR_ISSUER_ID
Install the script (see Installation).
In the root of each Xcode project, create a file named notarize.conf:
# notarize.conf -- per-project config for notarize-mac-app.sh# Place this file in the root of your Xcode project.# The .app bundle name produced by Xcode (without the .app extension).
APP_NAME="MyApp"# The Xcode scheme to archive (Product -> Scheme in Xcode).
SCHEME="MyApp"# Your 10-character Apple Team ID.# Find it by running:# security find-identity -v -p codesigning | grep "Developer ID"# It appears in parentheses at the end, e.g. "Developer ID Application: Your Name (XXXXXXXXXX)".# Also listed under Account -> Membership details on https://developer.apple.com
TEAM_ID="XXXXXXXXXX"# Target platform: macos (default) or catalyst for Mac Catalyst apps.
PLATFORM="macos"# Keychain profile name created by:# xcrun notarytool store-credentials "notarytool-profile" \# --key /path/to/AuthKey_KEYID.p8 \# --key-id YOUR_KEY_ID \# --issuer YOUR_ISSUER_ID# To find an existing profile name, open Keychain Access and search for "notary".# The Account field will show com.apple.gke.notary.tool.saved-creds.<profile-name>.# Defaults to "notarytool-profile" if not set.
PROFILE="notarytool-profile"To find your Team ID, run:
security find-identity -v -p codesigning | grep "Developer ID"It appears in parentheses at the end of the certificate name, e.g.
"Developer ID Application: Your Name (XXXXXXXXXX)". It is also listed
under Account -> Membership details on the Apple Developer Program portal.
Optional override (the script's default is notrary-profile):
PROFILE="notarytool-profile"# Keychain profile from Step 3The script derives the version automatically from xcodebuild -showBuildSettings
(MARKETING_VERSION), so there is nothing to bump in notarize.conf.
Run the script from the root of the Xcode project (where notarize.conf lives):
Example:
I will notarize an Mac desktop app of mine for example. Note the PLATFORM is
catalyst, that means it's a iPad app that the users can run on any Mac
device using Apple's Mac Catalyst technology. If the app is a pure macOS app,
then the PLATFORM will be macos.
➤ cd~/gitdev/Nirjhar
➤ cat notarize.conf
APP_NAME=Nirjhar
SCHEME=Nirjhar
TEAM_ID="P2MXXXXXXX"
PROFILE="notarytool-profile"
PLATFORM=catalyst➤ notarize-mac-app.shPass -v to see full command output (archive, codesign, create-dmg, etc.):
➤ notarize-mac-app.sh -v- If you are running by ssh'ng to the mac, keychain might be locked and the script will fail. Example:
➤ notarize-mac-app.sh
notarize-mac-app.sh 1.0.4
==> 1. Archiving Nirjhar 1.0.61...
** ARCHIVE FAILED **
The following build commands failed:
CodeSign /Users/muquit/Library/Developer/Xcode/DerivedData/Nirjhar-brxsdjblzgmdgkbhyhsoyfnquihw/Build/Intermediates.noindex/ArchiveIntermediates/Nirjhar/InstallationBuildProductsLocation/Applications/Nirjhar.app (in target 'Nirjhar' from project 'Nirjhar')
Archiving project Nirjhar with scheme Nirjhar
(2 failures)
Hint: 'errSecInternalComponent' means the login Keychain is locked.
This commonly happens when running via SSH. To fix it:
security unlock-keychain ~/Library/Keychains/login.keychain-db
notarize-mac-app.sh
To keep the Keychain unlocked for one hour:
security set-keychain-settings -t 3600 ~/Library/Keychains/login.keychain-db
security unlock-keychain ~/Library/Keychains/login.keychain-db
- Unlock the Keychian as per instruction
➤ security unlock-keychain ~/Library/Keychains/login.keychain-db
password to unlock /Users/muquit/Library/Keychains/login.keychain-db:
Now run notarize-mac-app.sh
➤ notarize-mac-app.sh
notarize-mac-app.sh 1.0.4
==> 1. Archiving Nirjhar 1.0.61...
==> 2. Extracting app from archive...
==> 3. Signing app with Developer ID...
build/export/Nirjhar.app: replacing existing signature
==> 4. Creating DMG...
Searching for mounted interstitial disk image using /dev/disk48s...
waited 1 seconds for .DS_STORE to be created.
==> 5. Signing DMG...
==> 6. Notarizing...
Conducting pre-submission checks for Nirjhar-mac-1.0.61.dmg and initiating connection to the Apple notary service...
Submission ID received
id: 1500a1c3-792f-4a68-8c61-79cf0f8c6159
Upload progress: 100.00% (1.27 MB of 1.27 MB)
Successfully uploaded file
id: 1500a1c3-792f-4a68-8c61-79cf0f8c6159
path: /Users/muquit/gitdev/Nirjhar/Nirjhar-mac-1.0.61.dmg
Waiting for processing to complete.
Current status: Accepted.......
Processing complete
id: 1500a1c3-792f-4a68-8c61-79cf0f8c6159
status: Accepted
==> 7. Stapling...
==> 8. Verifying...
Nirjhar-mac-1.0.61.dmg: accepted
source=Notarized Developer ID
==> Done: Nirjhar-mac-1.0.61.dmg
This dmg will run on any Mac (Intel or Apple silicon). When double clicked or opened it in Terminal, the following dialog will pop:
➤ open Nirjhar-mac-1.0.61.dmgJust drag the App to Applications folder.
If you prefer a GUI workflow, Xcode's Organizer can handle archiving and notarization without the script. The one-time setup (certificate, API key, Keychain credentials) is the same.
- In Xcode: Product -> Archive
- When the Organizer opens, select the archive and click Distribute App
- Choose Direct Distribution
- Xcode signs with the Developer ID certificate and submits for notarization
- Wait for "Status: Ready to distribute"
- Click Export Notarized App and save the
.appto disk
Xcode's Organizer only notarizes the .app -- the dmg must be handled
separately on the command line:
create-dmg \
--volname "MyApp" \
--window-size 540 380 \
--icon-size 128 \
--icon "MyApp.app" 150 190 \
--app-drop-link 390 190 \
MyApp-mac-1.0.0.dmg \
MyApp.app
codesign --sign "Developer ID Application: Your Name (TEAMID)" \
MyApp-mac-1.x.x.dmg
xcrun notarytool submit MyApp-mac-1.x.x.dmg \
--keychain-profile "notarytool-profile" \
--wait
xcrun stapler staple MyApp-mac-1.x.x.dmgspctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmgExpected output:
MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID
- Double-click the
.dmg-- it mounts as a virtual disk - Drag
MyApp.apponto the Applications folder shortcut in the window - Eject the dmg
- Launch from Applications
- After notarization Apple issues a ticket -- a small cryptographic receipt confirming the app was approved. Stapling embeds that ticket directly into the dmg so Gatekeeper can verify it without a network connection. Without stapling, Gatekeeper must contact Apple's servers at launch time, which fails for users who are offline.
- The Developer ID certificate is valid for about two years -- renew before it expires or signing will fail.
- Without the
-vflag the script suppresses all tool output; only progress lines are shown. build/is a scratch directory created byxcodebuild. It is safe to delete between runs.
When you SSH into a Mac, macOS does not unlock the login Keychain automatically
(that only happens at a GUI login). Any tool that needs a signing identity
(xcodebuild, codesign, notarytool) will fail with errSecInternalComponent
because the Keychain is locked.
If the archive step fails with this error, the script detects it and prints the commands to unlock the Keychain automatically. You can also unlock it manually before running the script:
security unlock-keychain ~/Library/Keychains/login.keychain-db
# enter your macOS login password when prompted
notarize-mac-app.shIf the Keychain re-locks during a long build, extend the timeout first:
security set-keychain-settings -t 3600 ~/Library/Keychains/login.keychain-db
security unlock-keychain ~/Library/Keychains/login.keychain-db
notarize-mac-app.shThe -t 3600 keeps the Keychain unlocked for one hour. Omit it to restore the
default (lock on sleep or after a system-defined idle time).
security lock-keychain ~/Library/Keychains/login.keychain-dbsecurity unlock-keychain ~/Library/Keychains/login.keychain-dbmacOS will prompt for your login password.
Set a timeout (in seconds) before unlocking:
security set-keychain-settings -t 3600 ~/Library/Keychains/login.keychain-db
security unlock-keychain ~/Library/Keychains/login.keychain-db-t 3600 keeps it unlocked for one hour. To restore the default (lock on
sleep), omit -t:
security set-keychain-settings ~/Library/Keychains/login.keychain-dbsecurity find-identity -v -p codesigning | grep "Developer ID"The 10-character Team ID appears in parentheses at the end, e.g.
"Developer ID Application: Your Name (XXXXXXXXXX)". It is also listed
under Account -> Membership details on the Apple Developer Program portal.
Open Keychain Access (Applications > Utilities > Keychain Access),
search for notary, and click the result. The Account field shows a
value like com.apple.gke.notary.tool.saved-creds.your-profile-name. The
part after saved-creds. is the profile name to use in notarize.conf.
spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmgExpected output:
MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID
xcrun notarytool history --keychain-profile "notarytool-profile"This lists all past submissions. To see details for a specific submission:
xcrun notarytool info <submission-id> --keychain-profile "notarytool-profile"Run with -v to see full output from every tool:
notarize-mac-app.sh -vsecurity find-identity -v -p codesigning | grep "Developer ID"A valid certificate shows without any expiry warning. To check the exact expiry date, open Keychain Access, find the certificate under My Certificates, and double-click it to see the Expires field. Developer ID certificates are valid for about two years -- renew before expiry or signing will fail.
rm -rf build/build/ is created by xcodebuild and is safe to delete between runs.
The .p8 API key downloaded in Step 2 grants full access to App Store Connect
and must be kept outside any git repository. A good option on macOS is an
encrypted sparse bundle disk image.
- Open Disk Utility (Applications > Utilities > Disk Utility)
- Choose File > New Image > Blank Image
- Fill in the fields:
- Name: e.g.
Personal - Save location: wherever you want the bundle to live
- Encryption:
256-bit AES(the default on recent macOS). Note: AES-128 is just fine (Post quantum resistant). - Image Format:
sparse bundle disk image
- Name: e.g.
- Set a strong password when prompted
- Copy secrets into the mounted volume (e.g.
/Volumes/Personal/) - Eject the volume when done
To access the files later, double-click the .sparsebundle to mount it, use
the files, then eject.
Useful when Disk Utility is not available (e.g. headless Mac or SSH session):
hdiutil create -type SPARSEBUNDLE -size 100m -encryption AES-256 \
-fs HFS+ -volname Personal ~/Personal.sparsebundlemacOS will prompt for a password to protect the image.
# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/PersonalA sparse bundle is a regular directory on disk. Time Machine backs it up
automatically along with the rest of your home directory -- the encrypted
contents are included, so your .p8 key is protected even in the backup.
No extra steps are needed.
A fixed-size disk image pre-allocates the full size on disk the moment you
create it. A 100 MB fixed image wastes 100 MB even if you store only a few
kilobytes. A sparse bundle image allocates space only for what you actually
store and grows up to a maximum cap as needed. For a .p8 file (a few
kilobytes), a sparse bundle with the default 100 MB cap uses negligible disk
space.
Created with assistance from Claude Code
TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026
