A Practical Guide to Packaging, Signing, and Notarizing macOS Apps
This is a general guide to packaging macOS desktop applications. It applies to apps built with Tauri, Electron, or other tools that generate an .app bundle and then need to be signed, notarized, and distributed in a DMG.
All application names, certificate names, Team IDs, Apple IDs, and paths in this article use placeholders to avoid exposing personal information.
Why Split the Packaging Workflow into Separate Steps?
Many desktop application frameworks can generate a DMG in a single command, for example:
<build-command> --bundles dmg
This approach is convenient, but it can become a black box when something fails. A complete macOS distribution package actually passes through several stages:
Build the .app
↓
Sign the .app
↓
Notarize the .app
↓
Staple the .app
↓
Create the DMG
↓
Sign the DMG
↓
Notarize the DMG
↓
Staple the DMG
↓
Perform final verification
When every step is hidden behind one command, it is difficult to tell whether a failure occurred during signing, notarization, DMG creation, or Gatekeeper validation.
The strategy used in this guide is:
Let the build tool generate and sign only the .app
↓
Verify the .app manually
↓
Create the DMG with create-dmg
↓
Notarize the DMG separately
↓
Staple and verify the DMG
This workflow requires a few more commands, but every stage can be verified independently, which makes troubleshooting much easier.
Prerequisites
Prepare the following on the local Mac:
- An Apple Developer Program account
- A
Developer ID Applicationcertificate - The private key associated with the certificate
- Xcode Command Line Tools
- An application build tool, such as Tauri CLI or Electron Builder
create-dmg
Install create-dmg with Homebrew:
brew install create-dmg
If the application needs to be cross-compiled for different macOS architectures, install the corresponding targets in advance. For Rust/Tauri, run:
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
Confirm the Certificate Type
When distributing a macOS application directly to users, do not use an Apple Distribution certificate. Use:
Developer ID Application
List the code-signing identities available on the Mac:
security find-identity -v -p codesigning | grep "Developer ID Application"
You should see output similar to:
"Developer ID Application: <Developer Name> (<TEAM_ID>)"
The complete string inside the quotation marks will be used later as APPLE_SIGNING_IDENTITY.
If you only have a .cer file, or the certificate in Keychain Access does not have a private key beneath it, signing will fail. Install the certificate on the Mac where the CSR was created, then export a .p12 containing the private key from My Certificates in Keychain Access.
Set the Environment Variables
Set the following variables before packaging locally:
export APPLE_SIGNING_IDENTITY="Developer ID Application: <Developer Name> (<TEAM_ID>)"
export APPLE_ID="<[email protected]>"
export APPLE_PASSWORD="<app-specific-password>"
export APPLE_TEAM_ID="<TEAM_ID>"
Keep these points in mind:
APPLE_PASSWORDis not the password used to sign in to the Apple Account. It must be an app-specific password generated from the Apple Account management page.APPLE_SIGNING_IDENTITYmust exactly match the certificate name shown bysecurity find-identity.- Never commit the real values of these environment variables to Git or expose them in public build logs.
Step 1: Build Only the .app
Do not generate a DMG yet. Build only the .app bundle:
<build-command> --target <TARGET_TRIPLE> --bundles app
For example, with Tauri:
npm run tauri -- build --target <TARGET_TRIPLE> --bundles app
A successful build will usually produce an .app directory such as:
<APP_BUNDLE_PATH>/<APP_NAME>.app
Set a variable to simplify the remaining commands:
APP="<APP_BUNDLE_PATH>/<APP_NAME>.app"
Step 2: Verify the .app
Verify the code signature:
codesign --verify --deep --strict --verbose=2 "$APP"
Expected output:
valid on disk
satisfies its Designated Requirement
Validate the stapled notarization ticket:
xcrun stapler validate "$APP"
Expected output:
The validate action worked!
Ask Gatekeeper to assess the application:
spctl --assess --type exec --verbose "$APP"
Expected output:
accepted
source=Notarized Developer ID
If all three checks pass, the .app bundle itself is healthy.
Step 3: Prepare the DMG Staging Directory
Set the staging and output paths:
DMG_STAGE="/tmp/<app-name>-dmg-stage"
DMG_OUT="dist/<APP_NAME>-<ARCH>.dmg"
Remove old artifacts and create the required directories:
rm -rf "$DMG_STAGE"
rm -f "$DMG_OUT"
rm -f dist/rw.*.dmg
mkdir -p "$DMG_STAGE"
mkdir -p dist
Copy the .app bundle into the DMG staging directory:
ditto "$APP" "$DMG_STAGE/<APP_NAME>.app"
Step 4: Create the DMG with create-dmg
create-dmg \
--volname "<APP_NAME>" \
--window-size 660 400 \
--window-pos 200 120 \
--icon-size 80 \
--icon "<APP_NAME>.app" 180 220 \
--hide-extension "<APP_NAME>.app" \
--app-drop-link 480 220 \
--codesign "$APPLE_SIGNING_IDENTITY" \
--no-internet-enable \
--skip-jenkins \
"$DMG_OUT" \
"$DMG_STAGE"
The --skip-jenkins option skips the Finder AppleScript customization step. By default, create-dmg controls Finder to arrange the icons inside the DMG. If the terminal does not have Automation permission, the command may fail with:
Not authorized to send Apple events to Finder. (-1743)
With --skip-jenkins, the DMG can still be signed, notarized, and distributed normally. It simply will not have the same polished icon layout when opened.
If the Finder layout is required, allow the terminal to control Finder in System Settings:
System Settings
→ Privacy & Security
→ Automation
→ Allow Terminal / iTerm / VS Code to control Finder
Step 5: Verify the DMG Signature
codesign --verify --verbose=2 "$DMG_OUT"
Expected output:
valid on disk
satisfies its Designated Requirement
Step 6: Notarize the DMG
Submit the DMG to Apple’s notarization service:
xcrun notarytool submit "$DMG_OUT" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
A successful submission will report:
status: Accepted
If the command does not return for a long time, open another terminal and inspect the submission history:
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
If a submission shows:
status: In Progress
Apple has received the submission and is still processing it. This does not necessarily mean the local command is stuck.
Inspect a specific submission:
xcrun notarytool info <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
If the status changes to:
status: Invalid
Download the notarization log:
xcrun notarytool log <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
Step 7: Staple and Perform the Final DMG Verification
After notarization succeeds, staple the ticket to the DMG:
xcrun stapler staple "$DMG_OUT"
xcrun stapler validate "$DMG_OUT"
Finally, verify the DMG with Gatekeeper:
spctl --assess --type open --context context:primary-signature --verbose "$DMG_OUT"
Expected output:
accepted
source=Notarized Developer ID
At this point, $DMG_OUT is ready to distribute to users as a macOS installer.
Common Problems
1. failed codesign application
Common causes include:
- An incorrect
APPLE_SIGNING_IDENTITY - A certificate other than
Developer ID Application - A certificate without its private key
- A locked keychain
codesignnot having permission to access the private key
Check the configured identity and available certificates:
echo "$APPLE_SIGNING_IDENTITY"
security find-identity -v -p codesigning | grep "Developer ID Application"
Unlock the login keychain:
security unlock-keychain ~/Library/Keychains/login.keychain-db
You can also test signing with a temporary binary:
cp /bin/echo /tmp/codesign-test
codesign --force \
--sign "$APPLE_SIGNING_IDENTITY" \
--options runtime \
--timestamp \
/tmp/codesign-test
If this test also fails, the problem is with the certificate or keychain rather than the application build.
2. Keychain Prompts for Private Key Access
The following prompt may appear during signing:
Keychain Access wants to export key "<key name>" from your keychain.
This is expected because codesign needs to read the private key.
Select:
Always Allow
Enter the local macOS login password, not the Apple Account password.
3. Notarization Remains In Progress
If the status is:
status: In Progress
Apple has received the submission and is still processing it. Processing commonly takes several minutes to around twenty minutes, but occasionally takes longer.
View the submission history:
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
Inspect a specific submission:
xcrun notarytool info <SUBMISSION_ID> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
4. DMG Creation Fails
If the framework’s built-in DMG script fails with an error such as:
failed to run bundle_dmg.sh
Switch to the following workflow:
Build the .app
↓
Verify the .app
↓
Create the DMG manually with create-dmg
↓
Notarize the DMG separately
If the failed process leaves the .app in this state:
invalid signature (code or signature have been modified)
Clean the bundle output directory and rebuild the .app. Do not use the damaged .app to create a DMG.
5. Not authorized to send Apple events to Finder. (-1743)
This error occurs when macOS privacy controls prevent the create-dmg AppleScript from controlling Finder.
The quickest solution is:
create-dmg ... --skip-jenkins ...
To preserve the Finder icon layout, grant the terminal Automation permission instead:
System Settings
→ Privacy & Security
→ Automation
→ Allow Terminal to control Finder
Complete Command Template
The following reusable template combines all the steps:
cd /path/to/project
export APPLE_SIGNING_IDENTITY="Developer ID Application: <Developer Name> (<TEAM_ID>)"
export APPLE_ID="<[email protected]>"
export APPLE_PASSWORD="<app-specific-password>"
export APPLE_TEAM_ID="<TEAM_ID>"
<build-command> --target <TARGET_TRIPLE> --bundles app
APP="<APP_BUNDLE_PATH>/<APP_NAME>.app"
DMG_STAGE="/tmp/<app-name>-dmg-stage"
DMG_OUT="dist/<APP_NAME>-<ARCH>.dmg"
codesign --verify --deep --strict --verbose=2 "$APP"
xcrun stapler validate "$APP"
spctl --assess --type exec --verbose "$APP"
rm -rf "$DMG_STAGE"
rm -f "$DMG_OUT"
rm -f dist/rw.*.dmg
mkdir -p "$DMG_STAGE"
mkdir -p dist
ditto "$APP" "$DMG_STAGE/<APP_NAME>.app"
create-dmg \
--volname "<APP_NAME>" \
--window-size 660 400 \
--window-pos 200 120 \
--icon-size 80 \
--icon "<APP_NAME>.app" 180 220 \
--hide-extension "<APP_NAME>.app" \
--app-drop-link 480 220 \
--codesign "$APPLE_SIGNING_IDENTITY" \
--no-internet-enable \
--skip-jenkins \
"$DMG_OUT" \
"$DMG_STAGE"
codesign --verify --verbose=2 "$DMG_OUT"
xcrun notarytool submit "$DMG_OUT" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG_OUT"
xcrun stapler validate "$DMG_OUT"
spctl --assess --type open --context context:primary-signature --verbose "$DMG_OUT"
Summary
The most important lesson when packaging macOS applications is to avoid treating the build, signing, notarization, DMG creation, and DMG notarization process as one indivisible black box.
By separating the workflow, you can determine exactly whether:
- The
.appwas signed successfully - The
.appwas notarized successfully - The DMG was created successfully
- The DMG was signed successfully
- The DMG was notarized successfully
- Gatekeeper accepts the final artifact
When a framework’s built-in DMG workflow fails, first confirm that the .app is healthy. If it is signed, notarized, and accepted by Gatekeeper, you can bypass the built-in DMG step, create the DMG manually with create-dmg, and notarize the DMG separately.