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

Answered by DTS Engineer in 716892022

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")
    }
}

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?

Accepted Answer

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")
    }
}

I'm looking at this thread with interest, because I'm looking into implementing proper keyboard shortcuts that don't depend on Carbon APIs.
I think I will manage to get the code working, but I'm struggling with the CGRequestListenEventAccess step. I'm calling this method, but nothing is happening at all. The documentation for this function is absent. Could you give some pointers?

I did a reset of the permissions of my app, and that triggered a prompt: tccutil reset All com.mycompany.myapp

I'm going through your example code, and while you said it probably won't compile (which is fine), I'm struggling with the class CGEventTapAction you're using. I can't find in the documentation.

I can't find in the documentation.

CGEventTapAction is my class name, that is, it’s the name of the class that I’ve created to encapsulate this functionality. The relevant API is CGEvent.tapCreate(…), which you can find documented here.

Share and Enjoy

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

so how do we move windows around like desktop manager apps such as Magnet? https://magnet.crowdcafe.com/

They are in the mac app store, so how can they do this while sandboxed? I keep getting macOS AXError kAXErrorCannotComplete when I try to have my app manipulate other apps windows (I can successfully manage my own app windows)

I’m not able to reverse engineer other developer’s apps on your behalf, but I can offer some general advice.


You can tell whether an app is sandboxed using codesign. For example, BBEdit is sandboxed:

% codesign -d --entitlements - /Applications/BBEdit.app | grep -A 2 com.apple.security.app-sandbox
…
	[Key] com.apple.security.app-sandbox
	[Value]
		[Bool] true

but Xcode is not:

% codesign -d --entitlements - /Applications/Xcode.app | grep -A 2 com.apple.security.app-sandbox 
…
% 

Not all Mac App Store apps are sandboxed. Some apps shipped on the store before sandboxing was required. So, when you see an app that does something that’s seemingly impossible, it’s a good idea to check whether it’s actually sandboxed or not.

Share and Enjoy

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

Accessibility permission in sandboxed app
 
 
Q