Accessibility permission in sandboxed app

Is it possible to create a sandboxed app that uses accessibility permission? And if so, how do I ask the user for that permission in a way that is allowed by the App Store?

Im creating a small menubar app and my current (rejected) solution is to create a pop-up, with link to Security & Privacy > Accessibility and the pop-up asks the user to manually add the app to the list and check the checkbox. This works in sandbox.

Reason for rejection:

"Specifically, your app requires to grant accessibility access, but once we opened the accessibility settings, your app was not listed."

I know it's not listed there and it has to be added manually. But its the only solution I've found to this issue. Is there perhaps any way to add the app there programmatically?

Im a bit confused since I've seen other apps in App Store that work the same way, where you have to add the app to the list manually. Eg. Flycut. 🤷‍♂️

I know about this alternative solution, and it's not allowed in sandboxed apps. It also adds the app to the accessibility list automagically:

func getPermission() {
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue():true] as CFDictionary). 
}

Does anyone have a solution for this?

Best regards, Daniel

Accepted Reply

Is there any way to allow this in a sandboxed environment?

Yes, but you have to take a slightly different tack. Rather than using an NSEvent global event monitor, use a CGEventTap. For weird historical reasons, the former requires the Accessibility privilege whereas the latter requires the Input Monitoring privilege. The Input Monitoring privilege is easily available to sandboxed apps, and even apps published on the Mac App Store. It even has APIs to check for (CGPreflightListenEventAccess) and explicitly request (CGRequestListenEventAccess) that privilege.

CGEventTap is not exactly fun to call from Swift. Pasted in below is a snippet that shows the basics. I pulled this out of a large test project, and removed some code that shouldn’t matter to you, so I apologise if this doesn’t compile out of the box.

Share and Enjoy

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

final class CGEventTapAction {

    init(log: OSLog) {
        self.log = log
    }
    
    let log: OSLog

    private var runState: RunState? = nil

    private struct RunState {
        let port: CFMachPort
        let setStatus: (String) -> Void
    }
    
    func start(_ setStatus: @escaping (String) -> Void) {
        precondition(self.runState == nil)
        
        os_log(.debug, log: self.log, "will create tap")
        let info = Unmanaged.passRetained(self).toOpaque()
        let mask = CGEventMask(1 << CGEventType.keyDown.rawValue)
        guard let port = CGEvent.tapCreate(
            tap: .cgSessionEventTap,
            place: .headInsertEventTap,
            options: .listenOnly,
            eventsOfInterest: mask,
            callback: { (proxy, type, event, info) -> Unmanaged<CGEvent>? in
                let obj = Unmanaged<CGEventTapAction>.fromOpaque(info!).takeUnretainedValue()
                obj.didReceiveEvent(event)
                // We don’t replace the event, so the new event is the same as
                // the old event, so we return it unretained.
                return Unmanaged.passUnretained(event)
            },
            userInfo: info
        ) else {
            os_log(.debug, log: self.log, "did not create tap")
            // We retained `self` above, but the event tap didn’t get created so
            // we need to clean up.
            Unmanaged<CGEventTapAction>.fromOpaque(info).release()
            setStatus("Failed to create event tap.")
            return
        }
        let rls = CFMachPortCreateRunLoopSource(nil, port, 0)!
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, .defaultMode)
        self.runState = RunState(port: port, setStatus: setStatus)
        os_log(.debug, log: self.log, "did create tap")
    }
    
    private func didReceiveEvent(_ event: CGEvent) {
        os_log(.debug, log: self.log, "did receive event")
        guard let runState = self.runState else { return }
        runState.setStatus("Last event at \(Date()).")
    }

    func stop() {
        guard let runState = self.runState else { return }
        self.runState = nil

        os_log(.debug, log: self.log, "will stop tap")
        CFMachPortInvalidate(runState.port)
        // We passed a retained copy of `self` to the `info` parameter
        // when we created the tap.  We need to release that now that we’ve
        // invalidated the tap.
        Unmanaged.passUnretained(self).release()
        os_log(.debug, log: self.log, "did stop tap")
    }
}
  • Thank you so much for your help! You were totally right, it sure wasn't "fun" to call CGEventTap from swift ;) But I managed to solve it thanks to you!

    I have a follow-up question regarding accessibility permission that I think might help more people since it seems a bit quirky: Programmatically press "delete" or "cmd + v" in sandboxed app

    I would appreciate it a lot if you could take a look!

Add a Comment

Replies

Why specifically do you need the Accessibility privilege? That is, what data are you getting, or actions are you doing, that you can’t do without it?

Share and Enjoy

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

Thank you for your reply! Im creating a small lorem ipsum text generator app that requires an observer for keyDown events. And this requires accessibility permission. So the data Im getting is essentially text from keyboard. The action I do in the app is to match the input text with regexp in order to trigger commands.

NSEvent.addGlobalMonitorForEvents(matching: .keyDown) {(event) in guard let characters = event.characters else{return}

In case you want a further explanation I’ve described the app properly over here: https://danieldanielsson.se/#/lorema

Is there any way to allow this in a sandboxed environment?

Is there any way to allow this in a sandboxed environment?

Yes, but you have to take a slightly different tack. Rather than using an NSEvent global event monitor, use a CGEventTap. For weird historical reasons, the former requires the Accessibility privilege whereas the latter requires the Input Monitoring privilege. The Input Monitoring privilege is easily available to sandboxed apps, and even apps published on the Mac App Store. It even has APIs to check for (CGPreflightListenEventAccess) and explicitly request (CGRequestListenEventAccess) that privilege.

CGEventTap is not exactly fun to call from Swift. Pasted in below is a snippet that shows the basics. I pulled this out of a large test project, and removed some code that shouldn’t matter to you, so I apologise if this doesn’t compile out of the box.

Share and Enjoy

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

final class CGEventTapAction {

    init(log: OSLog) {
        self.log = log
    }
    
    let log: OSLog

    private var runState: RunState? = nil

    private struct RunState {
        let port: CFMachPort
        let setStatus: (String) -> Void
    }
    
    func start(_ setStatus: @escaping (String) -> Void) {
        precondition(self.runState == nil)
        
        os_log(.debug, log: self.log, "will create tap")
        let info = Unmanaged.passRetained(self).toOpaque()
        let mask = CGEventMask(1 << CGEventType.keyDown.rawValue)
        guard let port = CGEvent.tapCreate(
            tap: .cgSessionEventTap,
            place: .headInsertEventTap,
            options: .listenOnly,
            eventsOfInterest: mask,
            callback: { (proxy, type, event, info) -> Unmanaged<CGEvent>? in
                let obj = Unmanaged<CGEventTapAction>.fromOpaque(info!).takeUnretainedValue()
                obj.didReceiveEvent(event)
                // We don’t replace the event, so the new event is the same as
                // the old event, so we return it unretained.
                return Unmanaged.passUnretained(event)
            },
            userInfo: info
        ) else {
            os_log(.debug, log: self.log, "did not create tap")
            // We retained `self` above, but the event tap didn’t get created so
            // we need to clean up.
            Unmanaged<CGEventTapAction>.fromOpaque(info).release()
            setStatus("Failed to create event tap.")
            return
        }
        let rls = CFMachPortCreateRunLoopSource(nil, port, 0)!
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, .defaultMode)
        self.runState = RunState(port: port, setStatus: setStatus)
        os_log(.debug, log: self.log, "did create tap")
    }
    
    private func didReceiveEvent(_ event: CGEvent) {
        os_log(.debug, log: self.log, "did receive event")
        guard let runState = self.runState else { return }
        runState.setStatus("Last event at \(Date()).")
    }

    func stop() {
        guard let runState = self.runState else { return }
        self.runState = nil

        os_log(.debug, log: self.log, "will stop tap")
        CFMachPortInvalidate(runState.port)
        // We passed a retained copy of `self` to the `info` parameter
        // when we created the tap.  We need to release that now that we’ve
        // invalidated the tap.
        Unmanaged.passUnretained(self).release()
        os_log(.debug, log: self.log, "did stop tap")
    }
}
  • Thank you so much for your help! You were totally right, it sure wasn't "fun" to call CGEventTap from swift ;) But I managed to solve it thanks to you!

    I have a follow-up question regarding accessibility permission that I think might help more people since it seems a bit quirky: Programmatically press "delete" or "cmd + v" in sandboxed app

    I would appreciate it a lot if you could take a look!

Add a Comment