Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automate Apple notarization with xcrun notarytool for macOS application distribution
.claude/skills/a5c-ai-macos-notarization-workflow/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 736% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 179% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 191% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 256% | 0% |
Automate Apple notarization workflow using xcrun notarytool for macOS applications. This skill handles the complete notarization process including submission, status checking, and stapling.
json{ "type": "object", "properties": { "projectPath": { "type": "string", "description": "Path to the project" }, "appPath": { "type": "string", "description": "Path to the signed app bundle or DMG" }, "authMethod": { "enum": ["app-store-connect-api", "apple-id", "keychain"], "default": "app-store-connect-api" }, "credentials": { "type": "object", "properties": { "keyId": { "type": "string" }, "issuerId": { "type": "string" }, "keyPath": { "type": "string" }, "appleId": { "type": "string" }, "teamId": { "type": "string" } } }, "waitForCompletion": { "type": "boolean", "default": true }, "staple": { "type": "boolean", "default": true } }, "required": ["projectPath", "appPath"] }
json{ "type": "object", "properties": { "success": { "type": "boolean" }, "submissionId": { "type": "string" }, "status": { "enum": ["Accepted", "Invalid", "In Progress", "Rejected"] }, "logUrl": { "type": "string" }, "errors": { "type": "array" }, "stapled": { "type": "boolean" } }, "required": ["success"] }
bash# Ensure Xcode command line tools are installed xcode-select --install # Verify code signing codesign --verify --deep --strict MyApp.app codesign -vvv --deep --strict MyApp.app # Check hardened runtime codesign -dvvv MyApp.app | grep runtime # Should show: flags=0x10000(runtime)
bash# Store App Store Connect API key in keychain xcrun notarytool store-credentials "MyProfile" \ --key ~/private_keys/AuthKey_XXXXXXXXXX.p8 \ --key-id XXXXXXXXXX \ --issuer xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Or store Apple ID credentials xcrun notarytool store-credentials "MyAppleIDProfile" \ --apple-id your.email@example.com \ --team-id XXXXXXXXXX \ --password @keychain:AC_PASSWORD
bash# Using stored credentials xcrun notarytool submit MyApp.app \ --keychain-profile "MyProfile" \ --wait # Using API key directly xcrun notarytool submit MyApp.app \ --key ~/private_keys/AuthKey_XXXXXXXXXX.p8 \ --key-id XXXXXXXXXX \ --issuer xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ --wait # Using Apple ID xcrun notarytool submit MyApp.app \ --apple-id your.email@example.com \ --team-id XXXXXXXXXX \ --password @keychain:AC_PASSWORD \ --wait
bash# Check specific submission xcrun notarytool info <submission-id> \ --keychain-profile "MyProfile" # Get submission log xcrun notarytool log <submission-id> \ --keychain-profile "MyProfile" \ developer_log.json # List recent submissions xcrun notarytool history \ --keychain-profile "MyProfile"
bash# Staple to app bundle xcrun stapler staple MyApp.app # Staple to DMG xcrun stapler staple MyApp.dmg # Staple to pkg xcrun stapler staple MyApp.pkg # Validate stapling xcrun stapler validate MyApp.app
bash#!/bin/bash # notarize.sh set -e APP_PATH="${1}" KEYCHAIN_PROFILE="${2:-MyProfile}" echo "=== Validating app bundle ===" codesign --verify --deep --strict "$APP_PATH" echo "=== Submitting for notarization ===" SUBMISSION_OUTPUT=$(xcrun notarytool submit "$APP_PATH" \ --keychain-profile "$KEYCHAIN_PROFILE" \ --wait \ --output-format json) SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | jq -r '.id') STATUS=$(echo "$SUBMISSION_OUTPUT" | jq -r '.status') echo "Submission ID: $SUBMISSION_ID" echo "Status: $STATUS" if [ "$STATUS" != "Accepted" ]; then echo "=== Notarization failed, fetching log ===" xcrun notarytool log "$SUBMISSION_ID" \ --keychain-profile "$KEYCHAIN_PROFILE" \ notarization_log.json cat notarization_log.json exit 1 fi echo "=== Stapling ticket ===" xcrun stapler staple "$APP_PATH" echo "=== Validating staple ===" xcrun stapler validate "$APP_PATH" echo "=== Notarization complete ==="
yamlname: Build and Notarize on: push: tags: ['v*'] jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Import signing certificate env: CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE }} CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }} run: | CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) echo -n "$CERTIFICATE_BASE64" | base64 --decode > $CERTIFICATE_PATH security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security import $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH - name: Build app run: | xcodebuild -project MyApp.xcodeproj \ -scheme MyApp \ -configuration Release \ -archivePath build/MyApp.xcarchive \ archive xcodebuild -exportArchive \ -archivePath build/MyApp.xcarchive \ -exportOptionsPlist ExportOptions.plist \ -exportPath build/ - name: Store notarization credentials env: API_KEY: ${{ secrets.NOTARIZATION_API_KEY }} API_KEY_ID: ${{ secrets.NOTARIZATION_API_KEY_ID }} API_ISSUER: ${{ secrets.NOTARIZATION_API_ISSUER }} run: | mkdir -p ~/private_keys echo -n "$API_KEY" > ~/private_keys/AuthKey.p8 xcrun notarytool store-credentials "CI_PROFILE" \ --key ~/private_keys/AuthKey.p8 \ --key-id "$API_KEY_ID" \ --issuer "$API_ISSUER" - name: Notarize app run: | xcrun notarytool submit build/MyApp.app \ --keychain-profile "CI_PROFILE" \ --wait xcrun stapler staple build/MyApp.app - name: Create DMG run: | create-dmg build/MyApp.app build/ xcrun notarytool submit build/*.dmg \ --keychain-profile "CI_PROFILE" \ --wait xcrun stapler staple build/*.dmg - name: Upload artifact uses: actions/upload-artifact@v4 with: name: MyApp path: build/*.dmg
Error: The signature does not include a secure timestamp.Fix: Sign with hardened runtime and timestamp:
bashcodesign --force --options runtime --timestamp --sign "Developer ID" MyApp.app
Error: The executable does not have the hardened runtime enabled.Fix: Include entitlements in signing:
bashcodesign --force --options runtime --timestamp \ --entitlements MyApp.entitlements \ --sign "Developer ID Application: Company" MyApp.app
Error: The signature of the binary is invalid.Fix: Sign all nested components:
bashfind MyApp.app -name "*.dylib" -o -name "*.framework" | \ xargs -I {} codesign --force --options runtime --timestamp --sign "Developer ID" {}
macos-entitlements-generator - Entitlements configurationmacos-codesign-workflow - Code signingcode-signing-setup process - Full signing workflowswiftui-macos-expert - macOS developmentcode-signing-specialist - Signing expertise| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 7,047 | 5,067 | -28% | 1 | 1 | 0% | 431 | 3,602 | +736% | 0 | 0 | — |
case-02 | pass→pass | 4,942 | 3,440 | -30% | 1 | 1 | 0% | 1,061 | 3,089 | +191% | 0 | 0 | — |
case-03 | pass→pass | 4,149 | 3,480 | -16% | 1 | 1 | 0% | 862 | 3,065 | +256% | 0 | 0 | — |
case-04 | pass→pass | 5,880 | 2,773 | -53% | 1 | 1 | 0% | 1,073 | 3,009 | +180% | 0 | 0 | — |
case-05 | pass→pass | 4,364 | 2,470 | -43% | 1 | 1 | 0% | 925 | 3,004 | +225% | 0 | 0 | — |
case-06 | pass→pass | 2,306 | 2,017 | -13% | 1 | 1 | 0% | 378 | 2,819 | +646% | 0 | 0 | — |
case-07 | pass→pass | 5,014 | 2,576 | -49% | 1 | 1 | 0% | 838 | 2,890 | +245% | 0 | 0 | — |
case-08 | pass→pass | 8,286 | 5,454 | -34% | 1 | 1 | 0% | 1,571 | 3,567 | +127% | 0 | 0 | — |
case-09 | pass→pass | 4,156 | 4,351 | +5% | 1 | 1 | 0% | 816 | 3,386 | +315% | 0 | 0 | — |
case-10 | pass→pass | 9,449 | 5,410 | -43% | 1 | 1 | 0% | 1,508 | 3,566 | +136% | 0 | 0 | — |
case-11 | fail→pass | 5,990 | 3,338 | -44% | 1 | 1 | 0% | 1,116 | 3,115 | +179% | 0 | 0 | — |
case-12 | pass→pass | 5,781 | 2,866 | -50% | 1 | 1 | 0% | 915 | 2,971 | +225% | 0 | 0 | — |
case-13 | pass→pass | 4,421 | 1,892 | -57% | 1 | 1 | 0% | 798 | 2,819 | +253% | 0 | 0 | — |
case-14 | pass→pass | 5,072 | 2,177 | -57% | 1 | 1 | 0% | 825 | 2,839 | +244% | 0 | 0 | — |
case-15 | fail→pass | 12,148 | 7,615 | -37% | 1 | 1 | 0% | 2,612 | 4,082 | +56% | 0 | 0 | — |
case-16 | pass→pass | 2,015 | 1,969 | -2% | 1 | 1 | 0% | 298 | 2,776 | +832% | 0 | 0 | — |
case-17 | pass→pass | 9,332 | 2,633 | -72% | 1 | 1 | 0% | 1,528 | 3,098 | +103% | 0 | 0 | — |
case-18 | pass→pass | 3,353 | 3,082 | -8% | 1 | 1 | 0% | 648 | 2,984 | +360% | 0 | 0 | — |
case-19 | pass→pass | 8,533 | 5,753 | -33% | 1 | 1 | 0% | 1,802 | 3,749 | +108% | 0 | 0 | — |
case-20 | pass→pass | 10,437 | 7,042 | -33% | 1 | 1 | 0% | 2,105 | 3,932 | +87% | 0 | 0 | — |
case-21 | pass→pass | 8,167 | 7,560 | -7% | 1 | 1 | 0% | 1,602 | 3,993 | +149% | 0 | 0 | — |
case-22 | pass→pass | 7,445 | 6,117 | -18% | 1 | 1 | 0% | 1,346 | 3,573 | +165% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +14 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.