Skip to content

CLI

The xa11y command-line tool mirrors the library API. It is the fastest way to inspect an application before writing code against it.

Install it with cargo install xa11y, pip install xa11y, or npm install @crowecawcaw/xa11y. All three provide the same command, built from the same Rust code. See Install xa11y.

For the task of arriving at a working selector, see Find the right selector. If you’re looking to build agents that control desktop applications, two purpose-built tools are built on xa11y: agent-desktop.dev as a CLI, and strands-xa11y as a tool for agents built with Strands, AWS’s open-source agent SDK.

List running applications with their PIDs.

Terminal window
xa11y apps
1234 Calculator focused
5678 Slack
9012 Firefox

Columns are tab-separated: process ID, then name. The foreground application carries a third column, the literal token focused. An application whose process ID cannot be resolved shows - in the first column.

On macOS, when the accessibility permission is missing, the command’s error is followed by an interactive prompt offering to open System Settings on the matching pane (open on the deep link). The pane is named for the running macOS version: Privacy & Security → Accessibility through macOS 26, and Privacy & Security → Device Control and Data Access on macOS 27 and newer, where the Accessibility entry no longer exists. The prompt only appears when standard input is a terminal; scripts and pipes get the error message alone. The same prompt follows the equivalent failure of every other command that reads the accessibility tree.

List the OS shell surfaces currently present.

Terminal window
xa11y shell
taskbar 1204 Taskbar
desktop 1204 Desktop
flyout 5120 System tray overflow window

Columns are tab-separated: kind, process ID, then name. A surface whose process ID cannot be resolved shows - in the second column. The listing is live, so a flyout appears only while it is on screen. Enumerating never opens or presses anything.

The kind column, what each kind maps to per platform, and the query API in every language are in Shell surfaces.

List top-level windows.

Terminal window
xa11y windows --pid 1234
window "Calculator" [enabled visible] bounds=(0,0,400,600)
window "Settings" [enabled visible] bounds=(20,20,300,200)
(2 windows)

Without --app / --pid every running application is enumerated and all top-level windows are listed; with either filter only that application’s windows are shown. Every window is listed exactly once. On macOS a single Application entry covers the process and each enumerated window keys uniquely, so a window is never dropped for sharing an identity with another (macOS AXIdentifier is not unique per window). On Linux, where AT-SPI may register several Application entries for one process (a main entry plus per-event-loop pieces), the same-pid entries are deduplicated by the (bus_name, stable_id) identity, so a process’s windows are process-complete rather than the entries themselves being one-per-process. Each line is one window rendered like xa11y find renders elements, with its actions including the window verbs below. A window state the platform cannot report (minimized, maximized, fullscreen) is simply absent from the line rather than shown as a guessed false.

Print the full accessibility tree for an application.

Terminal window
xa11y tree --app Calculator
application "Calculator" [enabled visible]
├── window "Calculator" [enabled visible] bounds=(0,0,400,600)
│ ├── group "Display" [enabled visible]
│ │ └── static_text "Result" value="42" [enabled visible]
│ └── group "Keypad" [enabled visible]
│ ├── button "7" [enabled visible focusable] actions=[press,focus]
│ ├── button "8" [enabled visible focusable] actions=[press,focus]
│ └── button "+" [enabled visible focusable] actions=[press,focus]
└── menu_bar [enabled visible]

This is the fastest way to understand an app’s structure. Every element shows its role, name, value, states, bounds, and available actions.

The same output is available in-code via Element.dump() (and Element.tree() for a structured snapshot) in every language binding. See Find the right selector for examples.

Find elements matching a CSS-like selector.

Terminal window
xa11y find "button" --app Calculator
button "7" [enabled visible focusable] actions=[press,focus]
button "8" [enabled visible focusable] actions=[press,focus]
button "+" [enabled visible focusable] actions=[press,focus]
(3 matches)

A selector that matches nothing is an error, not an empty result. The command prints no elements matched selector: <selector> and exits non-zero, which is what makes it usable as a shell assertion.

Use this to test selectors before putting them in code:

Terminal window
# Does this selector match what I expect?
xa11y find "text_field[name^='Message']" --app Slack
# How many list items are there?
xa11y find "list > list_item" --app Finder

-o selects the output format: pretty (the default, shown above), bounds, or center.

Perform an action on the first element matching a selector.

Terminal window
xa11y action press "button[name='7']" --app Calculator
ok

Actions that need a value use --value:

Terminal window
xa11y action set-value "text_field[name='Search']" --app Safari --value "hello"
xa11y action set-numeric-value "slider[name='Volume']" --app Music --value 88
xa11y action type-text "text_field[name='Message']" --app Slack --value "ship it"
xa11y action select-text "text_field" --app TextEdit --value "0,5"

Window actions use --at and --size instead:

Terminal window
xa11y action activate "window" --pid 1234
xa11y action move-to "window[name='Dialog']" --pid 1234 --at 100,200
xa11y action resize-to "window[name='Dialog']" --pid 1234 --size 640,480

set-value writes text. A slider, spin button, or progress bar carries a number instead, and rejects text with TextValueNotSupported; use set-numeric-value, which takes any finite number and does in one call what increment would take a step at a time.

Window verbs are only meaningful on window-like elements: window, or dialog, which every platform surfaces as a top-level window. move-to and resize-to take logical screen coordinates (the same space as the bounds=() in xa11y find output). Where a platform has no accessibility API for a verb — Linux cannot minimize, maximize, restore, or close a window — the command exits non-zero with Unsupported; it never falls back to input simulation.

Action Description
press Press a button or invoke a control
focus Move focus to the element
blur Remove focus from the element
toggle Toggle a checkbox or switch
expand Expand a disclosure or menu
collapse Collapse a disclosure or menu
select Select an item
show-menu Show a context menu
scroll-into-view Scroll the element into view
increment Increment a numeric value
decrement Decrement a numeric value
set-value Set text value (requires --value)
set-numeric-value Set numeric value, for sliders and spin buttons (requires --value N)
type-text Type text as keystrokes (requires --value)
select-text Select a text range (requires --value START,END)
activate Bring the window to the foreground and focus it
minimize Minimize the window
maximize Maximize the window
restore Restore the window from minimized/maximized
close Close the window
move-to Move the window (requires --at X,Y)
resize-to Resize the window (requires --size W,H)

Stream accessibility events in real time. Runs until you press ctrl-c.

Terminal window
xa11y events --app Calculator
[focus_changed] button "7"
[value_changed] static_text "Result"
[focus_changed] button "+"
[state_changed] check_box "Wrap" Checked=true
[value_changed] static_text "Result"

Event type names are snake_case, matching the roles in the same output and the event_type strings the Python binding reports. See Events for the full list. A target that could not be resolved prints as -, and a state_changed line appends the flag and its new value.

Useful for understanding what events fire when you interact with an app, or for debugging event subscriptions in your code.

These synthesise OS-level events at screen coordinates. They take no selector and read no accessibility data, so they need no target application. See Simulate input for when to prefer an accessibility action.

Command Flags
xa11y click --at X,Y [--button left|right|middle] [--count N] [--held K,K]
xa11y move --at X,Y
xa11y drag --from X,Y --to X,Y [--button B] [--duration-ms MS] [--held K,K]
xa11y scroll --at X,Y [--dx N] [--dy N]
xa11y key KEY [--held K,K]
xa11y type TEXT

Captures pixels and writes them as a PNG. With --annotate it also reads the accessibility tree, draws an outlined box over every element a selector matches, and prints a legend.

Terminal window
xa11y screenshot [--region X,Y,W,H] --out PATH
[--app NAME | --pid PID | --shell KIND]
[--annotate SELECTOR]...
[--legend text|json|none]
Flag Description
--out PATH Required. Writes the PNG to PATH, or to stdout for -
--region X,Y,W,H Captures this rectangle in logical screen coordinates. Omitted, the capture is whatever the platform treats as the whole screen: the virtual desktop on Windows, one display on macOS, the root window on Linux
--app NAME / --pid PID / --shell KIND The target whose tree --annotate searches, as described under Flags
--annotate SELECTOR Repeatable. One annotation group per occurrence
--legend text|json|none Legend format on stdout. Defaults to text

Without --annotate the command reads no accessibility data, takes no target, and prints nothing on stdout. A capture written to a file reports its dimensions on stderr.

Each occurrence is one group. A group has a letter (A, B, and so on past Z as AA), a colour from a seven-entry palette that cycles, and one box per element its selector matches inside the target. A box carries a tag of the group letter and a 1-based index, so B7 is the seventh match of the second --annotate. That index is the :nth(n) argument in the entry’s selector.

Selectors are the same syntax the library takes; see Selectors. A comma-separated alternation is refused with InvalidSelector, because <selector>:nth(n) would bind to its last clause alone. Pass one --annotate per clause.

Cropping and annotating are independent. --region chooses what is captured and --annotate chooses what is drawn on it, so a match outside the captured area is reported as omitted rather than clamped to an edge.

At most 100 elements are described across all groups, counting drawn boxes and omitted elements. Matches past the cap are neither drawn nor listed.

For the task, see Annotated screenshots.

text writes a group header block, one line per box, then a summary of what was omitted and what the cap cut off:

A button #E69F00 3 annotated
B text_field #56B4E9 1 annotated
A1 button "7" bounds=104,318,48,44 button:nth(1)
A2 button "8" bounds=156,318,48,44 button:nth(2)
A3 button "9" bounds=208,318,48,44 button:nth(3)
B1 text_field "Display" bounds=100,60,320,52 text_field:nth(1)
omitted: 1 element (outside_capture: button "Paste")
truncated: 37 more elements matched but were not described (cap: 100)

A group with no matches still gets a header line. An element with no accessible name shows - in the name column, which keeps it distinct from one named with the empty string. The omission summary lists at most five elements inline.

Once the cap has been reached, each header line from the first affected group onward carries a parenthetical, because its count is a lower bound:

B button #56B4E9 62 annotated (cap reached, so more may have matched)
C menu_item #009E73 0 annotated (cap reached at or before this group, so 0 is not "matched nothing")

Groups resolve in --annotate order and resolution stops at the cap, so the groups before the first affected one are exact. A group starved by the cap would otherwise read the same as one whose selector reached nothing.

json writes one object instead, with groups (letter, selector, colour, count, and capped per group), legend, omitted, truncated, and cap. It carries the whole omission list.

capped is the boolean behind the parentheticals above. true means the cap may have shortened this group, so "annotated": 0 alongside it does not mean the selector matched nothing. It is false on every group when truncated is 0.

none suppresses the legend and leaves stdout empty.

These three exit with code 2 before any capture is taken.

Invocation Message names
--annotate with no --app, --pid, or --shell The three target flags
--out - together with a legend --out FILE, or --legend none
--legend with no --annotate Adding --annotate SELECTOR, or dropping --legend

PNG bytes and legend text cannot share stdout, so the second refuses rather than moving the legend to stderr, where a caller piping the image would never learn that it went elsewhere. A --legend value outside text, json, and none is a usage error as well.

Serves the commands above as Model Context Protocol tools over stdio. An MCP client launches it as a subprocess and speaks JSON-RPC on its standard streams, one message per line. It is not meant to be run by hand.

Terminal window
xa11y mcp

The same server ships in all three packages, because all three launchers run the same Rust implementation. To wire it into a client, see Serve xa11y over MCP.

The server is dual-era. It answers server/discover for clients on the stateless revisions (2026-07-28 and later) and initialize for clients that open with a handshake (2025-11-25, 2025-06-18, 2025-03-26). A request naming a revision it does not speak gets a -32022 error listing the ones it does.

Each tool takes the arguments its CLI counterpart takes as flags. The accessibility tools need app, pid, or shell. The input tools take screen coordinates and no target. screenshot takes a target only when annotate gives it selectors to resolve.

Tool Arguments
apps none
shell none
windows app, pid, limit
tree app, pid, shell, max_depth
find selector, app, pid, shell, limit
action action, selector, app, pid, shell, value, at, size
click x, y, button, count, held
move x, y
drag from_x, from_y, to_x, to_y, button, duration_ms, held
scroll x, y, dx, dy
key key, held
type text
screenshot x, y, width, height, annotate, app, pid, shell
events_start app, pid, kinds
events_poll subscription_id, max, timeout_ms
events_stop subscription_id

action accepts the same verbs as xa11y action. key accepts the same key names as xa11y key. Selectors are the same syntax the library takes; see Selectors.

shell takes a kind string, is mutually exclusive with app, and combines with pid. A kind matching several surfaces comes back as an ambiguous_shell_surface failure with the candidates listed. See Shell surfaces.

The windows tool is the MCP counterpart of xa11y windows: without app/pid it lists the top-level windows of every running application, with either it lists those of that one. Results are capped at limit (default 50) and report truncated when anything was dropped.

annotate is an array of selectors, one annotation group per entry, and it is screenshot’s counterpart to the CLI’s repeatable --annotate. Every element a selector matches gets an outlined box and a tag drawn on the capture, and the result gains legend, omitted, and truncated alongside the image. It reads the accessibility tree, so it needs app, pid, or shell. That target scopes the selectors and leaves the captured area to x, y, width, and height. At most 100 elements are described; truncated counts the matches past that cap. See Annotate a screenshot.

The action tool differs from xa11y action in one way, deliberately. The command acts on the first match, as the library’s Locator does. The tool requires the selector to match exactly one element and returns an ambiguous_selector failure listing the candidates otherwise, because its schema promises that and a model told “exactly one” cannot tell that it got the first of several. Narrow the selector or pick one with :nth(n).

Both surfaces auto-wait: the element has to exist, be visible, and be enabled before the action runs, within the default timeout (5 seconds, or XA11Y_DEFAULT_TIMEOUT seconds when that is set in the server’s environment). The window verbs (activate, minimize, maximize, restore, close, move-to, resize-to) and scroll-into-view are the one exception: they wait only for enabled, because a minimized window is legitimately not visible and must still be reachable by the very verbs that restore or activate it. A call that is going to fail therefore takes that long, and no MCP argument shortens it. ok: true reports that the application accepted the call, not that anything changed; read the tree again to confirm an effect.

An element’s actions field reports what the application advertises through the platform’s action interface. It is not the set of verbs action accepts, and it is not a capability list: a slider that advertises nothing still increments. Pick the verb from the element’s own properties — numeric_value for the value verbs, an editable state for the text verbs.

xa11y events blocks for as long as you watch, and a tool call that never returns hangs the client. The MCP counterpart is a trio instead: events_start returns a subscription_id, events_poll drains what has arrived, and events_stop closes it.

Subscriptions are per application, so events_start takes app or pid and has no shell argument. kinds filters what gets buffered; the names are the ones in Events.

Events buffer from the moment events_start returns, so a subscription has to exist before the action it is meant to observe. At most 1024 are held, and past that the oldest are evicted: dropped in each poll result counts what was lost since the previous poll, dropped_total over the subscription’s life, and each event’s sequence makes the gap visible in the events themselves.

events_poll returns at most max events (100 by default) and does not block by default. A timeout_ms up to 15000 waits for the first event and returns as soon as one lands. live: false means the source is gone — the application exited, or the platform dropped the subscription — so nothing further can arrive.

A handle lives in the server process and no longer. It stops resolving once events_stop closes it, or after five minutes without a poll. Either way the next call naming it is a subscription_expired failure, which is a different kind from the subscription_not_found an id that was never issued gets.

Every tool returns structuredContent, mirrored as JSON in a text block for clients on revisions that predate structuredContent. find reports each match’s bounds and a precomputed center, so a result feeds straight into click or screenshot without arithmetic.

Results are bounded, because each one lands in a model’s context window. tree walks 12 levels by default and never returns more than 2000 nodes; find returns 50 matches by default and never more than 500. Both report whether they truncated.

Failures split the way the protocol asks. An unknown tool or a malformed request is a JSON-RPC error. Everything a model could fix by retrying — a selector that matched nothing, an application that is not running, a bad key name, a selector that matched several where one was required — comes back as a result with isError: true, carrying the failure’s kind and, where the error has one, its diagnosis: what the operation was waiting for, what it last observed, and which near-miss elements were present. find and action report a miss the same way, so a typo costs one call from either.

find -o bounds and find -o center emit coordinates in the form the input and screenshot commands take, which is how an accessibility query drives a pixel operation:

Terminal window
region=$(xa11y find 'button[name="OK"]' --app Safari -o bounds)
xa11y screenshot --region "$region" --out button.png
xa11y click --at "$(xa11y find 'button[name="OK"]' --app Safari -o center)"

The accessibility commands other than apps and shell require a target. The input-simulation commands take none of these flags, and screenshot takes one only when --annotate gives it selectors to resolve.

Flag Description
--app NAME Target application by exact, case-sensitive name (see xa11y apps)
--pid PID Target by process ID
--shell KIND Target a shell surface by kind (see xa11y shell). Mutually exclusive with --app; combine with --pid to pick between same-kind surfaces

tree, find, and action accept --shell wherever they accept --app. The kinds are listed in Shell surfaces.

A kind matching several surfaces, and a kind matching none, are operation failures: exit code 1, with the candidate list on stderr. The invocation was well formed; the desktop held the wrong number of surfaces. Two argument mistakes are usage errors at exit code 2: --shell passed together with --app, and a kind string that is not a known kind.

--pid is the only lever for choosing between same-kind surfaces, and it does not always separate them. Two Linux panels drawn by one panel process share a pid, so no --pid value narrows that match. The refusal says as much rather than picking one.

xa11y find and xa11y action take the same selector syntax as the library. See Selectors for operators, combinators, and attributes.

Terminal window
xa11y find "[focusable='true']" --app Slack