Accessibility Permission in macOS
macOS Security Overview
Apple provides multiple security mechanisms in macOS that rely on each other.
App Execution Security
- App Sandbox: isolates an app’s activities to limit potential damage.
- Gatekeeper: ensures only trusted apps can run.
- Notarization & Developer ID: verifies software integrity and developer legitimacy.
Gatekeeper
- Checking for a valid Developer ID signature from Apple, so that malicious developers can be revoked if necessary.
- Requiring apps to pass Apple’s notarization process, which scans software for suspicious content and security vulnerabilities.
Notarization & Developer ID
- Developer ID: A unique identifier Apple provides to trusted developers, helping users recognize legitimate software sources.
- Notarization: A process where Apple’s automated systems scan and approve apps, identifying malicious code or security flaws before they reach your Mac.
App Sandbox
- Prevents an app from freely reading or modifying files outside its container.
- Limits network access and system-level changes to minimize damage if the app is compromised.
System-Level Security
- Secure Enclave: provides hardware-level cryptographic security.
- SIP (System Integrity Protection): protects critical system files.
- XProtect: built-in anti-malware that scans for known threats.
SIP (System Integrity Protection)
- Restricts the root user from altering protected files and folders.
- Ensures only Apple-signed processes can make system-level changes.
XProtect
- Uses Apple-updated malware definitions to detect known threats.
- Operates behind the scenes, automatically blocking harmful software.
Secure Enclave
- Manages sensitive data like biometric information (e.g., Touch ID).
- Performs cryptographic operations in a secure, hardware-isolated environment.
Data & Privacy Protection
- FileVault: provides full-disk encryption to protect your files.
- Keychain: stores passwords and credentials securely.
- TCC (Transparency, Consent, & Control): manages permissions for sensitive data.
TCC (Transparency, Consent, & Control)
- Prompts for consent when an app wants to access things like your camera, microphone, or contacts.
- Allows you to review and adjust permissions in System Settings at any time.
FileVault
- Uses XTS-AES-128 encryption to secure the entire startup volume.
- Helps prevent unauthorized access if your Mac is lost or stolen.
Keychain
- Encrypts your passwords, certificates, and private keys.
- Relies on the system’s security frameworks to prevent unauthorized access.
The Accessibility Permission
Accessibility permission falls under the TCC framework. It opens your system to programmatic control for certain purposes.
Accessibility Examples
- Automation utilities
- Window management tools
- Voice control applications
- Screen readers for visually impaired users
- Simulated keyboard and mouse input
- Monitor user interactions with other applications
- Access and manipulate content from windows and controls across applications
Applications with accessibility permission are also referred to as trusted. macOS requires explicit user permission to designate an app as trusted. This takes the form of a dialog that requires admin authentication, and a Settings panel where permissions can be revoked. Spoiler alert: there is no clean way around manual authentication.
Configuration
To be able to request accessibility permissions you need to
- Declare an entitlement if you run on a hardened runtime.
- Declare the reason you need accessibility in your Info.plist.
- Include a provisioning profile if you plan to distribute in the App Store.
In the entitlements file:
<key>com.apple.security.accessibility</key>
<true/>
In the Info.plist:
<key>NSAccessibilityUsageDescription</key>
<string>Accessibility permissions are used for window management.</string>
Request Accessibility
To find out if the application has permissions:
// Returns TRUE if the current process is a trusted accessibility client
let isGranted = AXIsProcessTrusted()
To prompt the user for permissions:
let options: NSDictionary = [
kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true
]
AXIsProcessTrustedWithOptions(options)
The code above will either present a system dialog, or redirect the user to the Accessibility panel at Settings > Privacy & Security > Accessibility. Such panel can also be open programmatically with:
if let url = URL(string:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
) {
NSWorkspace.shared.open(url)
}
Grant accessibility
A workflow that is reliable is to
- Build the application
- Remove and add back the application to Accessibility.
- Run the application from Xcode > Product > Perform Action > Run Without Building
You will be able to debug the application, but it is of course, very inconvenient to go through all this after each build.
TCC provides a utility (tccutil) that can reset permissions, but not assign them. The TCC database is a SQLite file on disk you can read, but writing requires disabling SIP, which opens the possibility to make a catastrophic mistake that damages your system.
Disabling SIP
- Turn on the Mac and press ⌘R until you see the Apple logo or the recovery utilities.
- Go to the Utilities menu at the top and select Terminal.
- Type:
csrutil disable
- Restart the Mac.
crsutil status
to verify it is disabled. To re-enable SIP repeat the steps above but run
csrutil enable
.
TCC.db
# clears accessibility for one app
sudo tccutil reset Accessibility $PRODUCT_BUNDLE_IDENTIFIER
# clears accessibility for all apps
tccutil reset Accessibility
TCC.db
# is my app authorized? 1=granted, 2=denied
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT auth_value FROM access WHERE service='kTCCServiceAccessibility' AND client='dev.jano.orange';"
# read row
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT * FROM access WHERE service='kTCCServiceAccessibility' AND client='dev.jano.orange';"
# write (if SIP is disabled)
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "UPDATE access SET auth_value = 1 WHERE service = 'kTCCServiceAccessibility' AND client = 'dev.jano.orange';"
# list clients allowed in my machine
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT client FROM access WHERE service='kTCCServiceAccessibility';"
# show all services tracked there
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT DISTINCT service FROM access;"
The service
, client
, and auth_value
are well known. The others are not documented, but more or less this is the unofficial table structure
Column | Example Value | Description |
---|---|---|
service | kTCCServiceAccessibility | The type of permission |
client | dev.jano.orange | The bundle identifier |
client_type | 0 | 0 typically means bundled app |
auth_value | 2 | 2 usually indicates denied, 1 is allowed |
auth_reason | 4 | Reason code for the authorization |
auth_version | 1 | Version of the authorization |
csreq | ?? | Code signing requirement |
policy_id | null | Policy identifier |
indirect_object_identifier | 0 | Used for file/folder access permissions |
flags | UNUSED | Flag settings |
last_modified | null | Last modification timestamp |
pid | 0 | Process ID |
expired_at | 1736658020 | Unix timestamp when permission expires |
remote_pid | null | Remote process ID |
responsibility_id | UNUSED | Responsibility identifier |
temporary_grant | 0 | Whether this is a temporary permission |
There is an utility in GitHub: tccprofile but didn’t look into it. There is another local database at ~/Library/Application\ Support/com.apple.TCC/TCC.db
but it is not relevant.
Recommendations
(aka things to do to avoid problems)
Sign the application to make its identity clear to macOS. What I read online is that each build changes the app signature so permissions have to be requested again. But what I experienced is that signing with a Developer ID certificate is enough for the system to recognize it is the same app.
To sign your app
- Go to Signing & Capabilities
- Set a Team
- There are several options here. I use a Developer ID certificate and a Developer ID profile tied to it.
- Once signed, you can check the state of your app with codesign.
codesign
Verifying the application is signed.
% codesign -d -vv OrangeApp.app
Executable=Foo/Build/Debug/OrangeApp.app/Contents/MacOS/OrangeApp
Identifier=dev.jano.orangeapp
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20400 size=650 hashes=10+6 location=embedded
Signature size=4691
Authority=Apple Development: Ellie Williams (H6L34F7G12) 👈
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Signed Time=Jan 12, 2025 at 09:10:19 👈
Info.plist entries=28
TeamIdentifier=PPSA6C3P8Q 👈
Sealed Resources version=2 rules=13 files=2
Internal requirements count=1 size=183
Copy the product to a stable location. What I read is that the default location of ~/Library/Developer/Xcode/DerivedData/
is hidden and changes between builds, which may confuse the system. But what I experienced is that the system recognizes the application regardless where it is generated.
FYI: changing the destination folder may cause problems if one of your dependencies declares a localization bundle of its own. If this is your case, you can still work around it and change the destination folder.
There are three ways to declare a custom location:
- Set a build property.
- Xcode > project name > Build Settings > Build Location > Per-configuration Build Products Path.
- Product > Scheme > Edit Scheme… > Run > Build Configuration > Debug > Options > Build Location > Custom
Build property
This will place the product in Build/Debug/YourApp.app
CONFIGURATION_BUILD_DIR =
$(PROJECT_DIR)/Build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
Troubleshooting
The Console.app is your friend –despite its unfriendly UI. You can spot the Accesibility permission being denied if you paste the following in the search field (top, right):
type:error
subsystem:com.apple.sandbox.reporting
category:violation
The error will say something like
Sandbox: OrangeApp(16745) deny(1) mach-lookup com.apple.universalaccessAuthWarn
Another way to keep an eye on the console is to stream from the terminal:
log stream --predicate \
'process == "tccd" OR process == "OrangeApp" OR process="sandboxd"'