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 do

The tool is a loop

01pixelcoords

A 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 } }] }
02pixelactions plan --flow checkout.toml

Every 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 }] }] }
03pixelactions run --flow checkout.toml --yes

Relocate, 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 0
04pixelactions 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"}
05echo $?

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"] }
  }
}
toolactswhat it does
pixelactions_plannoResolves steps and returns every coordinate. Touches nothing.
pixelactions_actonly with --yesPerforms them.
pixelactions_findno (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

FeaturepixelactionsPyAutoGUISikuliXAutoHotkey
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 offerednot offerednot 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
verified against PyAutoGUI v0.9.54, 2026-07-29verified against SikuliX v2.0.5 (archived March 2026), 2026-07-29verified against AutoHotkey v2.0.26, 2026-07-29

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

macOSSupported — the loop works end to end; primary development platform
WindowsSupported — 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 pixelactions

Or 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 newerdoctor refuses an older one rather than failing mid-run. Install both:

cargo install pixelcoords pixelactions

macOS 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.