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.
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:
- LaunchServices reads the quarantine xattr to learn this is a first-launch.
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?
- 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:
- Looks for a stapled ticket in the binary or app bundle (
Contents/_CodeSignature/CodeResourcescontains the ticket if stapled). - If no stapled ticket, computes the CodeDirectory hash and queries Apple's notarization service over the network.
- 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:
-
Check the quarantine xattr:
xattr -l /path/to/Foo.appIf
com.apple.quarantineis present, it's a quarantined binary. -
Ask spctl:
spctl --assess --verbose /path/to/Foo.appThis tells you exactly why it's being rejected.
-
Check the signature:
codesign -dvvv /path/to/Foo.appShows the cert chain, team ID, entitlements, and any signature validation errors.
-
Check notarization specifically:
spctl --assess --type install /path/to/Foo.dmg -
Override (carefully):
xattr -d com.apple.quarantine /path/to/Foo.appRemoves 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.
What to read next
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.