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.
Commands
Section titled “Commands”xa11y apps
Section titled “xa11y apps”List running applications with their PIDs.
xa11y apps1234 Calculator focused5678 Slack9012 FirefoxColumns 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.
xa11y shell
Section titled “xa11y shell”List the OS shell surfaces currently present.
xa11y shelltaskbar 1204 Taskbardesktop 1204 Desktopflyout 5120 System tray overflow windowColumns 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.
xa11y windows
Section titled “xa11y windows”List top-level windows.
xa11y windows --pid 1234window "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.
xa11y tree
Section titled “xa11y tree”Print the full accessibility tree for an application.
xa11y tree --app Calculatorapplication "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.
xa11y find
Section titled “xa11y find”Find elements matching a CSS-like selector.
xa11y find "button" --app Calculatorbutton "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:
# 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.
xa11y action
Section titled “xa11y action”Perform an action on the first element matching a selector.
xa11y action press "button[name='7']" --app CalculatorokActions that need a value use --value:
xa11y action set-value "text_field[name='Search']" --app Safari --value "hello"xa11y action set-numeric-value "slider[name='Volume']" --app Music --value 88xa11y 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:
xa11y action activate "window" --pid 1234xa11y action move-to "window[name='Dialog']" --pid 1234 --at 100,200xa11y action resize-to "window[name='Dialog']" --pid 1234 --size 640,480set-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.
Available actions
Section titled “Available actions”| 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) |
xa11y events
Section titled “xa11y events”Stream accessibility events in real time. Runs until you press ctrl-c.
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.
Input simulation
Section titled “Input simulation”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 |
xa11y screenshot
Section titled “xa11y screenshot”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.
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.
--annotate
Section titled “--annotate”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.
--legend
Section titled “--legend”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 annotatedB 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.
Usage errors
Section titled “Usage errors”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.
xa11y mcp
Section titled “xa11y mcp”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.
xa11y mcpThe 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.
Protocol revisions
Section titled “Protocol revisions”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.
Events
Section titled “Events”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.
Results
Section titled “Results”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.
Errors
Section titled “Errors”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.
Composing the two halves
Section titled “Composing the two halves”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:
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.
Selectors
Section titled “Selectors”xa11y find and xa11y action take the same selector syntax as the library.
See Selectors for operators, combinators, and
attributes.
xa11y find "[focusable='true']" --app Slack