macOS Onboarding & Permission Request Guide
Research date: 2026-05-21
Sources: Apple HIG, Apple Developer Documentation, WWDC17/22/23/24 sessions, Apple Tech Notes
Table of Contents
- Apple HIG — Onboarding
- Apple HIG — Requesting Permissions
- macOS System Permission APIs
- Standard macOS First-Launch Patterns
- App Store & Notarization Requirements
- System Settings Deep-Link URL Schemes
- Implementation Reference
- Model Setup & Local File Verification Guide
1. Apple HIG — Onboarding
Core Philosophy (WWDC17 "Love at First Launch")
Apple's overriding principle is: lead with value, not with gates. The WWDC17 session uses the metaphor of a demanding landlord asking for a passport, birth certificate, and pay stubs before letting you view an apartment — that is what an app that opens with a login wall, terms screen, and four permission requests feels like.
"Always lead with great content. Show off what makes your app great right off the bat and let people dig in." — WWDC17, Session 816
"People have access to millions of apps in the App Store. You've already passed the first hurdle when someone installs your app. Now you have to prevent them from backing out and deleting your app." — WWDC17, Session 816
What Apple Says About First-Launch Experiences
From the HIG (Onboarding pattern page) and WWDC sessions:
DO:
- Show value and content immediately on first launch — no barriers
- Keep onboarding brief, enjoyable, and optional
- Teach through interactive experience, not static instruction slides
- Use progressive disclosure: teach one concept at a time, advance only after user demonstrates competency
- Provide a dedicated "How to Play" / "Help" reference that users can return to at any time
- Use multiple short, contextual tutorials rather than one long upfront tutorial
- Strive for interfaces so intuitive that no upfront instruction is needed at all
DO NOT:
- Greet users with login/registration screens before they see value
- Show splash screens, EULA agreements, or disclaimers as the first screen (put these on the App Store product page instead)
- Request reviews, ratings, or push notification opt-ins during onboarding
- Present all permissions upfront at launch
- Show non-core information (daily challenges, leaderboards, tournaments) during initial onboarding
- Re-show the tutorial after a user has dismissed or skipped it
- Assume the user knows things they haven't been taught
On Tutorials / Multi-Step Flows:
- Provide a skip option at the start of any introductory sequence
- If a user skips on first launch, do not present it again on subsequent launches
- Make it easy to find tutorials later (in a help menu, settings, or dedicated button)
- The Clash Royale model (five short tutorials, each adding complexity) is cited as exemplary
How Many Steps/Pages?
Apple does not specify a maximum step count in the HIG. The guidance is qualitative:
- "Brief and enjoyable" — err on the side of fewer
- Each step should teach exactly one thing
- Stop onboarding as soon as the user can be self-directed
- For permission-heavy apps: address permissions just-in-time (see Section 2), which naturally spreads them across normal usage rather than stacking them in a wizard
Progressive Disclosure Principles
- Begin simple. Introduce only the core action on step 1.
- Require competency before advancing. Let the user perform the action, then unlock the next concept.
- Earlier tutorials are more guided; later ones challenge the user to apply knowledge independently.
- Contextual tips (floating overlays, coach marks) at the moment of first use are preferred over front-loaded slides.
- Non-core features (power user settings, advanced configuration) belong in Settings, not onboarding.
What Should NOT Be in Onboarding
| Item | Reason |
|---|---|
| License agreements / terms of service | App Store displays these before download |
| Rating/review prompts | Premature; user has no opinion yet |
| Push notification opt-in | HIG: ask only when user triggers a feature that needs it |
| Account creation | Show value first; let registration be triggered by a specific action |
| All permission requests at once | Overwhelming; grant rates drop; HIG requires just-in-time |
| Splash screens with no content | Pure delay with no value |
| Marketing copy about the app | User already downloaded it |
2. Apple HIG — Requesting Permissions
The Foundational Rule: Just-in-Time, Not Upfront
"The app requests permissions on a need-to-know basis. So you retain a sense of control over both the experience and your private data." — WWDC17, Session 816 (describing Strava's exemplary approach)
The HIG (Privacy page) states explicitly:
- Request personal data only when your app clearly needs it.
- Request permission at launch only when necessary for your app to function — i.e., only when the feature in question is the entire core purpose of the app (example: a navigation app requesting location at first launch is acceptable because without it the app is useless).
- For all other permissions: ask when the user triggers the feature that needs the data, not before.
The concrete Strava example from WWDC17:
- Location permission → asked when user taps "Record a Ride"
- Camera permission → asked when user taps "Add Photo to Ride"
- Contacts permission → asked when user opens "Find Friends"
Result: users feel in control, grant rates are higher, and the permission prompt has obvious context.
When It Is Acceptable to Request at Launch
A permission may be requested at launch if:
- The app cannot function at all without it (e.g., a transit app that is entirely location-driven)
- Immediately after granting, the app visibly demonstrates the benefit (the transit map zooms to show nearby routes)
How to Frame Permission Requests (Purpose Strings)
The system alert shows your NSxxxUsageDescription string. Apple's rules:
- Short and specific. Usually one or two sentences maximum.
- Use sentence case (capitalize only the first word).
- Be polite — never pressure or alarm the user.
- Omit the app name — the system already shows it.
- Include concrete examples of how the data benefits the user.
- Do not replicate the system alert UI with a pre-prompt screen that looks like a system dialog.
Good example:
"Pheron Agent uses your microphone to transcribe voice commands and generate audio notes."
Bad example:
"PHERON AGENT REQUIRES MICROPHONE ACCESS. Please tap Allow or the app will not function."
What Apple Says About Requesting Multiple Permissions at Once
The HIG does not explicitly ban multi-permission onboarding pages, but the just-in-time principle makes them incompatible with good practice. The pattern of presenting a permission list page at launch (e.g., "Grant these 5 permissions to get started") conflicts with two HIG rules:
- Don't request permissions before the user understands why
- Don't present all permissions upfront
If you do need a permissions setup page (e.g., for an agent/automation tool where several permissions are core to the app's function), the HIG guidance supports requesting them only if:
- Each permission is clearly linked to a specific feature the user is about to use
- The user can skip or defer each individual one
- Denied permissions degrade gracefully with an offer to re-request later
What to Do When Permission Is Denied
- Never re-present the system alert for a denied permission. The system will not show it again; calling the request API after denial is a no-op.
- Provide a graceful degraded experience without the denied permission.
- Offer an obvious path to Settings to grant it later: use the System Settings deep-link URLs (Section 6).
- Use an in-app banner or contextual prompt at the relevant feature entry point.
Required Privacy Usage Description Strings (Info.plist)
All of these must be present in Info.plist before the relevant API is called, or the app will crash/terminate:
| Permission | Info.plist Key | Notes |
|---|---|---|
| Contacts | NSContactsUsageDescription |
Required for CNContactStore |
| Calendar (full access) | NSCalendarsFullAccessUsageDescription |
macOS 14+ / iOS 17+; replaces old key |
| Calendar (write-only) | NSCalendarsWriteOnlyAccessUsageDescription |
macOS 14+ / iOS 17+; for add-only access |
| Reminders (full) | NSRemindersFullAccessUsageDescription |
macOS 14+ / iOS 17+ |
| Microphone | NSMicrophoneUsageDescription |
Required for AVCaptureDevice audio |
| Camera | NSCameraUsageDescription |
Required for AVCaptureDevice video |
| Photos Library | NSPhotoLibraryUsageDescription |
Read access to photo library |
| Photos Add-Only | NSPhotoLibraryAddUsageDescription |
Write-only photo access |
| Location (always) | NSLocationAlwaysAndWhenInUseUsageDescription |
|
| Location (in use) | NSLocationWhenInUseUsageDescription |
|
| Accessibility | NSAccessibilityUsageDescription |
Hardened Runtime apps must declare this |
| Apple Events | NSAppleEventsUsageDescription |
Required for scripting other apps |
| Screen Recording | No Info.plist key | Controlled entirely via TCC; no usage string |
| Full Disk Access | No Info.plist key | Controlled entirely via TCC; no usage string |
| Bluetooth | NSBluetoothAlwaysUsageDescription |
macOS 12+ |
| Speech Recognition | NSSpeechRecognitionUsageDescription |
|
| Focus | NSFocusStatusUsageDescription |
Note on legacy Calendar key:
NSCalendarsUsageDescriptionis the old key (pre-macOS 14 / pre-iOS 17) and is deprecated. Apps targeting macOS 14+ must use the new granular keys. If you need to support macOS 13 and earlier as well, include both the old and new keys.
3. macOS System Permission APIs
3.1 Contacts — CNContactStore
Framework: Contacts
Info.plist key: NSContactsUsageDescription
Sandbox entitlement: com.apple.security.personal-information.addressbook
import Contacts
func requestContactsAccess() async {
let store = CNContactStore()
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .notDetermined:
do {
let granted = try await store.requestAccess(for: .contacts)
// handle granted/denied
} catch {
// handle error
}
case .authorized:
// already granted
case .denied, .restricted:
// guide user to System Settings
@unknown default:
break
}
}
Authorization status enum: CNAuthorizationStatus — .notDetermined, .restricted, .denied, .authorized
Key behavior:
- Calling
requestAccess(for:)when status is.denieddoes nothing (system won't prompt again) - The
asyncoverload (requestAccess(for:) async throws) is available in Swift 5.5+ / macOS 12+ - Completion-handler overload available for earlier targets
3.2 Calendar — EKEventStore (macOS 14+ API)
Framework: EventKit
Info.plist keys (macOS 14+):
- Full access:
NSCalendarsFullAccessUsageDescription - Write-only:
NSCalendarsWriteOnlyAccessUsageDescription
Sandbox entitlement: com.apple.security.personal-information.calendars
CRITICAL: The old requestAccess(to: .event) and requestAccess(to:completion:) are deprecated in macOS 14 / iOS 17 and do NOT prompt the user — they throw an error instead. Use the new granular APIs:
import EventKit
// Three levels of access:
// 1. No access — user must use EventKitUI pickers or Siri suggestions
// 2. Write-only — app can CREATE events but cannot read existing ones
// 3. Full access — app can read, write, and manage calendars
let store = EKEventStore()
// Check current status
let status = EKEventStore.authorizationStatus(for: .event)
// EKAuthorizationStatus: .notDetermined, .restricted, .denied, .fullAccess, .writeOnly
// Request full access (read + write)
func requestFullCalendarAccess() async throws {
try await store.requestFullAccessToEvents()
}
// Request write-only access (add events only)
func requestWriteOnlyCalendarAccess() async throws {
try await store.requestWriteOnlyAccessToEvents()
}
EKAuthorizationStatus cases (macOS 14+):
.notDetermined— not yet asked.restricted— system policy prevents access (e.g., parental controls).denied— user denied.fullAccess— full read/write granted.writeOnly— add-only granted
Principle of least privilege: Request write-only access unless you actually need to read existing events. With write-only, the app cannot read the calendar list, read existing events (even its own), or create new calendars.
3.3 Screen Recording — CGRequestScreenCaptureAccess
Framework: CoreGraphics
Info.plist key: None required
Entitlement: None (controlled by TCC; cannot be pre-approved via entitlement)
import CoreGraphics
// Check without prompting
func hasScreenRecordingPermission() -> Bool {
return CGPreflightScreenCaptureAccess()
}
// Request permission (opens System Settings prompt if not already granted)
// IMPORTANT: Does NOT wait for user response — returns immediately
func requestScreenRecordingPermission() {
CGRequestScreenCaptureAccess()
}
Critical behavior notes:
CGPreflightScreenCaptureAccess()— check only, never triggers the TCC prompt or adds app to listCGRequestScreenCaptureAccess()— triggers the system to add your app to the Screen Recording list and shows a prompt; returns immediately without waiting- Call
CGRequestScreenCaptureAccess()at app startup (before the user needs it) so the app appears in Privacy & Security > Screen Recording CGPreflightScreenCaptureAccess()was deprecated in macOS 15.1 (Sequoia) for some use cases; prefer ScreenCaptureKit for new code- Modern alternative: Use
ScreenCaptureKit—SCShareableContentAPIs require Screen Recording permission and handle the TCC check internally
To check status after requesting:
// Poll or use app activation notification since there's no callback
NotificationCenter.default.addObserver(
forName: NSApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
let granted = CGPreflightScreenCaptureAccess()
// update UI accordingly
}
3.4 Accessibility — AXIsProcessTrustedWithOptions
Framework: ApplicationServices
Info.plist key: NSAccessibilityUsageDescription (required for Hardened Runtime apps)
Entitlement: com.apple.security.accessibility (for sandboxed apps)
Note: Accessibility permission cannot be granted to sandboxed Mac App Store apps in the traditional sense. This permission is primarily for Developer ID / direct-distribution apps.
import ApplicationServices
// Check if trusted (no prompt)
func isAccessibilityTrusted() -> Bool {
return AXIsProcessTrusted()
}
// Check and optionally prompt / open System Settings
func requestAccessibilityPermission(prompt: Bool = true) -> Bool {
let options: NSDictionary = [
kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: prompt
]
return AXIsProcessTrustedWithOptions(options)
}
// Open Accessibility pane in System Settings directly
func openAccessibilitySettings() {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
}
Behavior notes:
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])opens System Settings to Accessibility pane on first call when not trusted, and returnsfalse- After the user grants permission, the function returns
true - Signing with a Developer ID certificate ensures the system recognizes it as the same app across builds (permission persists)
- To monitor for the user granting permission, observe
NSApplication.didBecomeActiveNotificationand re-checkAXIsProcessTrusted()
Info.plist (required for Hardened Runtime):
<key>NSAccessibilityUsageDescription</key>
<string>Pheron Agent uses accessibility features to observe and control other applications on your behalf.</string>
3.5 Full Disk Access — No Programmatic API
Info.plist key: None
Entitlement: None available for this TCC category via standard API
Full Disk Access (FDA) cannot be requested programmatically. Apple intentionally provides no API for this. There is no CGRequestFullDiskAccess() or equivalent.
How to handle:
Detection (heuristic): Try accessing a known TCC-protected path. If it succeeds, FDA is granted.
func hasFullDiskAccess() -> Bool { let testPath = NSHomeDirectory() + "/Library/Application Support/com.apple.TCC/TCC.db" return FileManager.default.isReadableFile(atPath: testPath) }This is an unofficial heuristic. Apple does not provide an official API. Some apps use the
FullDiskAccessSwift package (by inket) which formalizes this check.Guiding the user:
func openFullDiskAccessSettings() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { NSWorkspace.shared.open(url) } }In onboarding: Instruct the user to go to System Settings > Privacy & Security > Full Disk Access, then toggle your app on. Use a visual guide (screenshot or animation) showing the exact path.
Important: On macOS 10.15+, calling
hasFullDiskAccess()may automatically add your app to the Full Disk Access list in an unchecked state — which is actually desirable, as the user can then simply toggle it on.
3.6 Microphone — AVCaptureDevice
Framework: AVFoundation
Info.plist key: NSMicrophoneUsageDescription
Sandbox entitlement: com.apple.security.device.microphone
import AVFoundation
// Check current authorization status
func microphoneAuthorizationStatus() -> AVAuthorizationStatus {
return AVCaptureDevice.authorizationStatus(for: .audio)
}
// Request access (async/await)
func requestMicrophoneAccess() async -> Bool {
return await AVCaptureDevice.requestAccess(for: .audio)
}
// Request access (completion handler)
func requestMicrophoneAccess(completion: @escaping (Bool) -> Void) {
AVCaptureDevice.requestAccess(for: .audio, completionHandler: completion)
}
AVAuthorizationStatus cases: .notDetermined, .restricted, .denied, .authorized
Key behavior:
- The completion handler /
asyncreturn value reflects the immediate user decision - If already
.authorizedor.denied, callingrequestAccessreturns the existing status immediately without re-prompting - For camera, substitute
.audiowith.videoand useNSCameraUsageDescription
3.7 Permission Status Summary Table
| Permission | API to check | API to request | Can request programmatically | Info.plist key |
|---|---|---|---|---|
| Contacts | CNContactStore.authorizationStatus(for:) |
store.requestAccess(for:) |
Yes | NSContactsUsageDescription |
| Calendar (full) | EKEventStore.authorizationStatus(for: .event) |
store.requestFullAccessToEvents() |
Yes (macOS 14+) | NSCalendarsFullAccessUsageDescription |
| Calendar (write) | EKEventStore.authorizationStatus(for: .event) |
store.requestWriteOnlyAccessToEvents() |
Yes (macOS 14+) | NSCalendarsWriteOnlyAccessUsageDescription |
| Microphone | AVCaptureDevice.authorizationStatus(for: .audio) |
AVCaptureDevice.requestAccess(for: .audio) |
Yes | NSMicrophoneUsageDescription |
| Camera | AVCaptureDevice.authorizationStatus(for: .video) |
AVCaptureDevice.requestAccess(for: .video) |
Yes | NSCameraUsageDescription |
| Screen Recording | CGPreflightScreenCaptureAccess() |
CGRequestScreenCaptureAccess() |
Partial (no callback) | None |
| Accessibility | AXIsProcessTrusted() |
AXIsProcessTrustedWithOptions([prompt: true]) |
Opens System Settings | NSAccessibilityUsageDescription |
| Full Disk Access | Heuristic file read | No API — guide user manually | No | None |
4. Standard macOS First-Launch Patterns
4.1 Tracking First Launch with NSUserDefaults / @AppStorage
The standard pattern is checking for the absence of a flag key:
// Using @AppStorage (SwiftUI) — recommended
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
// In the App body:
var body: some Scene {
WindowGroup {
if hasCompletedOnboarding {
ContentView()
} else {
OnboardingView()
}
}
}
// Using UserDefaults (UIKit / AppKit compatible)
extension UserDefaults {
static let hasCompletedOnboardingKey = "hasCompletedOnboarding"
var hasCompletedOnboarding: Bool {
get { bool(forKey: Self.hasCompletedOnboardingKey) }
set { set(newValue, forKey: Self.hasCompletedOnboardingKey) }
}
}
// On first launch, the key does not exist, bool(forKey:) returns false
// Set to true when onboarding completes:
UserDefaults.standard.hasCompletedOnboarding = true
Important caveats for macOS:
- UserDefaults does NOT persist across app deletion/reinstallation (unlike iOS Keychain)
- If persistence across reinstalls is needed (e.g., for license acceptance), use the Keychain
- On macOS, UserDefaults are stored per-app in
~/Library/Preferences/com.yourcompany.yourapp.plist - Avoid using UserDefaults for any values that must be synchronized across multiple processes/actors in the same app without proper synchronization
4.2 macOS Window Presentation Pattern for Onboarding
Recommended approach for SwiftUI macOS apps:
@main
struct Pheron AgentApp: App {
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
var body: some Scene {
WindowGroup("Pheron Agent") {
Group {
if hasCompletedOnboarding {
ContentView()
} else {
OnboardingView(onComplete: {
hasCompletedOnboarding = true
})
}
}
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
}
}
Alternative: Sheet over content (for lighter onboarding like "What's New"):
// Present onboarding as a sheet when version changes
@AppStorage("lastSeenVersion") private var lastSeenVersion = ""
ContentView()
.sheet(isPresented: $showOnboarding) {
WhatsNewView()
}
.onAppear {
let currentVersion = Bundle.main.shortVersionString ?? ""
if lastSeenVersion != currentVersion {
showOnboarding = true
lastSeenVersion = currentVersion
}
}
4.3 What Apple's Own Apps Do
Shortcuts app: Opens directly to the library; no onboarding. A "Get Started" gallery is available in the sidebar for discovery.
Reminders: Opens to the list view immediately; no onboarding. Creates a default "Reminders" list on first launch.
Notes: Opens to an empty note or creates a "Welcome to Notes" sample note on first launch.
Common patterns in Apple's own apps:
- No blocking first-launch screen. Content appears immediately.
- Sample/default content is created to demonstrate the UI in context (Reminders default list, Notes welcome note).
- iCloud setup is handled by the OS at OS-level setup, not per-app.
- Permissions are requested inline when the user triggers the relevant feature.
4.4 Third-Party App Patterns (Bear, Notion, Spark)
These apps represent the "setup wizard" model — more opinionated but Apple-compliant if done well:
Bear (notes app):
- No blocking onboarding wall
- First launch shows a "Welcome to Bear" note in the editor (content-first)
- iCloud sync permission requested naturally by the OS
Notion:
- Web-auth flow only (account required); desktop app is essentially a web wrapper
- Not a good model for native macOS onboarding
Spark (email):
- Account setup is mandatory (app has no value without email) — so this is a legitimate launch-time request
- Permission for notifications deferred until after account setup and first email is viewed
The pattern behind these: Even when setup is required, show a preview of the finished experience before completing setup. Let users understand what they're setting up.
5. App Store & Notarization Requirements
5.1 PrivacyInfo.xcprivacy — Privacy Manifest
Added to Xcode 15; enforcement began May 1, 2024 for App Store submissions.
Every app and SDK must include a PrivacyInfo.xcprivacy file (a property list) describing data collection and API usage.
Top-level keys:
<?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>
<!-- Required if you use tracking APIs -->
<key>NSPrivacyTracking</key>
<false/>
<!-- Domains used for tracking (if NSPrivacyTracking = true) -->
<key>NSPrivacyTrackingDomains</key>
<array/>
<!-- Required Reason APIs (iOS/tvOS/watchOS/visionOS only, NOT macOS as of 2024) -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string><!-- Reading/writing user preferences -->
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string><!-- File timestamp needed for app logic -->
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string><!-- Check disk space before large file operations -->
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string><!-- Calculate time intervals for performance -->
</array>
</dict>
</array>
<!-- Data types collected -->
<key>NSPrivacyCollectedDataTypes</key>
<array>
<!-- Add entries for each data type your app collects -->
</array>
</dict>
</plist>
macOS-specific note: As of 2024,
NSPrivacyAccessedAPITypes(Required Reason APIs) enforcement applies to iOS, iPadOS, tvOS, visionOS, and watchOS, but not macOS. However, including it in your manifest is harmless and future-proofs your app.
5.2 Required Reason API Categories
These four categories always require declared reasons in NSPrivacyAccessedAPITypes (for applicable platforms):
| Category | String | Common APIs |
|---|---|---|
| File Timestamps | NSPrivacyAccessedAPICategoryFileTimestamp |
FileManager attribute queries |
| System Boot Time | NSPrivacyAccessedAPICategorySystemBootTime |
ProcessInfo.processInfo.systemUptime, mach_absolute_time() |
| Disk Space | NSPrivacyAccessedAPICategoryDiskSpace |
URLResourceKey.volumeAvailableCapacityKey |
| User Defaults | NSPrivacyAccessedAPICategoryUserDefaults |
UserDefaults read/write |
5.3 Sandbox Entitlements Required for Permissions
If your app is sandboxed (required for Mac App Store), add these entitlements to your .entitlements file:
<!-- Contacts -->
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<!-- Calendar -->
<key>com.apple.security.personal-information.calendars</key>
<true/>
<!-- Photos Library -->
<key>com.apple.security.personal-information.photos-library</key>
<true/>
<!-- Microphone -->
<key>com.apple.security.device.microphone</key>
<true/>
<!-- Camera -->
<key>com.apple.security.device.camera</key>
<true/>
<!-- Location -->
<key>com.apple.security.personal-information.location</key>
<true/>
<!-- Bluetooth -->
<key>com.apple.security.device.bluetooth</key>
<true/>
<!-- Apple Events (for scripting other apps) -->
<key>com.apple.security.automation.apple-events</key>
<true/>
Accessibility and Screen Recording do not have sandbox entitlements that grant them — they are always TCC-controlled regardless of sandbox status. Accessibility is generally incompatible with the Mac App Store sandbox.
Full Disk Access has no entitlement either. Sandboxed apps cannot have FDA; it is only available for non-sandboxed (Developer ID) apps.
5.4 Notarization Requirements
For Developer ID (non-App Store) distribution, notarization requires:
- Hardened Runtime must be enabled
- No unsigned or ad-hoc signed code in the bundle
- Required entitlements for any restricted capabilities (scripting, JIT, etc.)
- No private API usage
PrivacyInfo.xcprivacy— required for App Store; strongly recommended for notarization
When using Hardened Runtime with restricted features, add exception entitlements:
<!-- Allow JIT compilation (e.g., for JavaScript engines) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Allow unsigned executable memory -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Disable library validation (needed for some plugins) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
6. System Settings Deep-Link URL Schemes
Use NSWorkspace.shared.open(url) with these URLs to direct users to specific System Settings panes after a permission is denied.
func openSystemSettings(for permission: Permission) {
let urlString: String
switch permission {
case .accessibility:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
case .screenRecording:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
case .fullDiskAccess:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
case .microphone:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"
case .camera:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera"
case .contacts:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Contacts"
case .calendar:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"
case .reminders:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders"
case .photos:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_Photos"
case .location:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_LocationServices"
case .inputMonitoring:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
case .speechRecognition:
urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition"
}
guard let url = URL(string: urlString) else { return }
NSWorkspace.shared.open(url)
}
Complete Privacy & Security deep-link table (verified through macOS Sequoia 15.2):
| Setting | URL Anchor |
|---|---|
| Full Privacy & Security | com.apple.preference.security |
| Accessibility | ?Privacy_Accessibility |
| All Files (Full Disk Access) | ?Privacy_AllFiles |
| App Management | ?Privacy_AppBundles |
| Automation | ?Privacy_Automation |
| Bluetooth | ?Privacy_Bluetooth |
| Calendar | ?Privacy_Calendars |
| Camera | ?Privacy_Camera |
| Contacts | ?Privacy_Contacts |
| Developer Tools | ?Privacy_DevTools |
| Files and Folders | ?Privacy_ApplicationData |
| Focus | ?Privacy_UserTracking |
| Full Disk Access | ?Privacy_AllFiles |
| HomeKit | ?Privacy_HomeKit |
| Input Monitoring | ?Privacy_ListenEvent |
| Location Services | ?Privacy_LocationServices |
| Media & Apple Music | ?Privacy_Media |
| Microphone | ?Privacy_Microphone |
| Motion & Fitness | ?Privacy_Motion |
| Photos | ?Privacy_Photos |
| Reminders | ?Privacy_Reminders |
| Screen Recording | ?Privacy_ScreenCapture |
| Speech Recognition | ?Privacy_SpeechRecognition |
| System Events | ?Privacy_SystemPolicySysAdminFiles |
Version note: These URL anchors use the
com.apple.preference.security?prefix which has been stable through macOS Monterey, Ventura, Sonoma, and Sequoia. Apple does not officially document these and they can change between major OS versions. Test on each new macOS release. An alternative new format seen in macOS 14+ iscom.apple.settings.PrivacySecurity.extension?Privacy_*— both currently work.
7. Implementation Reference
7.1 Recommended Onboarding Architecture for Pheron Agent
Given that Pheron Agent is an autonomous agent that needs several permissions to function, the recommended approach is a lightweight permission setup page at first launch — justified because multiple permissions are core to the app's function, not incidental features.
Principles to follow:
- Show a brief value proposition screen first (what Pheron Agent can do for you), then the permissions page
- Each permission card explains exactly what feature it enables
- All permissions are individually skippable (the agent degrades gracefully without each one)
- Denied permissions show a "Grant Access" button that opens the relevant System Settings pane
- Monitor for permission changes via
NSApplication.didBecomeActiveNotification
7.2 Permission Monitoring Pattern
// Monitor for permission changes when app becomes active
// (after user may have toggled a permission in System Settings)
actor PermissionMonitor {
private var observers: [NSObjectProtocol] = []
func startMonitoring(onChange: @Sendable @escaping () async -> Void) {
let observer = NotificationCenter.default.addObserver(
forName: NSApplication.didBecomeActiveNotification,
object: nil,
queue: nil
) { _ in
Task { await onChange() }
}
observers.append(observer)
}
func stopMonitoring() {
observers.forEach { NotificationCenter.default.removeObserver($0) }
observers.removeAll()
}
}
7.3 UserDefaults Keys for Onboarding State
extension UserDefaults {
// Onboarding completion
var hasCompletedOnboarding: Bool {
get { bool(forKey: "hasCompletedOnboarding") }
set { set(newValue, forKey: "hasCompletedOnboarding") }
}
// Version tracking for "What's New" screens
var lastSeenAppVersion: String {
get { string(forKey: "lastSeenAppVersion") ?? "" }
set { set(newValue, forKey: "lastSeenAppVersion") }
}
// Track which permissions were deferred (to re-prompt contextually later)
var deferredPermissions: Set<String> {
get { Set(stringArray(forKey: "deferredPermissions") ?? []) }
set { set(Array(newValue), forKey: "deferredPermissions") }
}
}
7.4 WWDC Sessions to Watch
| Session | Year | Topic |
|---|---|---|
| Love at First Launch (816) | WWDC17 | Onboarding and first-launch design — foundational |
| What's new in privacy (10096) | WWDC22 | Permission best practices |
| What's new in privacy (10053) | WWDC23 | Privacy manifest, new permission model |
| Discover Calendar and EventKit (10052) | WWDC23 | New EKEventStore permission model |
| Get started with privacy manifests (10060) | WWDC23 | PrivacyInfo.xcprivacy deep dive |
| What's new in privacy (10123) | WWDC24 | Latest privacy requirements |
| Integrate privacy into your development process (246) | WWDC25 | Current-year privacy guidance |
| Write clear purpose strings (Tech Talk 110152) | — | Purpose string authoring |
Sources
- Apple HIG — Onboarding
- Apple HIG — Privacy / Requesting Permissions
- Apple HIG — Launching
- Apple HIG — Onboarding for Games
- WWDC17 — Love at First Launch (816)
- WWDC23 — What's new in privacy
- WWDC23 — Discover Calendar and EventKit
- WWDC23 — Get started with privacy manifests
- WWDC24 — What's new in privacy
- WWDC25 — Integrate privacy into your development process
- Tech Talk — Write clear purpose strings
- TN3153 — Adopting API changes for EventKit in iOS 17, macOS 14
- Privacy manifest files — Apple Developer Documentation
- NSPrivacyAccessedAPITypes — Apple Developer Documentation
- Describing use of required reason API
- Protected resources — Apple Developer Documentation
- CGPreflightScreenCaptureAccess
- CGRequestScreenCaptureAccess
- AXIsProcessTrustedWithOptions
- EKEventStore.requestFullAccessToEvents
- EKEventStore.requestWriteOnlyAccessToEvents
- CNContactStore.requestAccess
- Requesting Authorization for Media Capture on macOS
- NSCalendarsWriteOnlyAccessUsageDescription
- App Sandbox Entitlements
- Notarizing macOS Software
- Apple System Preferences URL Schemes (community gist)
- Accessibility Permission in macOS (jano.dev, Jan 2025)
- PermissionFlow library (jaywcjlove)
- User Privacy and Data Use — App Store
8. Model Setup & Local File Verification Guide
This section outlines the setup procedures, hardware requirements, VLM configurations, and exact file verification checks for Pheron Agent local model execution.
8.1 mlx-community — Apple Silicon Model Catalog
Pheron Agent downloads pre-quantized 4-bit models directly from the mlx-community organization on Hugging Face. The following tables contain the verified model catalogs:
Qwen3 Dense
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Qwen3-0.6B-4bit | mlx-community/Qwen3-0.6B-4bit |
0.35 GB | 1 GB | ✓ | ✓ |
| Qwen3-1.7B-4bit | mlx-community/Qwen3-1.7B-4bit |
0.97 GB | 2 GB | ✓ | ✓ |
| Qwen3-4B-4bit | mlx-community/Qwen3-4B-Instruct-2507-4bit |
2.28 GB | 4 GB | ✓ | ✓ |
| Qwen3-8B-4bit | mlx-community/Qwen3-8B-4bit |
4.62 GB | 6 GB | ✓ | ✓ |
| Qwen3-14B-4bit | mlx-community/Qwen3-14B-4bit |
8.32 GB | 10 GB | ✓ | ✓ |
| Qwen3-32B-4bit | mlx-community/Qwen3-32B-4bit |
18.4 GB | 22 GB | ✓ | ✓ |
Qwen3 MoE
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Qwen3-30B-A3B-4bit | mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit |
17.2 GB | 21 GB | ✓ | ✓ |
| Qwen3-Coder-30B-A3B-4bit | mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit |
17.2 GB | 21 GB | ✓ | — |
| Qwen3-235B-A22B-4bit | mlx-community/Qwen3-235B-A22B-4bit |
132 GB | 158 GB | ✓ | ✓ |
| Qwen3-Coder-480B-A35B-4bit | mlx-community/Qwen3-Coder-480B-A35B-Instruct-4bit |
270 GB | 324 GB | ✓ | — |
Llama
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Llama-3.2-1B-4bit | mlx-community/Llama-3.2-1B-Instruct-4bit |
0.71 GB | 1 GB | ✓ | — |
| Llama-3.2-3B-4bit | mlx-community/Llama-3.2-3B-Instruct-4bit |
1.82 GB | 3 GB | ✓ | — |
| Llama-3.1-8B-4bit | mlx-community/Meta-Llama-3.1-8B-Instruct-4bit |
4.53 GB | 6 GB | ✓ | — |
| Llama-3.3-70B-4bit | mlx-community/Llama-3.3-70B-Instruct-4bit |
39.7 GB | 48 GB | ✓ | — |
| Llama-4-Scout-4bit | mlx-community/Llama-4-Scout-17B-16E-Instruct-4bit |
61.1 GB | 73 GB | ✓ | — |
| Llama-4-Maverick-4bit | mlx-community/Llama-4-Maverick-17B-128E-Instruct-4bit |
226 GB | 271 GB | ✓ | — |
Gemma
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Gemma-3-1B-4bit | mlx-community/gemma-3-1b-it-4bit |
0.77 GB | 1 GB | Kısmi | — |
| Gemma-3-4B-4bit | mlx-community/gemma-3-4b-it-4bit |
3.44 GB | 5 GB | Kısmi | — |
| Gemma-3-12B-4bit | mlx-community/gemma-3-12b-it-4bit |
8.07 GB | 10 GB | Kısmi | — |
| Gemma-3-27B-4bit | mlx-community/gemma-3-27b-it-4bit |
16.9 GB | 20 GB | Kısmi | — |
| Gemma-4-E4B-4bit | mlx-community/gemma-4-e4b-it-4bit |
5.25 GB | 7 GB | ✓ | ✓ |
| Gemma-4-26B-MoE-4bit | mlx-community/gemma-4-26b-a4b-it-4bit |
15.6 GB | 19 GB | ✓ | ✓ |
Mistral
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Mistral-7B-v0.3-4bit | mlx-community/Mistral-7B-Instruct-v0.3-4bit |
4.08 GB | 5 GB | ✓ | — |
| Mistral-Nemo-12B-4bit | mlx-community/Mistral-Nemo-Instruct-2407-4bit |
6.91 GB | 9 GB | Kısmi | — |
| Mistral-Small-24B-4bit | mlx-community/Mistral-Small-24B-Instruct-2501-4bit |
13.3 GB | 16 GB | ✓ | — |
| Mistral-Small-3.2-24B-4bit | mlx-community/Mistral-Small-3.2-24B-Instruct-2506-4bit |
13.3 GB | 16 GB | ✓ | — |
| Mistral-Large-123B-4bit | mlx-community/Mistral-Large-Instruct-2407-4bit |
69 GB | 83 GB | ✓ | — |
Phi & DeepSeek
| Model | HF Repo | Disk Size | Min. RAM | Tool Calling | Thinking Mode |
|---|---|---|---|---|---|
| Phi-4-mini-4bit | mlx-community/Phi-4-mini-instruct-4bit |
2.18 GB | 3 GB | ✓ | — |
| Phi-4-14B-4bit | mlx-community/phi-4-4bit |
8.25 GB | 10 GB | Kısmi | — |
| DeepSeek-Coder-V2-Lite-4bit | mlx-community/DeepSeek-Coder-V2-Lite-Instruct-4bit-mlx |
8.84 GB | 11 GB | Kısmi | — |
8.2 Apple Silicon RAM Allocation Profiles
Pheron Agent enforces a strict 65% model weight allocation budget from the device's unified memory, reserving the rest for system processes and the KV cache. The following table maps RAM tiers to recommended working models:
- 8 GB RAM: Qwen3-0.6B, Qwen3-1.7B, Qwen3-4B, Qwen3-8B · Llama-3.2-1B, Llama-3.2-3B, Llama-3.1-8B · Gemma-3-1B, Gemma-3-4B, Gemma-4-E4B · Mistral-7B · Phi-4-mini.
- 16 GB RAM: All 8 GB models plus Qwen3-14B · Gemma-3-12B · Mistral-Nemo-12B, Mistral-Small-24B · Phi-4-14B · DeepSeek-Coder-V2-Lite.
- 24 GB RAM: All 16 GB models plus Qwen3-32B, Qwen3-30B-A3B · Gemma-3-27B, Gemma-4-26B · Mistral-Small-3.2-24B.
- 32 GB RAM: All 24 GB models plus Qwen3-Coder-30B.
- 48 GB RAM: All 32 GB models plus Llama-3.3-70B.
- 64 GB RAM: All 48 GB models (Note: Qwen3-72B was not released as dense, so it is absent).
- 128 GB RAM: All 64 GB models plus Mistral-Large-123B · Llama-4-Scout.
- 192 GB RAM: All 128 GB models plus Qwen3-235B-A22B.
- 512 GB RAM: All 192 GB models plus Qwen3-Coder-480B · Llama-4-Maverick.
8.3 VLM (Vision Language Model) System Integration
Pheron Agent's VLMInferenceActor.swift coordinates multimodal image-understanding capabilities (leveraged by the semantic_vision tool / UBID 84):
- Hardware Memory Threshold: Requires a minimum of 24 GB unified memory to run the VLM engine concurrently alongside the primary text orchestrator.
- Graceful Fallback: If RAM is less than 24 GB, VLM stays disabled and calls dynamically fall back to local Optical Character Recognition (OCR), warning the user without crashes.
- Automatic Model Selection:
- RAM 24–31 GB: Automatically resolves to
mlx-community/Qwen2.5-VL-3B-Instruct-4bit(~2.0 GB disk size). - RAM 32 GB+: Automatically resolves to
mlx-community/Qwen3-VL-4B-Instruct-MLX-4bit(~2.5 GB disk size).
- RAM 24–31 GB: Automatically resolves to
- User Override Key: You can override VLM checks manually using:
Note: Manual activation on low-RAM profiles may trigger VRAM bottlenecks.UserDefaults.standard.set(true, forKey: "vlmEnabled") - Catalog Selection Gaps: The current Swift catalog does not recommend larger models like
Qwen2.5-VL-7B,Qwen2.5-VL-72B, orLlama-4-Scoutfor memory configurations over 48 GB+. To select them, future updates to the hardcodedrecommendedModelID()function are required.
8.4 Local Model Execution File Manifest
When troubleshooting local models or verifying offline setups, check that the corresponding directory contains all required files.
Local Storage Path
Models must reside in:
~/Library/Application Support/PheronAgent/Models/[model-id]/
(Where [model-id] matches the identifier registered in the catalog, e.g. qwen3.5-9b-4bit)
Download Source URL
Files are fetched from the respective Hugging Face repo via:
https://huggingface.co/[HF_Repo]/resolve/main/[file_name]
Required File Checklist
Verify that all of the following are fully downloaded and not corrupted (HTML error pages instead of files):
[ ]config.json(Required)- Contains neural dimensions, head counts, layers, and model architecture type.
[ ]tokenizer.json(Required)- Contains vocabulary maps and merge lists.
[ ]tokenizer_config.json(Required)- Defines prompt formatting and chat templates.
[ ]generation_config.json(Highly Recommended)- Details generation bounds and stop tokens (critical for Qwen3.5/MLX v3 to prevent runaway output loops).
[ ]special_tokens_map.json(Recommended)- Maps control tags (e.g.
<|im_start|>,<|im_end|>).
- Maps control tags (e.g.
[ ]preprocessor_config.json(VLM/Vision Only)- Absolutely required for multimodal VLM layers (
Qwen2.5-VL-3BandQwen3-VL-4B) to initialize vision grids.
- Absolutely required for multimodal VLM layers (
[ ]Weights (.safetensorsfiles) (Required)- Single-file weights: Used by light models (<4B parameters). Requires:
model.safetensors
- Sharded weights: Large models split parameters into multiple files named
model-XXXXX-of-YYYYY.safetensors.- Crucial: All shards from
00001throughYYYYYmust be present. If even one shard is missing or incomplete, VRAM loading fails and the engine rolls back. - Example (Qwen3-14B): Requires
model-00001-of-00002.safetensorsandmodel-00002-of-00002.safetensors. - Example (Llama-3.3-70B): Requires shards
model-00001-of-00008.safetensorsthroughmodel-00008-of-00008.safetensors.
- Crucial: All shards from
- Single-file weights: Used by light models (<4B parameters). Requires: