Why does NSEvent.addGlobalMonitorForEvents still work in a Sandboxed macOS app

I am building a macOS utility using SwiftUI and Swift that records and displays keyboard shortcuts (like Cmd+C, Cmd+V) in the UI. To achieve this, I am using NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]).

I am aware that global monitoring usually requires the app to be non-sandboxed. However, I am seeing some behavior I don't quite understand during development:

  1. I started with a fresh SwiftUI project and disabled the App Sandbox.

  2. I requested Accessibility permissions using AXIsProcessTrustedWithOptions, manually enabled it in System Settings, and the global monitor worked perfectly.

  3. I then re-enabled the App Sandbox in "Signing & Capabilities."

  4. To my surprise, the app still records global events from other applications, even though the Sandbox is now active.

Is this expected behavior? Does macOS "remember" the trust because the Bundle ID was previously authorized while non-sandboxed, or is there a specific reason a Sandboxed app can still use addGlobalMonitor if the user has manually granted Accessibility access?

My app's core feature is displaying these shortcuts for the user's own reference (productivity tracking). If the user is the one explicitly granting permission via the Accessibility privacy pane, will Apple still reject the app for using global event monitors within a Sandboxed environment?

Code snippet of my monitor:

// This is still firing even after re-enabling Sandbox

eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { event in

    print("Captured: \(event.charactersIgnoringModifiers ?? "")")

}

I've tried cleaning the build folder and restarting the app, removing the app from accessibility permission, but the events keep coming through. I want to make sure I'm not relying on a "development glitch" before I commit to the App Store path.

Here is the full code anyone can use to try this :-


import SwiftUI



import Cocoa

import Combine



struct ShortcutEvent: Identifiable {

    let id = UUID()

    let displayString: String

    let timestamp: Date

}



class KeyboardManager: ObservableObject {

    @Published var isCapturing = false

    @Published var capturedShortcuts: [ShortcutEvent] = []

    private var eventMonitor: Any?



    // 1. Check & Request Permissions

    func checkAccessibilityPermissions() -> Bool {

        let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]

        let accessEnabled = AXIsProcessTrustedWithOptions(options)

        return accessEnabled

    }



    // 2. Start Capture

    func startCapture() {

        guard checkAccessibilityPermissions() else {

            print("Permission denied")

            return

        }

        

        isCapturing = true

        let mask: NSEvent.EventTypeMask = [.keyDown, .keyUp]

        

        eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { [weak self] event in

            self?.processEvent(event)

        }

    }



    // 3. Stop Capture

    func stopCapture() {

        if let monitor = eventMonitor {

            NSEvent.removeMonitor(monitor)

            eventMonitor = nil

        }

        isCapturing = false

    }



    private func processEvent(_ event: NSEvent) {

            // Only log keyDown to avoid double-counting the UI display

            guard event.type == .keyDown else { return }



            var modifiers: [String] = []

            var symbols: [String] = []



            // Map symbols for the UI

            if event.modifierFlags.contains(.command) {

                modifiers.append("command")

                symbols.append("⌘")

            }

            if event.modifierFlags.contains(.shift) {

                modifiers.append("shift")

                symbols.append("⇧")

            }

            if event.modifierFlags.contains(.option) {

                modifiers.append("option")

                symbols.append("⌥")

            }

            if event.modifierFlags.contains(.control) {

                modifiers.append("control")

                symbols.append("⌃")

            }



            let key = event.charactersIgnoringModifiers?.uppercased() ?? ""

            

            // Only display if a modifier is active (to capture "shortcuts" vs regular typing)

            if !symbols.isEmpty && !key.isEmpty {

                let shortcutString = "\(symbols.joined(separator: " ")) + \(key)"

                

                DispatchQueue.main.async {

                    // Insert at the top so the newest shortcut is visible

                    self.capturedShortcuts.insert(ShortcutEvent(displayString: shortcutString, timestamp: Date()), at: 0)

                }

            }

        }

}

PS :- I just did another test by creating a fresh new project with the default App Sandbox enabled, and tried and there also it worked!!

Can I consider this a go to for MacOs app store than?

Answered by DTS Engineer in 871186022

There’s a lot to unpack here but I’m still grinding through the holiday backlog so I’m going to focus on the critical bits.

First, don’t test stuff like this on your day-to-day development machine, because cached state can confuse you. Rather, run tests like this on a fresh Mac, one that’s never seen your app before. I generally do this in a VM, where I can restore to a fresh snapshot between each test.

Second, there is a supported way for a sandboxed app to achieve this goal on modern systems [1], namely, via the CGEventTap mechanism. See CGPreflightListenEventAccess, CGRequestListenEventAccess, and CGEventTapCreate in <CoreGraphics/CGEvent.h>. This relies on the Input Monitoring privilege, rather than the Accessibility privilege, and that’s available to sandboxed apps.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] macOS 10.15 and later, IIRC.

There’s a lot to unpack here but I’m still grinding through the holiday backlog so I’m going to focus on the critical bits.

First, don’t test stuff like this on your day-to-day development machine, because cached state can confuse you. Rather, run tests like this on a fresh Mac, one that’s never seen your app before. I generally do this in a VM, where I can restore to a fresh snapshot between each test.

Second, there is a supported way for a sandboxed app to achieve this goal on modern systems [1], namely, via the CGEventTap mechanism. See CGPreflightListenEventAccess, CGRequestListenEventAccess, and CGEventTapCreate in <CoreGraphics/CGEvent.h>. This relies on the Input Monitoring privilege, rather than the Accessibility privilege, and that’s available to sandboxed apps.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] macOS 10.15 and later, IIRC.

Why does NSEvent.addGlobalMonitorForEvents still work in a Sandboxed macOS app
 
 
Q