cheat sheet

codesign

End-to-end macOS distribution pipeline — sign binaries and app bundles with codesign, notarize with notarytool, staple tickets with stapler, and verify Gatekeeper acceptance with spctl.

codesign — macOS Code Signing, Notarization & Stapling

What it is

codesign is the Apple-supplied command-line tool for embedding cryptographic signatures into Mach-O binaries, application bundles (.app), kernel extensions, command-line tools, and installer packages on macOS. It ships at /usr/bin/codesign as part of the Command Line Tools. A code signature binds an identity (a certificate issued by Apple) to the bytes of an executable, declares the entitlements the executable may exercise, and locks the executable into the hardened runtime when so requested. Modern macOS distribution is a four-step pipeline: sign (codesign), notarize (xcrun notarytool), staple (xcrun stapler), verify (codesign --verify + spctl --assess). Skipping notarization is what triggers the "App can't be opened because Apple cannot check it for malicious software" dialog on macOS 10.15+; skipping stapling means a notarized app still hits the network on first launch to fetch its notarization ticket. The conceptual peer on Windows is Authenticode (signtool.exe); on Linux there is no direct equivalent — the closest is gpg --detach-sign plus a distribution-curated keyring.

Install

codesign, notarytool, stapler, and spctl are all bundled with Xcode and the Command Line Tools. If codesign --version reports command not found, install the tools.

bash
# Install Command Line Tools (no full Xcode required)
xcode-select --install

# Or, if Xcode is already installed, point xcode-select at it
sudo xcode-select -s /Applications/Xcode.app

# Verify
codesign --version
xcrun notarytool --version
xcrun stapler --help | head -1
spctl --version

Output:

text
0.7.0
0.42
OVERVIEW: Staple a notarization ticket to a code-signed file.
spctl version 1.3.0 (1.3.0)

Identities and certificates

Code signing requires a certificate with a private key in the user's keychain. The two relevant Apple certificate kinds for distribution outside the Mac App Store are Developer ID Application (for .app/binary signing of redistributable software) and Developer ID Installer (for .pkg packages). Both are issued by Apple via the Developer Program; the certificate's Common Name is what codesign --sign accepts as the identity argument.

bash
# List signing identities visible to codesign
security find-identity -v -p codesigning

# Just the names
security find-identity -v -p codesigning | awk -F'"' '/"/{print $2}'

Output:

text
  1) 1A2B3C4D5E6F7890ABCDEF0123456789ABCDEF01 "Developer ID Application: Alice Dev (TEAMID12)"
  2) 0F1E2D3C4B5A69788776655443322110FFEEDDCC "Developer ID Installer: Alice Dev (TEAMID12)"
  3) 9988776655443322110011223344556677889900 "Apple Development: alice@example.com (XXXXXXXXXX)"
     3 valid identities found

Identities can be referenced three ways: by full Common Name ("Developer ID Application: Alice Dev (TEAMID12)"), by SHA-1 fingerprint (1A2B3C4D5E6F...), or by the team ID parenthesized form. Scripts pin the fingerprint to avoid breaking when a certificate is renewed and the CN changes year-to-year.

Syntax

bash
codesign [--sign IDENTITY] [--options OPT,...] [--entitlements PLIST] \
         [--timestamp] [--deep] [--force] [--verbose=LEVEL] \
         PATH...

codesign --verify [--strict] [--deep] [--verbose=LEVEL] PATH
codesign --display [-d] [-r-] [--verbose=LEVEL] PATH
codesign --remove-signature PATH

Output: (none — exits 0 on success)

Essential options

OptionMeaning
-s / --sign IDENTITYSign with this identity (CN or fingerprint)
--options runtime[,library]Enable hardened runtime (required for notarization)
--entitlements FILE.plistAttach the entitlements file (XML plist)
--timestampInclude a secure timestamp from Apple's timestamp server (required for notarization)
--deepRecursively sign nested code (legacy; prefer signing each bundle explicitly)
-f / --forceReplace any existing signature
--preserve-metadata=...Keep entitlements/identifier/etc. from prior signature
--identifier IDOverride the bundle/program identifier
--prefix PREFIXSet the identifier prefix
--requirements - / --requirements TEXTDesignated requirement
--keychain KEYCHAINUse a specific keychain
-v / --verbose=LEVELVerbosity (1–4)
-d / --displayShow signature info
-r-Print the designated requirement
--verifyVerify the signature
--strictStrictest verification mode
--remove-signatureStrip the signature

Signing a single binary

The minimal case: a standalone Mach-O command-line tool. Sign with a Developer ID Application identity, attach the secure timestamp, and enable the hardened runtime. Verify with -dvv to dump the signed properties.

bash
# Sign
codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime \
         --timestamp \
         ./mytool

# Inspect the signature
codesign -dvv ./mytool

Output (codesign -dvv ./mytool):

text
Executable=/Users/alice/Projects/mytool/mytool
Identifier=mytool
Format=Mach-O thin (arm64)
CodeDirectory v=20500 size=531 flags=0x10000(runtime) hashes=11+2 location=embedded
Signature size=9056
Authority=Developer ID Application: Alice Dev (TEAMID12)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=May 25, 2026 at 14:02:11
Info.plist=not bound
TeamIdentifier=TEAMID12
Runtime Version=14.0.0
Sealed Resources=none
Internal requirements count=1 size=180

Strict verification — the form Gatekeeper itself runs — should also pass:

bash
codesign --verify --strict --verbose=2 ./mytool

Output:

text
./mytool: valid on disk
./mytool: satisfies its Designated Requirement

Signing an .app bundle

App bundles contain nested frameworks, helper tools, and resources. Each nested code item must be signed before the enclosing bundle is signed — the outer signature seals hashes of the inner signatures. Modern best practice is to sign each frame/helper explicitly with its own identifier and entitlements rather than relying on --deep (which is now deprecated for distribution because it skips entitlements files for nested code).

bash
# A typical .app layout
# MyApp.app/
#   Contents/
#     Info.plist
#     MacOS/MyApp
#     Frameworks/Sparkle.framework
#     Frameworks/MyLib.framework
#     PlugIns/Helper.appex
#     Resources/...

# Sign inside-out: nested frameworks first, then plugins, then the app
codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp --force \
         "MyApp.app/Contents/Frameworks/MyLib.framework/Versions/A/MyLib"

codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp --force \
         "MyApp.app/Contents/Frameworks/MyLib.framework"

codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp --force \
         --entitlements helper.entitlements.plist \
         "MyApp.app/Contents/PlugIns/Helper.appex"

# Outer bundle last, with its own entitlements
codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp --force \
         --entitlements MyApp.entitlements.plist \
         "MyApp.app"

# Verify the whole tree
codesign --verify --deep --strict --verbose=2 "MyApp.app"

Output:

text
MyApp.app: valid on disk
MyApp.app: satisfies its Designated Requirement

Loops for nested code

A common pattern for apps with many nested frameworks: walk the tree and sign each binary, sorting deepest first so children are sealed before parents.

bash
APP=MyApp.app
IDENTITY="Developer ID Application: Alice Dev (TEAMID12)"

find -s "$APP" \( -name "*.dylib" -o -name "*.framework" -o -name "*.bundle" \) \
  | sort -r \
  | while read -r f; do
      codesign --sign "$IDENTITY" --options runtime --timestamp --force "$f"
    done

codesign --sign "$IDENTITY" --options runtime --timestamp --force \
         --entitlements MyApp.entitlements.plist "$APP"

Output: (none — exits 0 on success)

The hardened runtime

The hardened runtime is a set of opt-in protections enabled by --options runtime. It restricts what a signed binary can do at execution: no unsigned writable memory pages (W^X), no dlopen of unsigned libraries (without a specific entitlement), no debugger attachment (without com.apple.security.get-task-allow), and no dyld environment variables. As of macOS 10.15, notarization requires the hardened runtimenotarytool rejects submissions without it. Once enabled, certain behaviours your app may rely on (loading random plugins, running scripts via interpreter shims) need explicit entitlement exceptions.

bash
# Inspect runtime flags on a binary
codesign -dvv ./mytool 2>&1 | grep -i flags

Output:

text
CodeDirectory v=20500 size=531 flags=0x10000(runtime) hashes=11+2

A flags value containing runtime (0x10000) means the hardened runtime is on. Other interesting flags: library-validation (0x2000), kill (0x200).

Entitlements

Entitlements are a property list (*.plist) of key-value pairs that grant a signed binary specific privileges. The classic categories are capabilities (com.apple.security.network.client, com.apple.security.files.user-selected.read-write), hardened-runtime exceptions (com.apple.security.cs.allow-jit, com.apple.security.cs.disable-library-validation), and App Sandbox keys (only meaningful for App Store apps). The plist is embedded into the signature; you cannot change entitlements after the fact without re-signing.

xml
<!-- MyApp.entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <false/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <false/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>
</plist>
bash
# Embed during sign
codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp \
         --entitlements MyApp.entitlements.plist \
         MyApp.app

# Inspect embedded entitlements
codesign -d --entitlements - MyApp.app

# Or as plain XML
codesign -d --entitlements :- MyApp.app

Output:

text
[Dict]
    [Key] com.apple.security.cs.allow-jit
    [Value]
        [Bool] true
    [Key] com.apple.security.network.client
    [Value]
        [Bool] true
    [Key] com.apple.security.files.user-selected.read-write
    [Value]
        [Bool] true

Notarization with notarytool

Notarization is Apple's malware scan and signing service for redistributable macOS software. You upload a signed .zip/.dmg/.pkg to Apple's service; they scan it and, if clean, issue a ticket that says "this binary's hash has been notarized". The modern client is xcrun notarytool (since Xcode 13); the older altool --notarize-app is deprecated. Authentication uses either an Apple ID + app-specific password + team ID, or — recommended for CI — an App Store Connect API key.

Authenticating once into the keychain

Store credentials in the login keychain under a profile name so subsequent invocations don't need to repeat them.

bash
# Method A: Apple ID + app-specific password (interactive)
xcrun notarytool store-credentials "AC_PASSWORD" \
  --apple-id alice@example.com \
  --team-id TEAMID12 \
  --password app-specific-password-goes-here

# Method B: App Store Connect API key (CI-friendly)
xcrun notarytool store-credentials "AC_API" \
  --key ~/Keys/AuthKey_ABCD1234.p8 \
  --key-id ABCD1234 \
  --issuer 12345678-1234-1234-1234-123456789012

Output:

text
This process stores your credentials securely in the Keychain. You reference these credentials later using a profile name.

Validating your credentials...
Success. Credentials validated.
Credentials saved to Keychain.
To use them, specify `--keychain-profile "AC_PASSWORD"`

Submitting for notarization

submit --wait blocks until the service returns a verdict. The --output-format json flag is the script-friendly form. If the verdict is anything other than Accepted, the log subcommand fetches the JSON breakdown — read this carefully, the error code field tells you which binary or which signature attribute caused the rejection.

bash
# 1. Package the signed app as a zip (notarytool only accepts zip/dmg/pkg)
ditto -c -k --keepParent MyApp.app MyApp.zip

# 2. Submit and wait for verdict
xcrun notarytool submit MyApp.zip \
  --keychain-profile "AC_PASSWORD" \
  --wait

# 3. Inspect submission log if it failed
xcrun notarytool log <SUBMISSION_UUID> --keychain-profile "AC_PASSWORD"

Output:

text
Conducting pre-submission checks for MyApp.zip and initiating connection to the Apple notary service...
Submission ID received
  id: 9f8e7d6c-5b4a-3210-fedc-ba9876543210
Successfully uploaded file
  id: 9f8e7d6c-5b4a-3210-fedc-ba9876543210
  path: /Users/alice/Projects/MyApp.zip
Waiting for processing to complete.
Current status: Accepted...........
Processing complete
  id: 9f8e7d6c-5b4a-3210-fedc-ba9876543210
  status: Accepted

A failing run instead returns status: Invalid and you fetch the log:

bash
xcrun notarytool log 9f8e7d6c-... --keychain-profile "AC_PASSWORD" \
  | jq '.issues[] | {severity, message, path}'

Output:

json
{"severity":"error","message":"The binary is not signed with a valid Developer ID certificate.","path":"MyApp.zip/MyApp.app/Contents/MacOS/MyApp"}
{"severity":"error","message":"The signature does not include a secure timestamp.","path":"MyApp.zip/MyApp.app/Contents/MacOS/MyApp"}
{"severity":"error","message":"The executable does not have the hardened runtime enabled.","path":"MyApp.zip/MyApp.app/Contents/MacOS/MyApp"}

Polling without --wait

For long-running CI where you don't want to hold a worker:

bash
xcrun notarytool submit MyApp.zip --keychain-profile "AC_PASSWORD"
# ... record the submission id ...

xcrun notarytool info <UUID> --keychain-profile "AC_PASSWORD"
xcrun notarytool history --keychain-profile "AC_PASSWORD"

Output:

text
id: 9f8e7d6c-...
  createdDate: 2026-05-25T14:02:11.000Z
  status: In Progress
  name: MyApp.zip

Stapling with stapler

Notarization produces a ticket that the running Mac fetches from Apple over the network on first launch. Stapling attaches that ticket directly to the artifact so it works offline (or behind a firewall). Stapling is mandatory for .dmg distribution: the ticket lives on the DMG itself; without it, users running offline will see the malware warning even though the DMG is notarized.

bash
# Staple an app (after notarization succeeded)
xcrun stapler staple MyApp.app

# Staple a DMG
xcrun stapler staple MyApp.dmg

# Validate that the ticket is attached and matches
xcrun stapler validate MyApp.app
xcrun stapler validate -v MyApp.dmg

Output:

text
Processing: /Users/alice/Projects/MyApp.app
Processing: /Users/alice/Projects/MyApp.app
The staple and validate action worked!

Processing: /Users/alice/Projects/MyApp.dmg
The validate action worked!

xcrun stapler validate exits non-zero if the ticket is missing or doesn't match the artifact — wire that into your CI as the canonical "ready to ship" check.

Gatekeeper assessment with spctl

Gatekeeper is the macOS policy daemon that decides whether the system will launch a downloaded binary. spctl --assess runs the same check Gatekeeper would run on first launch; if it passes you can confidently distribute the artifact. The --type exec form is for binaries; --type install is for installer packages; the default for .app bundles is exec.

bash
spctl --assess --type exec --verbose=4 MyApp.app
spctl --assess --type install --verbose=4 MyApp.pkg

# As of macOS 14.4, Gatekeeper requires notarization for downloaded code
spctl --status                    # is Gatekeeper enabled?

Output:

text
MyApp.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Alice Dev (TEAMID12)

MyApp.pkg: accepted
source=Notarized Developer ID
origin=Developer ID Installer: Alice Dev (TEAMID12)

assessments enabled

A failing assessment is the most common pre-ship diagnostic — if spctl rejects the artifact, no amount of codesign --verify passing will save you on a user's Mac.

text
MyApp.app: rejected (the code is valid but does not seem to be an app)
source=Unnotarized Developer ID

source=Unnotarized Developer ID means the binary is correctly signed but has not been notarized — re-run notarytool submit and stapler staple.

Designated requirements

A designated requirement is a small expression (a "csreq") embedded in the signature that says "the code I will accept as compatible with me has these properties" — same team ID, same identifier, same anchor. Apple ships sensible defaults; you usually only inspect them. Read them with codesign -d -r-.

bash
# Show the designated requirement as text
codesign -d -r- MyApp.app

# Decode an existing requirement from binary form
csreq -r- -t < req.bin

# Author one from scratch (rarely needed)
csreq -r 'identifier "com.example.myapp" and anchor apple generic and certificate leaf[subject.OU] = "TEAMID12"' -b req.bin

Output:

text
Executable=/Users/alice/Projects/MyApp.app/Contents/MacOS/MyApp
designated => identifier "com.example.myapp" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "TEAMID12"

Removing a signature

--remove-signature strips a code signature, leaving an unsigned binary. This is occasionally useful when re-signing a third-party binary with your own identity for internal redistribution, or when an embedded signature is preventing modification of a binary you control.

bash
codesign --remove-signature ./mytool
codesign -dvv ./mytool 2>&1 | grep -i signature

Output:

text
./mytool: code object is not signed at all

The full pipeline

Putting it together — sign every nested binary, sign the outer app, package as a DMG (or zip), notarize, staple both the DMG and the app, verify everything one more time:

bash
APP=MyApp.app
DMG=MyApp.dmg
ID="Developer ID Application: Alice Dev (TEAMID12)"
ENT=MyApp.entitlements.plist
PROFILE="AC_PASSWORD"

# 1. Sign nested code (deepest first)
find -s "$APP" \( -name "*.dylib" -o -name "*.framework" -o -name "*.bundle" \) \
  | sort -r \
  | while read -r f; do
      codesign --sign "$ID" --options runtime --timestamp --force "$f"
    done

# 2. Sign outer app
codesign --sign "$ID" --options runtime --timestamp --force \
         --entitlements "$ENT" "$APP"

# 3. Verify locally before submitting
codesign --verify --deep --strict --verbose=2 "$APP"
spctl --assess --type exec --verbose=4 "$APP" || \
  echo "Note: spctl will say 'rejected' until notarization+stapling complete"

# 4. Package
hdiutil create -volname MyApp -srcfolder "$APP" -ov -format UDZO "$DMG"

# 5. Sign the DMG itself
codesign --sign "$ID" --timestamp "$DMG"

# 6. Notarize
xcrun notarytool submit "$DMG" --keychain-profile "$PROFILE" --wait

# 7. Staple both DMG and inner app
xcrun stapler staple "$DMG"
xcrun stapler staple "$APP"   # if shipping the app on its own too

# 8. Final verification
xcrun stapler validate "$DMG"
spctl --assess --type install --verbose=4 "$DMG"   # for installer pkgs
spctl --assess --type exec   --verbose=4 "$APP"

Output (final spctl):

text
MyApp.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Alice Dev (TEAMID12)

codesign vs other signing tools

ToolScopeWhen to reach for it
codesignMach-O, .app, frameworks, bundlesEvery macOS signing task
productsign.pkg installer packagesSigning Distribution .pkg for installer tool
pkgbuild / productbuildBuild .pkg filesBefore productsign
xcrun notarytoolApple notary serviceAfter every signing pass for distribution
xcrun staplerAttach notary ticketAfter successful notarization
spctlGatekeeper assessmentPre-ship verification
csreqRead/write designated requirementsWhen customising signing requirements
xattr -d com.apple.quarantineRemove quarantine bitWhen sideloading binaries you trust
signtool.exe (Windows)AuthenticodeThe Windows analogue
gpg --detach-sign (Linux)Detached signatureLoosely equivalent for tarballs / releases

Common pitfalls

  1. Forgetting --timestamp — notarization will reject any signature without a secure timestamp. The flag is not the default; you must pass it explicitly on every codesign invocation in the pipeline.
  2. Using --deep instead of explicit per-binary signing--deep skips per-bundle entitlements and is deprecated for distribution. Sign each nested framework/helper individually with its own identity and entitlements file.
  3. Hardened runtime not enabled (--options runtime) — notarization will reject the binary with The executable does not have the hardened runtime enabled. This is the single most common cause of failed first notarization runs.
  4. App-specific password used directly each invocation — leaks the password into shell history and ps. Always run notarytool store-credentials once and use --keychain-profile.
  5. Notarizing without stapling the DMG — the app inside is notarized, but the DMG itself isn't. Users opening the DMG offline see the unsigned warning. Staple both.
  6. codesign --verify passes but spctl --assess failscodesign checks structural validity; spctl checks Gatekeeper policy (notarization status, certificate trust). The first does not imply the second.
  7. Signing without --force re-signs in place but does not strip a broken existing signature — when iterating, always pass --force (or --remove-signature first) to ensure the new signature replaces the old.
  8. Editing files inside the bundle after signing — any change to Resources/, Info.plist, or executable bytes invalidates the signature. Re-sign after every modification. Tools like plistutil are a common silent breaker.
  9. Wrong certificate kind for the artifact.app and binaries need Developer ID Application; .pkg installers need Developer ID Installer. Using the wrong one will fail Gatekeeper assessment even after notarization.
  10. Stapling a .zip — you can notarize a .zip (with the .app inside), but you cannot staple a .zip. Staple the inner .app, re-zip, and ship that.
  11. Entitlements file with stray top-level keyscodesign accepts malformed plists silently in older versions. Always run plutil -lint MyApp.entitlements.plist before signing.
  12. Forgetting that secure timestamps require Apple's timestamp server reachable — air-gapped or restricted-egress build machines need a proxy or pre-signed timestamps. --timestamp=none exists but disqualifies you from notarization.

Sources

References consulted while writing this article. Links open in a new tab.

  • Apple Developer — codesign(1) man page — Authoritative flag list used while writing the options reference.
  • SS64 — codesign — Cross-version notes.

Real-world recipes

Bootstrap script: sign + notarize + staple a single-binary CLI

A redistributable command-line tool — the simplest possible pipeline that still produces a Gatekeeper-accepted result on macOS 14+.

bash
#!/usr/bin/env bash
set -euo pipefail

BINARY=mytool
ID="Developer ID Application: Alice Dev (TEAMID12)"
PROFILE="AC_PASSWORD"

# 1. Sign
codesign --sign "$ID" --options runtime --timestamp --force "$BINARY"

# 2. Wrap in a zip for notarytool
ditto -c -k --keepParent "$BINARY" "$BINARY.zip"

# 3. Notarize
xcrun notarytool submit "$BINARY.zip" --keychain-profile "$PROFILE" --wait

# 4. Notarytool stamps the ticket against the binary's hash, but command-line
#    tools cannot be stapled directly (stapling needs a wrapping bundle/DMG/pkg).
#    For a CLI ship the .zip — Gatekeeper will fetch the ticket on first run.

# 5. Verify
codesign --verify --strict --verbose=2 "$BINARY"
echo "Done. Distribute $BINARY.zip"

Output:

text
Processing complete
  status: Accepted
mytool: valid on disk
mytool: satisfies its Designated Requirement
Done. Distribute mytool.zip

CI snippet: notarize and bail on the first failed log issue

bash
#!/usr/bin/env bash
set -euo pipefail

ARTIFACT="MyApp.dmg"
PROFILE="AC_API"   # App Store Connect API key profile

submission=$(xcrun notarytool submit "$ARTIFACT" \
  --keychain-profile "$PROFILE" \
  --output-format json --wait)

status=$(jq -r '.status' <<<"$submission")
uuid=$(jq -r '.id'     <<<"$submission")

if [[ "$status" != "Accepted" ]]; then
  echo "Notarization failed with status: $status"
  xcrun notarytool log "$uuid" --keychain-profile "$PROFILE" | jq .
  exit 1
fi

xcrun stapler staple "$ARTIFACT"
xcrun stapler validate "$ARTIFACT"
echo "$ARTIFACT is shippable."

Output:

text
MyApp.dmg is shippable.

Remove quarantine from a self-signed internal tool

For an internal binary that is signed with your own (untrusted) certificate, distributing it via download will attach com.apple.quarantine xattrs that Gatekeeper checks. Strip the xattr after install on each target machine.

bash
xattr -dr com.apple.quarantine /Applications/InternalTool.app
spctl --assess --type exec /Applications/InternalTool.app

Output:

text
/Applications/InternalTool.app: rejected (the code is valid but does not seem to be an app)
source=Unsigned Origin

For internal-only distribution you can also disable Gatekeeper for ad-hoc-signed code with spctl --add plus the binary path — but the modern and safer answer is to enroll the binary in your MDM-pushed signed Developer ID flow.

Compare two signed binaries

When debugging "works on my machine, fails on theirs", a fast first check is whether the two copies of the binary have identical signatures.

bash
codesign -dvv ./build-a/MyApp.app 2>&1 | grep -E "Identifier|CodeDirectory|Authority|TeamIdentifier|Timestamp"
codesign -dvv ./build-b/MyApp.app 2>&1 | grep -E "Identifier|CodeDirectory|Authority|TeamIdentifier|Timestamp"

# Or hash-compare the CDHash directly
codesign -dvv ./build-a/MyApp.app 2>&1 | grep CDHash
codesign -dvv ./build-b/MyApp.app 2>&1 | grep CDHash

Output:

text
Identifier=com.example.myapp
CodeDirectory v=20500 size=531 flags=0x10000(runtime) hashes=11+2
Authority=Developer ID Application: Alice Dev (TEAMID12)
TeamIdentifier=TEAMID12
Timestamp=May 25, 2026 at 14:02:11
CDHash=8a1f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f90
CDHash=8a1f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f90

Strip and re-sign a downloaded third-party tool with your identity

When wrapping a public binary into your own distribution, you must strip its signature and re-sign — otherwise notarization fails because the Team ID won't match yours.

bash
codesign --remove-signature ./vendor/tool
codesign --sign "Developer ID Application: Alice Dev (TEAMID12)" \
         --options runtime --timestamp \
         ./vendor/tool
codesign --verify --strict --verbose=2 ./vendor/tool

Output:

text
./vendor/tool: valid on disk
./vendor/tool: satisfies its Designated Requirement

Inspect entitlements of any signed binary

When debugging "feature X works in build A but not in build B", check whether the relevant entitlement is present.

bash
codesign -d --entitlements :- /Applications/SomeApp.app \
  | plutil -p -                # pretty-print the embedded plist

Output:

text
{
  "com.apple.security.app-sandbox" => 1
  "com.apple.security.network.client" => 1
  "com.apple.security.network.server" => 0
  "com.apple.security.files.user-selected.read-write" => 1
}

Audit every signed binary in /Applications

A one-liner that flags any installed app that fails strict signature verification — useful during incident response.

bash
for app in /Applications/*.app; do
  result=$(codesign --verify --strict --no-strict --deep "$app" 2>&1)
  if [[ $? -ne 0 ]]; then
    echo "FAIL: $app"
    echo "    $result"
  fi
done

Output:

text
FAIL: /Applications/OldShareware.app
    /Applications/OldShareware.app: a sealed resource is missing or invalid