Consume human-verified coordinates, perform the interaction, confirm it landed
click · double_click · drag · scroll · type · key · verify · changed · wait_for · wait_gone · pause — one vocabulary across four surfaces
pixelcoords freezes your screen, lets you mark labeled regions, and writes pixel-exact coordinates with crops. pixelactions reads that session and acts on it — referencing regions by label, never by raw coordinate, so a run survives the UI moving. The loop is find → act → assert.
Status: early. The loop works end to end on macOS, Windows, and Linux — both X11 and Wayland (GNOME and KDE). That is every platform this tool set out to cover. A model can drive it over MCP, and every run leaves an audit record. Published as v0.9.7 on crates.io.
session = "~/captures/checkout"
[[step]]
action = "click"
target = "email"
[[step]]
action = "type"
text = "a@b.com"
[[step]]
action = "key"
chord = "enter"
[[step]]
action = "verify"
target = "success"Sixty seconds
pixelcoords
# a human freezes the screen, marks and labels regions
# → Downloads/pixelcoords-captures/<timestamp>/ session.json + crops: the ground truth
pixelactions doctor --probe
# prove input permission — move the cursor 1px and back
# a missing macOS grant makes event posting a SILENT no-op; the probe raises the dialog
pixelactions plan --session <dir> click:email type:"a@b.com" key:enter verify:success
# every coordinate after conversion, acts on nothing
pixelactions run --session <dir> click:email type:"a@b.com" key:enter verify:success --yes
# relocate → refuse if ambiguous → act → verify; exit 0 done, 1 failed, 2 malformed, 3 refused
pixelactions serve --session <dir>
# JSON per line on stdin/stdout — any language owns the loop
pixelactions mcp
# read-only: a model can plan and find, but not act
pixelactions mcp --yes
# ...and act. A model cannot pass this flag; you doThe tool is a loop
pixelcoordsA human freezes the screen, marks and labels regions. The saved session is the ground truth.
{ "schema": 1,
"selections": [{ "shape": "rect", "label": "submit",
"px": { "x": 812, "y": 440, "w": 96, "h": 40 } }] }pixelactions plan --flow checkout.tomlEvery coordinate after conversion, before anything moves. The permanent dry run, not a temporary phase.
{ "executed": false, "steps": [{ "summary": "click submit",
"points": [{ "x": 430.0, "y": 170.0,
"space": "logical", "monitor": 0, "scale": 2.0 }] }] }pixelactions run --flow checkout.toml --yesRelocate, act, verify. A crop that matches in more than one place produces no correction at all — the run refuses before anything is injected.
{ "summary": "click submit", "outcome": "verified",
"elapsed_ms": 412 } # exit 0pixelactions serve --session <dir>One JSON request per line, one response back — a program in any language owns the loop.
→ {"id": 2, "do": "click", "target": "email"}
← {"id": 2, "result": "done", "outcome": "verified"}echo $?Exit codes are the API: 0 done, 1 a step failed, 2 malformed, 3 refused. Grab the mouse into a screen corner and the run stops — a refusal is never worth retrying.
refused: cursor in a screen corner — kill switch # exit 3
A model can drive it
pixelcoords serves a read-only MCP server, so a model could already ask where to click. Nothing let it say do it. Now it can — over stdio, against a session a human marked.
{
"mcpServers": {
"pixelactions": { "command": "pixelactions", "args": ["mcp"] }
}
}| tool | acts | what it does |
|---|---|---|
| pixelactions_plan | no | Resolves steps and returns every coordinate. Touches nothing. |
| pixelactions_act | only with --yes | Performs them. |
| pixelactions_find | no (captures) | Re-locates a region that moved. |
Acting is off unless you launch it with --yes. This surface posts real input, so unlike a read-only server it has no safe default — and a model cannot pass a command-line flag. So the consent stays with whoever wired the client. A per-call confirmation would be written by the model itself: a speed bump against a slip, not a gate against intent.
A refusal is an answer, not an error. A failed step, a region that could not be found, an act call on a read-only server — all come back as ordinary results with ok: false. Only a malformed question is a protocol error. A model that reads a refusal as a broken tool retries, and retrying something that posts input is the worst thing this could do.
It adds nothing underneath: the kill switch, re-location, verification and the audit log are the run loop's, and a model-driven run gets all four because they were already there.
What a run reports
{
"schema": 1,
"session": "/Users/you/Downloads/pixelcoords-captures/20260728-182121-117",
"executed": true,
"steps": [
{ "index": 0, "summary": "click submit", "outcome": "verified",
"points": [{ "x": 430.0, "y": 170.0, "space": "logical", "monitor": 0, "scale": 2.0 }],
"elapsed_ms": 412 }
]
}points are the coordinates actually used, relocation corrections included — where the click went, not where the session said it would. outcome distinguishes verified from executed: the OS accepting an event is not the app reacting to one, and “nothing errored” is not “it worked”. The honesty runs deep enough that scroll always reports executed, never verified — it changes its own region on purpose, so confirm it with a wait_for on whatever it should bring into view.
To assert an outcome rather than an acceptance, name what should have moved. changed:panel proves a region stopped matching, which is the question verify cannot answer: correlation is contrast-normalized, so a region that dims uniformly behind a modal still scores ~1.0 and still reads as matching. changed compares RGB directly. And because that comparison happens where the session recorded the region, a region that merely moved would differ for the wrong reason — so when something does differ, it spends one re-location to tell a change from a departure, and refuses rather than claiming one it cannot stand behind.
The same report is appended to an audit log as the run goes — NDJSON under $XDG_STATE_HOME, %LOCALAPPDATA% on Windows, or ~/.local/state; doctor prints which. On by default. Written step by step rather than at the end, so a run the watchdog stopped or someone killed still says what it did first. It never contains typed text, and nothing strips it: steps are recorded by their summary, and a type summary is type 26 chars.
Where it stands
| Feature | pixelactions | PyAutoGUI | SikuliX | AutoHotkey |
|---|---|---|---|---|
| Price | Free | Free | Free | Free |
| Platforms | macOS · Windows · Linux (X11 + Wayland)X11 via XTEST, GNOME/KDE Wayland via the sanctioned portal + EIS path, Windows across the whole virtual desktop rather than the primary monitor — the incumbent everyone runs (PyAutoGUI) has no answer for Wayland at all. Windows multi-monitor is unit-tested but not yet run on real hardware | Windows · macOS · Linux (X11)no Wayland; primary monitor only | macOS · Windows · Linux (JVM)requires Java; development archived, continued by the OculiX fork | Windows only |
| Declarative, reviewable flows | TOML flows + chained argva pull request shows "click submit", not arithmetic | not offered | not offered | not offered |
| Language it takes to drive it | Any — JSON line protocol, or MCPa 40-line stdlib Python client ships in the docs; an MCP server for models, gated on a flag they cannot pass; no embedded interpreter, ever | Python | Jython, JRuby, JavaScript — inside its runtime | AutoHotkey, its own language |
| Re-location before acting | Every target re-located against a fresh capture before actinga crop that matches in more than one place stops the run — ambiguity is the test, not distance | locateOnScreen image search, in-script | Continuous visual search across the screen | ImageSearch, in-script |
| Verified execution | Distinguishes executed from verified; exit codes 0/1/2/3 | not offered | In-script image matching | ImageSearch / PixelGetColor, in-script |
| Maintenance status | In active development — v0.9.7 on crates.io | Last release May 2023 | Archived upstream March 2026; the OculiX fork continues | Active — v2.0.26, May 2026 |
| License | MIT, open source | BSD-3, open source | MIT, open source | GPLv2, open source |
Non-goals
Knowing what a tool is means knowing what it isn't. These are settled:
- An embedded interpreter— your bot is written in your language; the binary speaks a line protocol instead
- A scripting language— a flow is a list of steps — loops, branching, and data live in your program
- A network surface— this process holds the permission to click and type — stdio with its caller, or nothing
- A recorder— record-and-replay makes unreviewable artifacts; regions are marked by a human, on purpose
- Browser automation— Playwright and Selenium own the web, and own it well
- Cloud, accounts, telemetry— offline by design, permanently
Platform status
| macOS | Supported — the loop works end to end; primary development platform |
|---|---|
| Windows | Supported — SendInput across the whole virtual desktop, kill switch included. One limit: UIPI, which no permission lifts — a process at medium integrity cannot drive an elevated window, the UAC dialog, or the login screen |
| Linux (X11) | Supported — XTEST in root-window pixels. No caveat: X11 reports the pointer position, so the kill switch is armed |
| Linux (Wayland) | Supported on GNOME and KDE, via the portal + EIS path. One caveat: no kill switch — a Wayland flow must set failsafe = false deliberately |
Binaries ship for every platform above — shipping one that refuses to inject would imply support a build does not have. Windows placement is measured rather than assumed, but on a single-display machine: multi-monitor and mixed-DPI layouts are unit-tested and have not been run on real hardware yet. This table is kept honest — claims match runs.
Those runs were 0.2.0 through 0.4.0. Nine releases have shipped since — the pixelcoords seam, changed, the audit log, the MCP surface, and five rounds of bug fixes — and on Windows, X11 and Wayland none of them has been driven by hand. macOS has. This page will say so until someone sits at each machine.
Two slices of that are automatic now. A scenario script runs in CI against a live X server and checks that a marked region is located, that a click lands where the plan said, that a cursor in a screen corner refuses the step, and that the audit log records a refused run — a real X server and a real synthetic event, on a bare 1280×1024 virtual display with no window manager, which is not a desktop. And everything the tool does other than synthesise input now runs against a real display on macOS, Windows and Linux every push: planning against a session marked from a genuine capture, every verb, every settings key, the exit codes, the refusals, the line protocol, and the agent tools. What neither can answer is whether a click reached an application, and whether the permission model let it.
Two ways in
cargo install pixelactionsOr skip the toolchain: prebuilt binaries — download, unpack, run.
Rust 1.88+ for the cargo route. pixelactions drives the pixelcoords binary for capture-time work, and needs 0.7.6 or newer — doctor refuses an older one rather than failing mid-run. Install both:
cargo install pixelcoords pixelactionsmacOS asks for an Accessibility grant on first run. The grant attaches to the terminal that launches pixelactions, not the binary — doctor --probe proves the grant instead of assuming it.
Windows asks for nothing, and has nothing to install. What it has instead is a limit no permission lifts: a process at medium integrity cannot send input to an elevated window, the UAC dialog, or the login screen. doctor reports which of the two you are.
Linux/Wayland asks you to share a screen once, and remembers it; Linux/X11 asks nothing, because X11 has nothing to ask. Building on Linux needs the xkbcommon headers — libxkbcommon-dev and pkg-config.
No account, no network surface, no daemon — one small native binary that runs, acts, and exits. MIT-licensed, because the aim was to build the best executor in this category and give it away.