Skip to content

Gatekeeper: what it actually does and how to read its decisions

The gauntlet every newly-downloaded app passes through — quarantine xattr, signature check, notarization check, user prompt. Where each decision is made and how to debug it.

Published 5 min read

When a freshly-downloaded app launches the first time, Gatekeeper is what decides whether macOS lets it run silently, runs it with a confirmation prompt, or refuses outright. The decision logic is layered: a quarantine extended attribute, a signature check, a notarization check, and finally a user dialog if needed.

This article walks each gate and shows how to debug when Gatekeeper says no.

The quarantine xattr

When a file is downloaded by an app that opted in (Safari, Mail, Messages, most browsers), the OS attaches a quarantine extended attribute to the file:

xattr -p com.apple.quarantine ~/Downloads/Foo.zip
# 0083;629a8c1a;Safari;org.mozilla.firefox|...

The xattr encodes:

  • A flags field — does this need user confirmation?
  • A timestamp.
  • The app that wrote it (the "where this came from").
  • A UUID linking to a record in the LaunchServices quarantine database.

The xattr is preserved through copies and into archives — extracting a downloaded zip preserves quarantine on the extracted files. This is what links a freshly-extracted binary back to "Safari downloaded this on Tuesday."

Files without a quarantine xattr (built locally, copied from another mounted disk image you signed yourself) are not subject to Gatekeeper checks — Gatekeeper only gates downloaded code.

The first-launch flow

When a quarantined app launches for the first time:

  1. LaunchServices reads the quarantine xattr to learn this is a first-launch.
  2. syspolicyd (the policy daemon) is consulted. It checks:
    • Is the binary code-signed and the signature valid?
    • Is the signing certificate trusted (Apple Developer ID, App Store, or Apple itself)?
    • Is the binary notarized?
  3. Decision tree based on findings:
    • App Store-signed → silent allow.
    • Developer-ID-signed + notarized → silent allow, but on Sequoia+ may prompt once.
    • Developer-ID-signed + NOT notarized → refuse (since 10.15+).
    • Self-signed / ad-hoc-signed → confirm dialog with the user; needs explicit override.
    • Unsigned → refuse.

The "Apple could not verify..." dialog is the user-facing manifestation of a Gatekeeper failure.

syspolicyd — where the policy lives

syspolicyd is the userspace daemon that implements Gatekeeper. Every first-launch goes through it.

syspolicyd consults:

  • The local SystemPolicy database (/var/db/SystemPolicy).
  • The notarization ticket store (/var/db/oah/).
  • The system's clock (notarization tickets have validity windows).
  • The current Gatekeeper mode (System Settings → Privacy & Security).

Its decision is cached: once it says "yes" to a binary, future launches don't re-check (until the binary changes or quarantine is reset).

You can interact with syspolicyd directly via spctl:

spctl --status                              # show current mode
spctl --assess --verbose /path/to/Foo.app   # show the decision and reason
spctl --add --label "MyApp" /path/to/Foo    # whitelist manually

The --verbose output is invaluable for debugging:

/path/to/Foo.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Acme Inc. (TEAMID12)

Or, when refused:

/path/to/Bar.app: rejected
source=Unnotarized Developer ID

The notarization lookup

When syspolicyd checks notarization, it:

  1. Looks for a stapled ticket in the binary or app bundle (Contents/_CodeSignature/CodeResources contains the ticket if stapled).
  2. If no stapled ticket, computes the CodeDirectory hash and queries Apple's notarization service over the network.
  3. If still no ticket: notarization-required and notarization missing → refuse.

The network lookup happens once per binary per machine; the answer is cached locally.

This is why developer-distributed apps should staple the ticket — it removes the network dependency on first launch. Without stapling, the first launch needs internet access to verify.

Gatekeeper modes

System Settings → Privacy & Security → "Allow applications from" has historically offered:

  • App Store only — accept App Store-signed only.
  • App Store and identified developers — accept App Store + notarized Developer ID.
  • Anywhere — accept anything (removed from the UI in 10.12; only reachable via terminal via spctl --master-disable).

On Sequoia+, the UI was tightened further: even "App Store and identified developers" now requires per-app right-click→Open for first-time runs of certain categories.

How to debug a Gatekeeper refusal

When an app won't run because of Gatekeeper:

  1. Check the quarantine xattr:

    xattr -l /path/to/Foo.app
    

    If com.apple.quarantine is present, it's a quarantined binary.

  2. Ask spctl:

    spctl --assess --verbose /path/to/Foo.app
    

    This tells you exactly why it's being rejected.

  3. Check the signature:

    codesign -dvvv /path/to/Foo.app
    

    Shows the cert chain, team ID, entitlements, and any signature validation errors.

  4. Check notarization specifically:

    spctl --assess --type install /path/to/Foo.dmg
    
  5. Override (carefully):

    xattr -d com.apple.quarantine /path/to/Foo.app
    

    Removes the quarantine xattr; Gatekeeper no longer applies. Only do this for binaries you trust — Gatekeeper is the front-line malware defense for novel binaries.

What Gatekeeper does NOT do

  • Gatekeeper doesn't check every launch. Once a binary is accepted, subsequent launches skip the check (until quarantine is re-applied or the binary changes).
  • Gatekeeper doesn't check binaries you compiled yourself. Without a quarantine xattr, no gate applies.
  • Gatekeeper doesn't sandbox. It decides whether to launch an app; once launched, the sandbox is what restricts what the app can do.
  • Gatekeeper doesn't scan in real time. Notarization is the scan; Gatekeeper just checks the result.

What surprises newcomers

  • First-launch friction is by design. Apple's making "I downloaded this from the internet" deliberately consequential.
  • App Store apps don't show prompts because the App Store pipeline is its own pre-vetting.
  • Quarantine survives extraction. A binary inside a downloaded zip carries the same trust state as the zip.
  • Right-click → Open is the user-facing override. Hard for malware to trick a user into; deliberate enough for legit cases.
apple-oss-distributions/SecurityOSX/libsecurity_codesigning/lib/policydb.cppThe system policy database — what syspolicyd consults.View on GitHub(line )

And the code signing chain article for the cryptography Gatekeeper relies on.

Related

Every Mach-O on a Mac is signed; every signature chains to an Apple root cert. Here's what the kernel actually verifies, what notarization adds, and how the SEP roots it all.
Six interlocking layers — code signing, AMFI, entitlements, sandbox profiles, SIP, TCC, and the SEP — that together decide what code is allowed to do on a Mac.
A separate ARM core, its own OS, a hardware mailbox. Here's how the main CPU talks to the SEP, what operations cross the boundary, and why kernel exploits don't compromise FileVault.