Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Table Of Contents

Introduction

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.

Overview

notarize-mac-app.sh automates the entire release pipeline for distributing a macOS app outside the App Store:

  1. Archive the Xcode project
  2. Extract and deep-sign the .app with a Developer ID certificate
  3. Package it into a DMG with a drag-to-Applications layout (via create-dmg)
  4. Sign the dmg
  5. Submit to Apple's notary service and wait for approval (via notarytool)
  6. Staple the notarization ticket to the dmg
  7. Verify Gatekeeper acceptance with spctl

Prerequisites

Installation

Clone the repository

git clone https://github.com/muquit/NotarizeMacApp.git

Then 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.sh

One-time Setup

These steps are performed once and apply to all projects.

Step 1: Create a Developer ID Application Certificate

  1. Open Xcode -> Settings -> Accounts
  2. Select your Apple ID -> Manage Certificates
  3. 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)"

Step 2: Create an App Store Connect API Key

  1. Go to appstoreconnect.apple.com
  2. Navigate to Users and Access -> Integrations -> App Store Connect API
  3. Click +, give it a name (e.g. notarytool), set role to Developer
  4. Download the .p8 file -- you can only download it once
  5. 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.

Step 3: Store Credentials in Keychain

xcrun notarytool store-credentials "notarytool-profile" \
--key /path/to/AuthKey_KEYID.p8 \
--key-id YOUR_KEY_ID \
--issuer YOUR_ISSUER_ID

This 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.

Setting Up on a Different Mac

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 .p8 API 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:

  1. 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.

  2. Install create-dmg:

    brew install create-dmg
  3. Store the credentials in the new Mac's Keychain using the same .p8 file:

    xcrun notarytool store-credentials "notarytool-profile" \
    --key ~/.private/AuthKey_KEYID.p8 \
    --key-id YOUR_KEY_ID \
    --issuer YOUR_ISSUER_ID
  4. Install the script (see Installation).

Per-project Setup

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 3

The script derives the version automatically from xcodebuild -showBuildSettings (MARKETING_VERSION), so there is nothing to bump in notarize.conf.

Usage

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.sh

Pass -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.dmg

alt nirjhar_app nirjhar_app.png

Just drag the App to Applications folder.

Notarizing from Xcode (Alternative)

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.

Archive and Distribute

  1. In Xcode: Product -> Archive
  2. When the Organizer opens, select the archive and click Distribute App
  3. Choose Direct Distribution
  4. Xcode signs with the Developer ID certificate and submits for notarization
  5. Wait for "Status: Ready to distribute"
  6. Click Export Notarized App and save the .app to disk

Create and Sign the dmg

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.dmg

Verify

spctl --assess --type open --context context:primary-signature --verbose \
MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How Users Install the dmg

  1. Double-click the .dmg -- it mounts as a virtual disk
  2. Drag MyApp.app onto the Applications folder shortcut in the window
  3. Eject the dmg
  4. Launch from Applications

Notes

  • 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 -v flag the script suppresses all tool output; only progress lines are shown.
  • build/ is a scratch directory created by xcodebuild. It is safe to delete between runs.

Troubleshooting

Running via SSH

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.sh

If 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.sh

The -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).

FAQ

How do I lock the Keychain manually?

security lock-keychain ~/Library/Keychains/login.keychain-db

How do I unlock the Keychain manually?

security unlock-keychain ~/Library/Keychains/login.keychain-db

macOS will prompt for your login password.

How do I keep the Keychain unlocked for a long build?

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-db

How do I find my Team ID?

security 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.

How do I find my notarytool profile name?

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.

How do I check whether a DMG is properly notarized?

spctl --assess --type open --context context:primary-signature --verbose MyApp-mac-1.0.0.dmg

Expected output:

MyApp-mac-1.0.0.dmg: accepted
source=Notarized Developer ID

How do I check the notarization history for a DMG?

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"

The script failed but I see no error -- what happened?

Run with -v to see full output from every tool:

notarize-mac-app.sh -v

How do I check whether my Developer ID certificate is still valid?

security 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.

How do I clean the build directory?

rm -rf build/

build/ is created by xcodebuild and is safe to delete between runs.

Storing Secrets Securely on Mac

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.

Creating a Sparse Bundle Disk Image

  1. Open Disk Utility (Applications > Utilities > Disk Utility)
  2. Choose File > New Image > Blank Image
  3. 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
  4. Set a strong password when prompted
  5. Copy secrets into the mounted volume (e.g. /Volumes/Personal/)
  6. Eject the volume when done

To access the files later, double-click the .sparsebundle to mount it, use the files, then eject.

Creating a Sparse Bundle from the Command Line

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.sparsebundle

macOS will prompt for a password to protect the image.

Mounting and Unmounting from the Command Line

# mount
hdiutil attach ~/Personal.sparsebundle
# unmount
hdiutil detach /Volumes/Personal

Backups

A 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.

Sparse Bundle vs Fixed-size Image

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.

Credits

Created with assistance from Claude Code


TOC/glossary expansion by https://github.com/muquit/markdown-toc-go v1.0.5 on Jun-26-2026

About

A shell script to archive, sign, notarize, and staple a macOS/Catalyst app

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages