Camera, Calendar, Reminders and Apple Events: the four permissions a notch app asks for

What each one is for, when the prompt appears, what the app does when the answer is no, and the ones it never asks for

By Arin Prajapati 9 min read
White headline on black reading The four permissions a notch app asks for, with the kicker Swift and macOS and the NotchStar logo

TL;DR

  • NotchStar asks for four permissions: camera (the Mirror tool), Calendar and Reminders (the agenda), and Apple Events (to control Music and Spotify). Each has a usage string in project.yml, and three of them have an entitlement in NotchStar.entitlements.
  • No prompt appears at launch. Each one is requested when its tool is first used, or earlier if the person accepts it during the welcome tour. Every tool has a visible state for “no”.
  • It never asks for Accessibility, Screen Recording or notifications. A search of Sources/ on 4 November 2026 found no use of the symbols that would need them.

The problem: four prompts and none of them is optional to get right

A notch app lives in a corner, so an unexpected permission dialog is a bad surprise. It appears over something the person was doing, with a system sheet the app cannot restyle. Worse, a denied permission is not an error the app can retry: the system remembers the answer, the app gets nothing, and only System Settings can change it.

Four things have to be correct for each permission:

  1. The usage string in Info.plist, which macOS shows in the prompt.
  2. The entitlement, which the hardened runtime needs, in the signed app (the previous post covers how codesign --deep lost these).
  3. The moment of asking, so the dialog explains itself.
  4. The behavior when the answer is no.

This post goes through the real code, in project.yml, Tools/Calendar/CalendarTool.swift, Tools/Mirror/MirrorTool.swift, Tools/NowPlaying/NowPlayingTool.swift and Onboarding/OnboardingPermissions.swift. It does not include screenshots of the system dialogs. None were captured, and the dialog wording is the usage strings below.

What each permission is for

PermissionToolPlist keyUsage string (exact)Entitlement key
CameraMirrorNSCameraUsageDescriptionNotchStar shows a live mirror preview from your camera. Nothing is recorded.com.apple.security.device.camera
CalendarCalendar and RemindersNSCalendarsFullAccessUsageDescriptionNotchStar shows your upcoming events in the notch.com.apple.security.personal-information.calendars
RemindersCalendar and RemindersNSRemindersFullAccessUsageDescriptionNotchStar shows your due reminders in the notch.none listed
Apple EventsHome (Now Playing)NSAppleEventsUsageDescriptionNotchStar controls Music and Spotify playback from the notch.com.apple.security.automation.apple-events

The strings are the whole explanation the person gets in the prompt, so each one says what the tool does with the access. The camera string also says what it does not do. Reminders has a usage string but no separate key in the entitlements file; whether that is required for Reminders under the hardened runtime was not verified for this post.

The app targets macOS 14, per project.yml, and uses the full access calls (requestFullAccessToEvents, requestFullAccessToReminders). “Full access” is the only level the agenda needs, since it has to read events, and it marks a reminder complete.

Calendar and Reminders: ask once, read what is allowed

The agenda asks for two things in one function. The refresh method looks at the current status of each store before deciding anything:

let ev = EKEventStore.authorizationStatus(for: .event)
let rm = EKEventStore.authorizationStatus(for: .reminder)
guard ev == .fullAccess || ev == .notDetermined else { access = .denied; return }
let mayAsk = !UITesting.isOn && ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil
if ev == .notDetermined && mayAsk { _ = try? await store.requestFullAccessToEvents() }
if rm == .notDetermined && mayAsk { _ = try? await store.requestFullAccessToReminders() }
guard EKEventStore.authorizationStatus(for: .event) == .fullAccess else { access = .denied; return }

Reading it carefully shows the behavior:

  • The guard on the first line treats anything other than “full access” or “not asked yet” as denied. That includes write only access and restricted accounts.
  • Events and Reminders are asked in sequence, one after the other, so a first run can show two system dialogs, one for each.
  • A denied Calendar answer ends the function before Reminders is read, so reminders are never shown if events are denied, even when Reminders was allowed. The reverse is fine: events show and the reminder list stays empty. This follows from the code and was not exercised with a real denied account.
  • The date range is from the start of today for two days, and only events that have not ended are listed.

The mayAsk line is the wrong turn that taught the rule below.

The wrong turn: tests that ask the person a question

An app that asks for permission when a view appears also asks it when a test renders that view. A system prompt blocks a test run, and it asks the developer a question they never triggered. The comment above the mayAsk line says exactly that: “Tests (unit and UI) only read the current permission: a system prompt would block them and ask the user a question they did not trigger.”

The 1.0.4 release (11 October 2026) made it a rule for the whole app: the --ui-testing launch argument keeps the user’s license, note, clipboard and screenshots out of test runs, and tests never trigger permission prompts. The status is read and the request is skipped, so tests render the “granted” and “denied” states without ever calling the system. In the code the rule is two lines.

Camera: the preview that runs only while it is on screen

The Mirror view reads the status when it is created and acts on it in a .task:

switch status {
case .authorized: CameraPreview(session: camera.session) /* ... */
case .notDetermined: EmptyHint(systemImage: "camera", text: "Allow camera access to use the mirror")
default:
    PermissionCard(message: "NotchStar needs camera access for the mirror.") {
        NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera")!)
    }
}
// in .task
if status == .notDetermined {
    _ = await AVCaptureDevice.requestAccess(for: .video)
    status = AVCaptureDevice.authorizationStatus(for: .video)
}
if status == .authorized { camera.start() }

Three states, three views. Authorized shows the live preview. Not determined shows a plain hint while the system prompt is on screen. Anything else (denied or restricted) shows a card with one button that opens the Camera page of System Settings, through the x-apple.systempreferences URL. The app cannot grant itself access, so the honest thing to do is send the person to the place that can.

The capture session has a lifecycle tied to the view. CameraController carries this comment: it “runs only while the Mirror view exists, so the camera light is on exactly when the user is looking at themselves.” The view calls camera.stop() in onDisappear, and start() only runs after authorization. A camera that stays on in the background would be a worse failure than a missing prompt.

The website says the same in its privacy page: “The Mirror shows a live camera preview only while its tab is open. Nothing is recorded or saved.” The page and the code agree on the day this was written, and the Mirror was reported working on the owner’s own Mac on 7 October 2026. A real camera feed was never recorded for the site, which shows a still for that reason.

Apple Events: asked by the first script that runs

Apple Events is the odd one out because the prompt belongs to a target, not a framework. The app has no call that “requests” it. The prompt appears when the first Apple Event is sent to Music or Spotify, which the app does through NSAppleScript:

private static func run(_ source: String) -> NSAppleEventDescriptor? {
    var err: NSDictionary?
    return NSAppleScript(source: source)?.executeAndReturnError(&err)
}

Two things keep that from happening too early. The bridge reads the list of running applications first, and only scripts the ones that are already open (MusicBridge.current() has the doc comment “Queried only while the view is visible, and only for apps already running”). And per the project’s facts, scripting is the fallback: Now Playing normally reads the system through the open source MediaRemoteAdapter, and falls back to AppleScript for Music and Spotify if the stream is silent or exits. Playback controls (command(_:app:)) also send events.

There is no preflight for Apple Events in the code, and there is no status to show. Any error, a denial included, lands in the err variable, which is never read. So the only visible effect of a no is that the fallback returns nothing.

The welcome tour can ask early

The tour is optional. OnboardingPermissions offers the two bundles of access in one place, and the doc comment says: “every one can be skipped: each tool still asks for what it needs the first time it is opened.”

func requestCalendar() async {
    _ = try? await store.requestFullAccessToEvents()
    _ = try? await store.requestFullAccessToReminders()
    refresh()
}
func requestCamera() async {
    _ = await AVCaptureDevice.requestAccess(for: .video)
    refresh()
}

The tour deals with calendar and camera, and it reads denied and restricted into flags, so it can show a denied state. It does not ask for Apple Events.

What it does not ask for

Permissions that a notch app might be expected to want, and what the code shows:

PermissionEvidence it is not used
AccessibilityA code comment: global mouse monitors need no Accessibility permission, only key events do, and the controller installs mouse monitors only
Screen RecordingThe demo video recorder renders its own view in an off screen window, so it does not capture the screen (documented in docs/DEMO_MODE.md)
NotificationsNo UNUserNotification use in Sources/
Window and screen capture APIsNo CGWindowList or ScreenCaptureKit use in Sources/

The search is a grep, and the only hit was SMAppService in AppSettings.swift. That is the launch at login setting. It is not a privacy permission, but it has its own approval state (requiresApproval), and the settings screen checks it. It belongs on a list of things a user may be asked to approve.

One more place can show a system dialog without a plist key. The screenshot watcher reads the folder macOS saves screenshots into, which is often the Desktop, and the code comments record the cost: “open() can block while macOS asks for Desktop access, so it happens off the main thread,” and “Desktop can be an iCloud folder, and coordinating it on the main thread hangs launch.” Folder access prompts were another hazard to design around.

Resetting a permission while testing

To see a prompt again, reset the answer for the app’s bundle identifier. The tool is tccutil, and the service names for these four are Camera, Calendar, Reminders and AppleEvents:

tccutil reset Camera com.notchstar.app
tccutil reset Calendar com.notchstar.app
tccutil reset Reminders com.notchstar.app
tccutil reset AppleEvents com.notchstar.app

The commands above were checked for syntax against a bundle id that does not exist, so no real permission was reset. The bundle identifier com.notchstar.app comes from project.yml.

What is still bad

  • A denied Calendar answer hides Reminders too, even if Reminders was allowed. The code does not read them independently.
  • Apple Events has no visible state. A denial is silent, and the person gets no card that sends them to Settings, unlike the camera.
  • The dialog wording was not captured. The post lists the usage strings, and the system adds its own text around them that can change between macOS releases.
  • Behavior on a Mac that has never been asked, on a managed Mac that restricts camera or calendars, and after a reset was tested through status checks in unit tests and not on a fresh machine.
  • Whether Reminders needs its own entitlement key under the hardened runtime was not checked.

Try it yourself

  1. Add the usage strings you need to Info.plist before you write the code that asks. Run with a missing one once to see how it fails.
  2. Read the authorization status first, in a function that does not request anything. Drive your views from that value.
  3. Request at the moment of use, when the tool opens. Put a plain hint on screen while the system dialog is up.
  4. Give the denied state one button that opens the right System Settings pane.
  5. Add a test guard like mayAsk so no test run calls a request.
  6. Check the signed app for the entitlements with codesign -d --entitlements - YourApp.app.
  7. Reset with tccutil reset <Service> <bundle id> and run again from the top.

Cheat sheet

NeedPlist keyRequest callDenied state
CameraNSCameraUsageDescriptionAVCaptureDevice.requestAccess(for: .video)Card with a link to Privacy_Camera
Calendar eventsNSCalendarsFullAccessUsageDescriptionrequestFullAccessToEvents()access = .denied
RemindersNSRemindersFullAccessUsageDescriptionrequestFullAccessToReminders()list stays empty
Apple EventsNSAppleEventsUsageDescriptionnone, asked by the first scriptscript error, ignored
  • A usage string for every permission, saying what the tool does
  • An entitlement in the signed app for each one that needs it
  • No prompt at launch
  • A visible state for not determined and for denied
  • A button to the right System Settings pane
  • Tests that read status and never request
  • A capture session that stops when its view disappears

Close

NotchStar is the app these prompts belong to, and the privacy page lists what it reads. If you want to try it, it is at notchstar.com.

NotchStar is a native Mac notch app, $4 once, with a 7 day free trial. Try it, or read the ten tools.