Swift wrapper for macOS Accessibility—chainable, fuzzy-matched queries
that read, click, and inspect any UI. The dark arts meet modern Swift!
Platform target: macOS 14.0 and later. AXorcist sits on top of the Accessibility APIs that only ship on macOS, so CI and releases intentionally stay macOS-only.
AXorcist harnesses the supernatural powers of macOS Accessibility APIs to give you mystical control over any application's interface. Whether you're automating workflows, testing applications, or building assistive technologies, AXorcist provides the incantations you need to make UI elements bend to your will.
AXorcist enables developers to create sophisticated automation tools, testing frameworks, and accessibility utilities by providing:
- Type-safe API: Compile-time safety for accessibility attributes and operations
- Modern Swift Patterns: Async/await, structured concurrency, and error handling
- Comprehensive Command System: Query, action, observation, and batch operations
- Element Management: Efficient UI element discovery and interaction
- Permission Handling: Streamlined accessibility permission workflows
This document provides a comprehensive overview of all AXorcist classes and their usage patterns. For interactive API documentation, run ../view-docs.sh to open the DocC archives.
The central orchestrator for all accessibility operations.
@MainActor
public class AXorcist {
static let shared = AXorcist()
public func runCommand(_ commandEnvelope: AXCommandEnvelope) -> AXResponse
}Key Features:
- Singleton pattern for consistent state management
- Command-based architecture for all operations
- MainActor isolation for thread safety
- Comprehensive error handling
Usage Example:
import AXorcist
let axorcist = AXorcist.shared
let query = QueryCommand(
appIdentifier: "Safari",
locator: Locator(criteria: [Criterion(attribute: "AXRole", value: "AXButton")]))
let command = AXCommandEnvelope(
commandID: "find-button",
command: .query(query)
)
let response = axorcist.runCommand(command)Swift wrapper around AXUIElement providing modern API patterns.
public struct Element: Equatable, Hashable {
public let underlyingElement: AXUIElement
public var attributes: [String: AttributeValue]?
public var prefetchedChildren: [Element]?
public var actions: [String]?
}Key Features:
- Type-safe property access with computed properties
- Automatic value conversion between CF and Swift types
- Hierarchy navigation with caching support
- Action execution with error handling
- Batch attribute fetching for performance
Common Operations:
// Create element wrapper
let element = Element(axUIElement)
// Access properties safely
let title = element.title()
let role = element.role()
let isEnabled = element.isEnabled()
// Perform actions
try element.performAction(.press)
_ = element.setValue("Hello World", forAttribute: "AXValue")
// Navigate hierarchy
let children = element.children()
let parent = element.parent()Modern async/await API for accessibility permissions.
public struct AXPermissionHelpers {
static func hasAccessibilityPermissions() -> Bool
static func requestPermissions() async -> Bool
static func permissionChanges(interval: TimeInterval = 1.0) -> AsyncStream<Bool>
static func isSandboxed() -> Bool
}Key Features:
- Async/await permission handling
- Real-time permission monitoring with AsyncStream
- Sandbox detection for permission strategy
- Non-blocking permission requests
Usage Patterns:
// Check current status
let hasPermissions = AXPermissionHelpers.hasAccessibilityPermissions()
// Request permissions asynchronously
let granted = await AXPermissionHelpers.requestPermissions()
// Monitor permission changes
for await hasPermissions in AXPermissionHelpers.permissionChanges() {
if hasPermissions {
print("Permissions granted!")
// Enable accessibility features
} else {
print("Permissions revoked!")
// Disable accessibility features
}
}- Features
- Installation
- Quick Start
- Element Search and Matching
- Available Commands
- Actions
- Notifications and Observing
- Command-Line Usage
- Advanced Examples
- Architecture
- Troubleshooting
- 🔍 Powerful Search: Find UI elements using multiple criteria with flexible matching
- 🎯 Precise Navigation: Navigate UI hierarchies with path-based locators
- 🎬 Actions: Perform clicks, set values, and trigger UI interactions
- 👁️ Observation: Monitor UI changes in real-time with notifications
- 🚀 Batch Operations: Execute multiple commands efficiently
- 📊 Rich Attributes: Access all accessibility attributes and computed properties
- 🔧 CLI Tool: Full command-line interface for scripting and automation
- 📝 Comprehensive Logging: Debug support with detailed operation logs
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/openclaw/AXorcist.git", from: "0.1.6")
]Install the signed, notarized universal CLI with Homebrew:
brew install openclaw/tap/axorcOr build and install it from source:
swift build -c release --product axorc
install -m 755 .build/release/axorc /usr/local/bin/axorcRun axorc permissions after installation. macOS will need Accessibility permission for inspection and automation.
Maintainers: see docs/releasing.md for the artifact and tap workflow.
import AXorcist
// Initialize AXorcist
let axorcist = AXorcist()
// Create a query command
let query = QueryCommand(
appIdentifier: "com.apple.TextEdit",
locator: Locator(criteria: [
Criterion(attribute: "AXRole", value: "AXTextArea")
]),
attributesToReturn: ["AXValue", "AXRole"]
)
// Execute the command
let response = axorcist.runCommand(AXCommandEnvelope(
commandID: "query-1",
command: .query(query)
))# Print a shallow accessibility tree
axorc tree --app Safari --depth 3
# Find the Back button
axorc find --app Safari --role AXButton --title Back
# Use the full JSON protocol for actions and advanced queries
echo '{"command_id":"back","command":"performAction","application":"Safari","locator":{"criteria":[{"attribute":"AXTitle","value":"Back"}]},"action_name":"AXPress"}' | axorc raw --stdinAXorcist supports multiple matching strategies:
exact- Exact string match (default)contains- Case-insensitive substring matchregex- Regular expression matchcontainsAny- Matches if any comma-separated value is containedprefix- String starts with the expected valuesuffix- String ends with the expected value
role/AXRole- Element's role (e.g., "AXButton", "AXWindow")subrole/AXSubrole- Additional role informationidentifier/id/AXIdentifier- Developer-assigned unique IDtitle/AXTitle- Element's titlevalue/AXValue- Element's valuedescription/AXDescription- Detailed descriptionhelp/AXHelp- Tooltip/help textplaceholder/AXPlaceholderValue- Placeholder text
enabled/AXEnabled- Is element enabled?focused/AXFocused- Is element focused?hidden/AXHidden- Is element hidden?busy/AXElementBusy- Is element busy?
pid- Process ID (exact match only)domclasslist/AXDOMClassList- Web element classesdomid/AXDOMIdentifier- DOM element IDcomputedname/name- Computed accessible name
{
"criteria": [
{"attribute": "role", "value": "AXButton"},
{"attribute": "title", "value": "Submit"}
]
}{
"criteria": [
{"attribute": "role", "value": "AXTextField"},
{"attribute": "title", "value": "email", "match_type": "contains"}
]
}{
"criteria": [
{"attribute": "domclasslist", "value": "btn-primary", "match_type": "contains"}
]
}{
"criteria": [
{"attribute": "title", "value": "Save"},
{"attribute": "title", "value": "Submit"},
{"attribute": "title", "value": "OK"}
],
"matchAll": false
}Navigate through UI hierarchies with path hints:
{
"path_from_root": [
{"attribute": "role", "value": "AXWindow", "depth": 1},
{"attribute": "identifier", "value": "main-content", "depth": 3},
{"attribute": "role", "value": "AXButton"}
]
}Each path component supports:
attribute- What to matchvalue- Expected valuedepth- Max search depth for this step (default: 3)match_type- How to match (default: exact)
Find elements and retrieve their attributes.
{
"command_id": "find-text-area",
"command": "query",
"application": "com.apple.TextEdit",
"locator": {
"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]
},
"attributes": ["AXValue", "AXRole", "AXTitle"],
"max_depth": 10
}Execute actions on elements.
{
"command_id": "press-back",
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [{"attribute": "AXTitle", "value": "Back"}]
},
"action_name": "AXPress"
}Retrieve the currently focused element.
{
"command_id": "focused-element",
"command": "getFocusedElement",
"attributes": ["AXRole", "AXTitle", "AXValue"]
}Find element at specific screen coordinates.
{
"command_id": "element-at-point",
"command": "getElementAtPoint",
"point": [500, 300],
"attributes": ["AXRole", "AXTitle"]
}Execute multiple commands in sequence.
{
"command_id": "inspect-and-fill",
"command": "batch",
"sub_commands": [
{
"command_id": "find-text-area",
"command": "query",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]}
},
{
"command_id": "fill-text-area",
"command": "performAction",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"action_name": "AXSetValue",
"action_value": "Hello, World!"
}
]
}Monitor UI changes in real-time.
{
"command_id": "watch-text-edit",
"command": "observe",
"application": "com.apple.TextEdit",
"notifications": ["AXValueChanged", "AXFocusedUIElementChanged"],
"include_element_details": ["AXRole", "AXTitle", "AXValue"],
"watch_children": false
}Recursively collect all elements.
{
"command_id": "collect-buttons",
"command": "collectAll",
"application": "Safari",
"attributes": ["AXRole", "AXTitle"],
"max_depth": 5,
"filter_criteria": {"AXRole": "AXButton"}
}Available actions to perform on elements:
- AXPress - Click/activate an element
- AXIncrement - Increment value (sliders, steppers)
- AXDecrement - Decrement value
- AXConfirm - Confirm action
- AXCancel - Cancel action
- AXShowMenu - Show context menu
- AXPick - Pick/select element
- AXRaise - Bring element to front
- AXSetValue - Set value (for text fields)
{
"command_id": "replace-text",
"command": "performAction",
"application": "TextEdit",
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"action_name": "AXSetValue",
"action_value": "New text content"
}Monitor UI changes with these notifications:
- AXFocusedUIElementChanged - Focus changes
- AXValueChanged - Value changes
- AXUIElementDestroyed - Element destruction
- AXWindowCreated - Window creation
- AXWindowResized - Window resizing
- AXTitleChanged - Title changes
- AXSelectedTextChanged - Text selection changes
- AXLayoutChanged - Layout updates
{
"command_id": "watch-text",
"command": "observe",
"application": "TextEdit",
"notifications": ["AXValueChanged", "AXFocusedUIElementChanged"],
"locator": {"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]},
"include_element_details": ["AXRole", "AXTitle", "AXValue"]
}axorc has human-readable inspection commands and a stable JSON mode for scripts and advanced automation.
# Check permission and recovery instructions
axorc permissions
# Print a hierarchy; use a bundle identifier when names are ambiguous
axorc tree --app com.apple.dock --depth 3
# Limit a tree to one role and emit JSON for scripts
axorc tree --app com.apple.dock --role AXDockItem --json
# Find one element with exact matching
axorc find --app Safari --role AXButton --title Back
# Use case-insensitive substring matching
axorc find --app Safari --title address --containsRun axorc --help or axorc help find for the complete terminal reference. Human-readable output goes to stdout, diagnostics go to stderr, and failures return nonzero exit codes.
Every JSON command requires command_id and command. Input can come from standard input, a file, an argument, or the legacy root-level syntax:
# Standard input
echo '{
"command_id": "enabled-button",
"command": "query",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXEnabled", "value": "true"}
]
}
}' | axorc raw --stdin
# File
axorc raw --file command.json
# Argument
axorc raw --json '{"command_id":"health","command":"ping"}'
# Action using path navigation
echo '{
"command_id": "press-back",
"command": "performAction",
"application": "com.apple.Safari",
"locator": {
"path_from_root": [
{"attribute": "AXRole", "value": "AXWindow"},
{"attribute": "AXIdentifier", "value": "toolbar"}
],
"criteria": [{"attribute": "AXTitle", "value": "Back"}]
},
"action_name": "AXPress"
}' | axorc raw --stdinExisting invocations such as axorc --stdin and axorc '{...}' remain supported. Prefer the explicit raw subcommand in new scripts.
{
"command_id": "find-submit",
"command": "query",
"application": "com.apple.Safari",
"locator": {
"path_from_root": [
{"attribute": "AXRole", "value": "AXWindow", "depth": 1},
{"attribute": "AXRole", "value": "AXWebArea", "depth": 5}
],
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXDOMClassList", "value": "submit-button primary", "match_type": "contains"}
]
},
"attributes": ["AXTitle", "AXValue", "AXEnabled", "AXPosition", "AXSize"]
}{
"command_id": "fill-form",
"command": "batch",
"sub_commands": [
{
"command_id": "fill-email",
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXTextField"},
{"attribute": "AXPlaceholderValue", "value": "Email", "match_type": "contains"}
]
},
"action_name": "AXSetValue",
"action_value": "user@example.com"
},
{
"command_id": "fill-password",
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXTextField"},
{"attribute": "AXPlaceholderValue", "value": "Password", "match_type": "contains"}
]
},
"action_name": "AXSetValue",
"action_value": "example-value"
},
{
"command_id": "submit-form",
"command": "performAction",
"application": "Safari",
"locator": {
"criteria": [
{"attribute": "AXRole", "value": "AXButton"},
{"attribute": "AXTitle", "value": "Sign In", "match_type": "contains"}
]
},
"action_name": "AXPress"
}
]
}{
"command_id": "watch-text",
"command": "observe",
"application": "com.apple.TextEdit",
"notifications": ["AXValueChanged", "AXSelectedTextChanged"],
"locator": {
"criteria": [{"attribute": "AXRole", "value": "AXTextArea"}]
},
"include_element_details": ["AXRole", "AXTitle", "AXValue"],
"watch_children": true
}- AXorcist - Main orchestrator class
- Element - Wrapper around AXUIElement with convenience methods
- ElementSearch - Tree traversal and matching engine
- AXElementMatcher - Criteria matching logic
- PathNavigator - Hierarchical navigation
- AXObserverCenter - Notification management
All operations are MainActor-isolated for thread safety when interacting with the Accessibility API.
- Early termination on first match
- Depth-limited searches
- Efficient tree traversal with visitor pattern
- Caching of frequently accessed attributes
Check Accessibility permission and print recovery instructions:
axorc permissionsUse the debug flag to see detailed search logs:
axorc raw --file command.json --debug- Element not found: Try broader criteria or increase search depth
- Action failed: Ensure element is enabled and supports the action
- Observer not working: Check notification names and app identifier
Enable debug logging in commands:
{
"command_id": "debug-query",
"command": "query",
"debug_logging": true,
...
}AXorcist is released under the MIT License. See LICENSE for details.
Please follow the main Peekaboo contributing guidelines and open pull requests against this repository when proposing AXorcist changes.
| Date | Command | Scope | Line Coverage |
|---|---|---|---|
| 2025-11-13 | swift test --package-path AXorcist --enable-code-coverage --filter AXorcistTests.PingIntegrationTests |
Ping integration suite only | 2.39 % |
| 2025-11-12 | swift test --package-path AXorcist --enable-code-coverage --filter AXorcistTests.PingIntegrationTests |
Ping integration suite only | 2.39 % |
Only the
PingIntegrationTestssubset currently runs in this headless environment; the automation-tagged suites require interactive UI access. Coverage is produced withxcrun llvm-cov report AXorcist/.build/debug/axPackagePackageTests.xctest/Contents/MacOS/axPackagePackageTests -instr-profile AXorcist/.build/debug/codecov/default.profdata.

