| name | macos-accessibility |
| version | 2.0.0 |
| description | macOS accessibility automation with AXUIElement for UI testing, element inspection, and system control. Use when automating macOS UI via accessibility APIs or AXUIElement. Do NOT use for Linux accessibility (use linux-at-spi2). |
| compatibility | macOS 10.15+, Xcode Command Line Tools |
| risk_level | MEDIUM |
| token_budget | 3000 |
macOS Accessibility - Code Generation Rules
0. Anti-Hallucination Protocol
0.2 Security Patterns (security rules)
CWE-284: Accessibility Permission Abuse
- Do not: Request accessibility for non-accessibility purposes
- Instead: Minimal permissions, document why accessibility is needed
CWE-200: Screen Content Exposure
- Do not: Log or transmit screen content without consent
- Instead: Mask sensitive areas, user consent for any capture
1. Security Principles
1.1 TCC Permission Model (CWE-284)
Principle: macOS requires explicit Accessibility permission via TCC (Transparency, Consent, Control).
func automateUI() {
let app = AXUIElementCreateApplication(pid)
}
import ApplicationServices
func checkAccessibilityPermission() -> Bool {
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true]
return AXIsProcessTrustedWithOptions(options as CFDictionary)
}
func automateUI() {
guard checkAccessibilityPermission() else {
print("Please grant Accessibility permission in System Preferences")
return
}
let app = AXUIElementCreateApplication(pid)
}
1.2 AXUIElement Reference Safety (CWE-416)
Principle: AXUIElement references become invalid when UI changes. Never cache without validation.
class UICache {
var cachedButton: AXUIElement?
func clickCached() {
AXUIElementPerformAction(cachedButton!, kAXPressAction as CFString)
}
}
import ApplicationServices
class SafeUIReference {
private let appPID: pid_t
private let path: [String]
init(pid: pid_t, path: [String]) {
self.appPID = pid
self.path = path
}
func resolve() -> AXUIElement? {
var current = AXUIElementCreateApplication(appPID)
for identifier in path {
guard let children = getChildren(of: current) else { return nil }
guard let match = children.first(where: { getIdentifier($0) == identifier }) else {
return nil
}
current = match
}
return isValid(current) ? current : nil
}
private func isValid(_ element: AXUIElement) -> Bool {
var role: CFTypeRef?
let result = AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role)
return result == .success
}
}
1.3 Bounds Validation (CWE-119)
Principle: Validate element positions before simulating input to prevent off-screen clicks.
2. Version Requirements
# macOS SDK
macOS >= 12.0 (Monterey)
Swift >= 5.7
# Framework
ApplicationServices.framework
# For Swift convenience wrappers
AXSwift >= 0.3.2 (optional)
3. Code Patterns
WHEN creating AXUIElement references, always check result codes
var value: CFTypeRef?
AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &value)
let title = value as! String
import ApplicationServices
enum AXAttributeError: Error {
case notSupported
case illegalArgument
case invalidElement
case timeout
case unknown(AXError)
}
func getAttribute<T>(
_ element: AXUIElement,
_ attribute: String
) -> Result<T, AXAttributeError> {
var value: CFTypeRef?
let result = AXUIElementCopyAttributeValue(
element,
attribute as CFString,
&value
)
switch result {
case .success:
if let typedValue = value as? T {
return .success(typedValue)
}
return .failure(.illegalArgument)
case .attributeUnsupported:
return .failure(.notSupported)
case .invalidUIElement:
return .failure(.invalidElement)
case .cannotComplete:
return .failure(.timeout)
default:
return .failure(.unknown(result))
}
}
switch getAttribute(button, kAXTitleAttribute as String) as Result<String, AXAttributeError> {
case .success(let title):
print("Button: \(title)")
case .failure(.invalidElement):
print("Element no longer exists")
case .failure(let error):
print("Error: \(error)")
}
WHEN traversing UI hierarchy, use bounded recursive search
func findAll(_ element: AXUIElement) -> [AXUIElement] {
var results: [AXUIElement] = [element]
if let children = getChildren(element) {
for child in children {
results.append(contentsOf: findAll(child))
}
}
return results
}
import ApplicationServices
struct AXSearchConfig {
let maxDepth: Int
let maxResults: Int
let timeout: TimeInterval
static let `default` = AXSearchConfig(
maxDepth: 10,
maxResults: 500,
timeout: 5.0
)
}
func findElements(
root: AXUIElement,
matching predicate: (AXUIElement) -> Bool,
config: AXSearchConfig = .default
) -> [AXUIElement] {
var results: [AXUIElement] = []
var visited = Set<Int>()
let startTime = Date()
func search(_ element: AXUIElement, depth: Int) {
guard depth < config.maxDepth else { return }
guard results.count < config.maxResults else { return }
guard Date().timeIntervalSince(startTime) < config.timeout else { return }
let hash = CFHash(element)
guard !visited.contains(hash) else { return }
visited.insert(hash)
if predicate(element) {
results.append(element)
}
var childrenRef: CFTypeRef?
let result = AXUIElementCopyAttributeValue(
element,
kAXChildrenAttribute as CFString,
&childrenRef
)
guard result == .success,
let children = childrenRef as? [AXUIElement] else {
return
}
for child in children.prefix(50) {
search(child, depth: depth + 1)
}
}
search(root, depth: 0)
return results
}
WHEN performing actions, verify element state first
func clickButton(_ button: AXUIElement) {
AXUIElementPerformAction(button, kAXPressAction as CFString)
}
import ApplicationServices
enum ActionError: Error {
case notEnabled
case notVisible
case actionFailed(AXError)
case elementInvalid
}
func safePerformAction(
_ element: AXUIElement,
action: String
) -> Result<Void, ActionError> {
var role: CFTypeRef?
guard AXUIElementCopyAttributeValue(
element,
kAXRoleAttribute as CFString,
&role
) == .success else {
return .failure(.elementInvalid)
}
var enabled: CFTypeRef?
if AXUIElementCopyAttributeValue(
element,
kAXEnabledAttribute as CFString,
&enabled
) == .success {
if let isEnabled = enabled as? Bool, !isEnabled {
return .failure(.notEnabled)
}
}
var position: CFTypeRef?
guard AXUIElementCopyAttributeValue(
element,
kAXPositionAttribute as CFString,
&position
) == .success else {
return .failure(.notVisible)
}
let result = AXUIElementPerformAction(element, action as CFString)
if result == .success {
return .success(())
}
return .failure(.actionFailed(result))
}
switch safePerformAction(button, action: kAXPressAction as String) {
case .success:
print("Button clicked")
case .failure(.notEnabled):
print("Button is disabled")
case .failure(.elementInvalid):
print("Button no longer exists")
case .failure(let error):
print("Click failed: \(error)")
}
WHEN observing UI changes, use AXObserver with proper cleanup
var observer: AXObserver?
AXObserverCreate(pid, callback, &observer)
AXObserverAddNotification(observer!, element, kAXFocusedUIElementChangedNotification as CFString, nil)
import ApplicationServices
class AXUIObserver {
private let observer: AXObserver
private let element: AXUIElement
private var notifications: Set<String> = []
private let callback: (AXUIElement, String) -> Void
init?(
pid: pid_t,
element: AXUIElement,
callback: @escaping (AXUIElement, String) -> Void
) {
self.element = element
self.callback = callback
var observerRef: AXObserver?
let result = AXObserverCreate(pid, { observer, element, notification, refcon in
guard let refcon = refcon else { return }
let wrapper = Unmanaged<AXUIObserver>.fromOpaque(refcon).takeUnretainedValue()
wrapper.callback(element, notification as String)
}, &observerRef)
guard result == .success, let obs = observerRef else {
return nil
}
self.observer = obs
CFRunLoopAddSource(
CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer),
.defaultMode
)
}
func observe(_ notification: String) -> Bool {
let refcon = Unmanaged.passUnretained(self).toOpaque()
let result = AXObserverAddNotification(
observer,
element,
notification as CFString,
refcon
)
if result == .success {
notifications.insert(notification)
return true
}
return false
}
deinit {
for notification in notifications {
AXObserverRemoveNotification(
observer,
element,
notification as CFString
)
}
CFRunLoopRemoveSource(
CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer),
.defaultMode
)
}
}
4. Anti-Patterns
Do not:
- Cache AXUIElement references without validation
- Ignore AXError return codes
- Traverse UI trees without depth/count limits
- Perform actions without checking enabled state
- Leak AXObserver references (use RAII pattern)
- Assume Accessibility permission is granted
- Click elements outside visible screen bounds
5. Testing
import XCTest
@testable import YourApp
class AXUIElementTests: XCTestCase {
func testPermissionCheckDoesNotCrash() {
# ... (additional test cases follow same pattern)
6. Pre-Generation Checklist
Before generating macOS Accessibility code: