hypruse

by ilyaskhallouki

Not rated
GitHub

About

Computer use for Hyprland - MCP server giving AI agents native hands on your Wayland desktop.

Details

Author
ilyaskhallouki
Categories
Productivity

Setup

Install hypruse in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/ilyaskhallouki/hypruse

Follow the installation instructions in the repository README, then restart your MCP client.

Computer use forHyprland.AnMCPserver that gives AI agents native hands on your Wayland desktop: workspaces, windows, mouse, keyboard, screenshots.

No ydotool daemon. No root. No portals. No X11.

Computer use exists on macOS and Windows. On Linux there is effectively nothing: the Claude Desktop Linux beta explicitly shipswithoutscreen control, Anthropic's reference implementation is an X11 container, and the existing Wayland attempts lean on setuid uinput hacks or GNOME-only portals.

Meanwhile Hyprland already exposes everything an agent needs, better than any accessibility bridge: a complete IPC surface for state and window management, and first-class Wayland protocols for input. hypruse just wires them to MCP:

- Semantic first.desktopreturns the real window/workspace tree (addresses, classes, titles, geometry) in one call. The agent switches workspaces and focuses windows the way you do (instantly, over IPC), not by squinting at pixels.
- Vision when it matters.Screenshots of a monitor, an exact window crop, or a zoomed region, with the geometry/scale metadata to map any pixel back to a clickable coordinate: a coarse-to-fine loop
grounded in the GUI-agents research.
- Native input.Clicks and scrolls are spoken directly over the Wayland wire (zwlr_virtual_pointer_v1); typing goes throughwtype's virtual keyboard with a proper XKB keymap, unicode-safe on any layout.

agent (Claude Code, or any MCP client) │ stdio ▼ hypruse ├── hyprctl -j ········▶ desktop state: monitors, workspaces, windows, layers ├── hyprctl dispatch ··▶ focus / move / close / launch / movecursor ├── grim ··············▶ screenshots: monitor, window crop, region ├── busctl (AT-SPI) ···▶ accessibility tree: named controls, current │ values, exact coords (ui / marks / click_ui) ├── wtype ·············▶ keyboard (zwp_virtual_keyboard_v1, real XKB keymap) └── raw Wayland wire ··▶ click & scroll (zwlr_virtual_pointer_v1)

Optional binaries gate two more tools:imagemagickdraws the numbered overlay formarks, andwl-clipboardbacks the opt-inclipboardtool.

- No ydotool / uinput.That path needs a daemon, udev rules or root, and types US scancodes that break on other layouts. hypruse is just another Wayland client of your compositor, same standing aswlrctl.
- No portals.xdg-desktop-portal-hyprlanddoes not implement the RemoteDesktop portal (InputCapture is capture, not injection), so anything built on libei/portals silently degrades on Hyprland. hypruse doesn't try.
- Cursor positioning viahyprctl dispatch movecursor(global logical coordinates, exact on any monitor layout), with only button/axis events on the virtual pointer, sidestepping the known multi-monitor bugs of absolute virtual-pointer motion (
hyprwm/Hyprland#6749).

The acting tools (pointer,keyboard,click_ui,hypr,use_bind,sequence) take an optionalthenargument that appends the result to the same call, so the agent sees the effect without a second round-trip:then='desktop'adds a fresh semantic snapshot (~20 ms, cheap, best for window/focus changes),then='screenshot'a stable capture (best for visual changes),then='ui'the acted-on window's controls with their current values (a few hundred exact tokens, best after typing or toggling;click_uireads the window it clicked even if the click handed focus to a dialog, the others read the focused window),then='none'nothing (the default everywhere exceptsequence, which defaults to'desktop').

The tools group into five capabilities, ordered most-reliable-and-cheapest first. An agent that reaches for them in this order is both faster and more accurate, and many tasks never need a screenshot at all.

1. Semantic desktop control (start here)

desktopreturns the entire window and workspace tree in one call: every window's address, class, title, and geometry, the active window, the cursor, and any layer surfaces on screen (launchers, bars, notification popups).hyprandlaunchthen act on it over IPC in milliseconds: switch workspace, focus/move/close/fullscreen/float a window by address, or start an app.

Use it well:never take a screenshot to find or arrange windows. Readdesktop, act on the address you want.launchblocks on the realopenwindowevent and hands back the new window's address, so there is nothing to poll or guess; it also relocates single-instance apps (browsers) that ignore workspace rules.

When an app exposes an accessibility tree (most GTK and Qt apps), you can target controls by name instead of by pixel.uilists every control with its exact global coordinate, and reports the current value of the controls that carry one: the text in a field, a slider's percentage, a checkbox's state.click_uiresolves a name and clicks it in one call, through the real cursor (so the beacon and every safety guarantee still apply).marksdraws numbered marks over a screenshot with a legend, for when you want to see the options first and thenclick_ui(mark=N).

Use it well:reach forclick_ui name="Save"before estimating any pixel, since it is exact and spends no image. Read a form's state withui(did the box actually tick?) instead of screenshotting it. An ambiguous name returns the candidates rather than guessing. When an app exposes no tree (terminals, canvas apps, Electron/Chrome without--force-renderer-accessibility) the tool says so, and you fall back to vision.

3. Vision when it matters: the zoom loop

For everything the accessibility tree cannot name,screenshot(monitor, window crop, or region) andzoom(a native-resolution re-capture around a point) carry a strict coordinate contract,global = geometry + pixel / scale, that stays exact on every monitor and fractional scale.

Use it well:don't guess a small control from a full-screen image. Work coarse-to-fine: screenshot the window, estimate the target,zoomthere, re-estimate on the sharp crop, then click. This two-step loop is theresearch-backedway to hit small targets.

For an agent the model calls dominate task latency, not the desktop, so the real speedups are structural.sequenceruns an ordered micro-plan (click, type, press enter, wait) in a single call, stopping the moment the desktop changesstructurallyin a way a step did not intend (a window opening, closing, or moving, an unexpected workspace switch, or a seat-taking launcher or on-screen keyboard; it deliberately ignores bare focus changes and notification popups).then='desktop' | 'screenshot' | 'ui'fuses a fresh view of the result into the acting call itself.wait_forblocks on real compositor events (a window or launcher opening, a title changing, a workspace switch, an urgency hint, screen-sharing starting) instead of sleeping and hoping.

Use it well:collapse a known click/type/enter flow into onesequence. After typing into a form, addthen='ui'to read the effect back in a few hundred exact tokens instead of a screenshot. After a launch or a shortcut that opens something,wait_forthe event rather than sleeping.

hypruse hands an agent your real seat, so it ships the controls to bound what that agent can do. Beyond the always-on approval prompts and the Waybar activity beacon, opt-in env flags narrow what an agent can touch:HYPRUSE_READONLYexposes only the observation tools;HYPRUSE_CONFINErestricts input to the windows the agent launched, or a class/workspace allowlist;HYPRUSE_AUTH_GUARD(on by default) refuses to drive authentication dialogs;HYPRUSE_STRICTrefuses to act if you took the seat back;HYPRUSE_MARKtags agent-owned windows and announces when the agent opens a window or captures the screen. Two more record rather than restrict:HYPRUSE_JOURNALwrites an auditable NDJSON line per tool call, refusals included, andHYPRUSE_DRYRUNruns every check and delivers nothing, so you can watch an agent plan the work before it touches your desktop.

Use it well:run read-only for the first week. When you trust a workflow, allowlist its tools and, if you want to walk away, confine the agent to a scope so your password manager on another workspace stays untouchable. Keep a panic bind handy (hypruse stop, orpkill -f hypruse). TheSecurity modelhas the full story.

Requirements: Hyprland,grim,wtype(most Hyprland setups already have both), anduv. The accessibility tools (ui/marks/click_ui) usebusctl, which ships with systemd. Optional:wl-clipboardfor the opt-in clipboard tool,imagemagickfor numberedmarkscaptures.

yay -S hypruse # or hypruse-git for main

Then let it set itself up and verify the environment:

hypruse init # detects your MCP clients, registers (asks first), runs doctor hypruse doctor # just the diagnostics
claude mcp add -s user hypruse -- uvx hypruse
claude mcp add -s user hypruse -- uv run --directory /path/to/hypruse hypruse

Any other MCP client: runuvx hypruseas a stdio server. hypruse is also in the officialMCP registryasio.github.IlyasKhallouki/hypruse, so clients that browse the registry can install it from there.

Read-only mode:setHYPRUSE_READONLY=1in the server config to expose only the observation tools (desktop,screenshot,zoom,ui,marks,binds,wait_for). The agent can see and narrate but cannot click, type, or launch. A good first week.

The Linux beta shipswithoutAnthropic's first-party computer use, but stdio MCP servers work in chat, which makes hypruse the workaround. In~/.config/Claude/claude_desktop_config.json:

{ "mcpServers": { "hypruse": { "command": "uvx", "args": ["hypruse"], "env": { "HYPRUSE_SCREENSHOT_MODE": "image" } } } }

Two Desktop-specific notes: useimagemode (Desktop renders inline MCP images and has no file-read tool), and the app must run natively inside your Hyprland session so the server inheritsWAYLAND_DISPLAY/HYPRLAND_INSTANCE_SIGNATURE; from a VM or container it cannot reach your compositor. If your Desktop install bypasses tool-approval prompts, treat theWaybar indicator + panic keybindas mandatory, not optional.

Read this section before installing.hypruse hands an agent your mouse, your keyboard, your screen contents, and an app launcher.The layers that keep that sane:
- Approval:MCP clients gate tool calls. In Claude Code, allowlist the read-only tools (desktop,screenshot) and leavepointer/keyboard/hypr/launchon ask-first until you trust a workflow.
- Visibility:the server maintains an activity beacon ($XDG_RUNTIME_DIR/hypruse/state.json); the shipped
Waybar moduleis invisible when idle and shows a robot indicator while an agent has hands on your desktop.
- Interruption:click the indicator, or bind a panic key. The portable form works for every install:bind = SUPER SHIFT, BackSpace, exec, pkill -f hypruse. Ifhypruseis on your PATH (the AUR or a pipx install),bind = SUPER SHIFT, BackSpace, exec, hypruse stopis nicer: it signals the server to shut down gracefully, releasing any held pointer button and clearing the beacon. For auvxinstall useexec, uvx hypruse stop; for a source checkout,exec, uv run --directory /path/to/hypruse hypruse stop. Killing it mid-action is safe either way: button press/release pairs never span tool calls, and even a long drag's held button is released on the way out.
- The seat is shared.There is one cursor and one keyboard focus, and Hyprland's focus-follows-mouse means a cursor move alone can retarget keystrokes. Don't type while an agent is driving; watch the indicator.
- Scope:stdio only (no network listener), nothing persisted except the beacon and the capped screenshot cache in$XDG_RUNTIME_DIR(tmpfs, newest 20) and, if you turn it on, the
action journalon disk under$XDG_STATE_HOME(rotated, one generation, and text-redacted by default). No clipboard access unless you opt in:HYPRUSE_CLIPBOARD=1registers aclipboardtool (never in read-only mode); clipboards hold passwords, so leave it off unless a workflow needs it. A screenshot sees everything visible: treat an agent session like screen sharing.
- What the agent reads is untrusted.Window titles, accessibility names and values, and clipboard text flow verbatim into the agent's context, and any web page, filename, or document can put instructions there (prompt injection). hypruse cannot sanitize meaning, so the approval layer is the backstop: keep consequential tools (launch,keyboard,clipboard) on ask-first when the agent will look at untrusted windows, and treat "the screen told me to" as attacker input when reviewing an approval prompt.
- Input never lands where it silently would not work.Three always-on checks (no env flag) refuse or annotate rather than report a phantom success: a click aimed under a launcher or on-screen keyboard layer surface (which sits above windows and would swallow it), typing while a launcher holds the keyboard grab, and any input while the session islocked(a livehyprlock/swaylockprocess, which is anext-session-lockclient invisible to the window and layer lists). While locked,keyboard/click_ui/pointerrefuse unlessallow_auth=truesays a human wants the agent driving the unlock prompt. These are truthfulness aids, not a sandbox: they fail open on an unreadable system state, so they harden the common case without being a boundary you can lean on.

Six opt-in env flags. The first four narrow what an agent can touch, and each fails towardlessaction; the last two record and rehearse rather than restrict. All compose with the layers above:

- HYPRUSE_CONFINErestricts input to a scope of windows:launched(only windows hypruse itself opened this session),class:firefox,kitty, orworkspace:3,special:notes. Keyboard,click_ui, andhyprwindow ops are refused outside the scope; apointerclick is refused when any window under the point is out of scope (Hyprland's window list is not z-ordered, so hypruse fails closed rather than guess which window is on top). This is what lets you leave an agent working while your password manager sits on another workspace, untouchable.use_bindis refused outright while confinement is set, because a keybind runs an arbitrary compositor action that cannot be scoped to a window.
- HYPRUSE_AUTH_GUARD(defaulton) refuses to click or type into a system authentication dialog (polkit agents, the GNOME keyring prompt), so a manipulated agent cannot approve a privilege escalation. SetHYPRUSE_AUTH_GUARD=strictto also refuse typing into a password field inside an ordinary window (a browser login), detected via the accessibility tree. A per-callallow_auth=trueonpointer/keyboard/click_uioverrides it, and because it changes the tool's arguments the override surfaces distinctly in the approval prompt.HYPRUSE_AUTH_GUARD=0disables it.
- HYPRUSE_STRICTrefuses to act when the cursor or focused window moved since hypruse's last action (the human, or a popup, took the seat): the agent must re-readdesktop/screenshotand retry, so it never types into a window you just switched to.
- HYPRUSE_MARKmakes the agent's presence legible on the desktop: it tags every window the agent openshypruse-ownedand flashes an on-screen notice when the agent opens a window or captures the screen. It also installs aborder_colorwindowrule on that tag so owned windows get a colored outline, but whether aruntimewindow rule renders depends on your Hyprland version and config precedence (on some setups it does not take effect). For a guaranteed outline, add the rule to your Hyprland config, which hypruse's tagging then matches:windowrule = border_color rgb(ff5555), tag hypruse-owned(older Hyprland:tag:hypruse-owned).
- HYPRUSE_JOURNALrecords what the agent did: one NDJSON line per tool call in$XDG_STATE_HOME/hypruse/journal.ndjson(HYPRUSE_JOURNAL=1), or a path of your own. Read it withhypruse journal, re-run it withhypruse replay. See
The recordbelow.
- HYPRUSE_DRYRUNis a rehearsal: every argument check and every guard above runs, then the call reports what itwouldhave done and delivers nothing.

The flags above decide what an agent may do in the moment and then forget it happened.HYPRUSE_JOURNALis the memory. One JSON object per line, sotail -f,grep, andjqall work on a live file:

{"v":1,"seq":7,"ts":"2026-08-02T09:12:13.456Z","kind":"act","tool":"pointer", "args":{"action":"click","x":800,"y":60},"outcome":"ok","ms":37,"result":"click ok"}

kindsplits the two questions people actually ask:actis input delivered to your desktop,observeis the agent looking, which is what a privacy audit wants (when the screen was captured, when the clipboard was read). Observationresultsare never recorded, only that they happened, so the journal never becomes a second copy of everything the agent saw.Refusals are recorded too, with the guard's own message: it is the only place the history of your trust layers doing their job exists.

Typed and copied text is recorded as a length plus a short digest, not as text, because keystrokes are passwords.HYPRUSE_JOURNAL_TEXT=1keeps it verbatim, which you need only to replay typing. Note what a digest is and is not: it proves two entries typed the same thing and it will not hand a reader your password, but it is an unsalted SHA-256 prefix next to an exact character count, so a four-digit PIN is trivially recovered from it. Treat the journal as sensitive either way. It is written 0600 in a 0700 directory, and rotated atHYPRUSE_JOURNAL_MAX_BYTES(8 MiB, one previous generation kept as.1; set 0 to never rotate).

The journal is a recorder, not a guard: if it cannot be written the action still happens and hypruse warns once on stderr, because failing your desktop over a log line is the wrong trade. What it doesnotrecord is athen=observation as its own entry: the acting call that carried it is recorded,then=screenshotand all, but the capture does not get a second line of its own.

Read it back withhypruse journal(--actsfor actions only,--refusedfor what the guards stopped,-n Nto tail,-vto include each result):

1 09:12:10 session start pid 4211 0.10.0 confine=launched auth_guard=1 strict=True 14 09:12:13 act pointer action=click x=800 y=60 16 09:12:15 act keyboard action=type text=<11 chars> 19 09:12:19 act hypr action=close_window target=0x5f2a10 REFUSED TrustError: 0x5f2a10 (Signal) is outside the agent's confinement scope 312 actions (0 dry), 604 observations, 1 refused by a trust layer, 0 errors

HYPRUSE_DRYRUN=1turns the same session into a rehearsal. Every acting tool validates its arguments and runs every trust guard, then reports the plan instead of executing it, so a dry run refuses exactly what a real run would:

DRY RUN, nothing was delivered: would click push button 'Send' at (1204, 880) in signal

Nothing reaches the desktop: not the click, not the keystroke, not even the window focus that normally precedes typing. Enforced twice, once at each tool and once at the input path itself, so a code path nobody thought of fails loudly rather than quietly acting during a simulation. The agent is told dry run is on, so it reports a plan instead of retrying an action that "did not work". The scope is the agent's actions, not the server's own startup:HYPRUSE_MARK, if you set it, still installs its window rule when the server starts.

hypruse replay <journal>re-issues a journal's actions through the same tool functions, so the same guards apply to the replay. It prints the plan and stops there unless you pass--execute. Before it takes the seat it refuses outright, rather than failing halfway and leaving your desktop part-way through someone else's plan, when: an action was recorded by a newer hypruse, a recorded window no longer exists (--skip-missingruns the rest), typed text was recorded as a digest, the entry is aclick_ui(mark=N)whose numbering died with the session that drew it, the entry is a clipboard write andHYPRUSE_CLIPBOARDis not set, orHYPRUSE_READONLYorHYPRUSE_DRYRUNis set. It paces itself from the recorded timing, capped by--max-gapand scaled by--speed, and its own actions are recorded and marked, so replaying the same file again runs the original plan rather than the plan plus the replay of it.

The honest limit is window addresses: they are heap pointers, so yesterday's journal mostly names windows that are gone, and an address can even be reused by a different window later, which no pre-flight can catch. Replay is for re-running a flow on a desktop that still looks like the one recorded.

Dry run and replay compose in the obvious direction: let the agent work withHYPRUSE_DRYRUN=1, read the journal, and replay it with--executeonce the plan is one you like.

Measured on a live session (Hyprland 0.55, 1080p, 20 windows):desktop~20 ms (one batchedhyprctlcall), workspace/window dispatch ~10-20 ms, full-monitor screenshot ~65 ms (fast JPEG default; ~800 ms if you ask for lossless PNG, grim's zlib path dominates), region/zoom captures well under that. If tool callsfeelslow, it is almost certainly the MCPapproval promptin front of each call, not the server. Allowlist the tools you trust and the latency disappears. Claude Code (.claude/settings.json):

{ "permissions": { "allow": [ "mcp__hypruse__desktop", "mcp__hypruse__screenshot", "mcp__hypruse__hypr" // add pointer/keyboard/launch once you trust your workflows ] } }

Everything speaks Hyprland's global logical coordinates, the spacehyprctl cursorposand windowatuse. Screenshots are pixel-space; each capture returnsgeometryandscalesoglobal = origin + pixel / scale. On scale 1.0 monitors (most setups) image pixelsareglobal coordinates.

Thezoomtool does the precision arithmetic for the agent: give it an estimated global point and it captures a native-resolution box around it, clamped to the screen (or to a window), with the same metadata contract. That two-step loop, estimate on the full view then re-estimate on the zoom, is theresearch-backedway to hit small controls.

Captures default to JPEG q90: on a 1080p frame that is roughly 12x faster to encode than PNG (grim's zlib path dominates capture time, measured ~65 ms vs ~800 ms) and 3-4x smaller, while full-res q90 reads UI text well. Passlossless=truefor exact pixels (PNG). In image mode, captures also fit the host's result-size limit (Claude Desktop caps tool results at 1 MB) by degrading quality before resolution, since grim's downscale filter is slower than a full-res capture, and cap the long edge atHYPRUSE_MAX_IMAGE_EDGEpixels (default 1568) so the host never downscales the image under the model. The applied scale is folded into the returned metadata, so coordinate mapping stays exact; tune withHYPRUSE_MAX_IMAGE_BYTES, or passscalefor a deliberate zoom-out.

By default the screenshot tool writes the image under$XDG_RUNTIME_DIR/hypruse/and returns its path; MCP hosts with a file reader (Claude Code'sRead) render it natively. This default exists because some hosts (including Claude Code 2.1.x) serialize inline MCP image blocks to base64 text the model cannot see.HYPRUSE_SCREENSHOT_MODE=imageswitches to inline image content blocks for hosts that render them correctly.

uv sync --group dev uv run pytest # unit tests, no compositor needed uv run pytest -m e2e --override-ini addopts= # live seat-safe checks uv run python scripts/e2e_input.py # supervised: takes the seat ~10s

The input e2e is deliberately manual: it borrows your cursor and keyboard, counts down, proves click/scroll/type delivery by reading the target terminal's screen back over kitty remote control, and restores your focus.

Grounded in measured hot-path latencies and the finding that LLM calls are 76 to 96% of computer-use task latency (OSWorld-Human), so cutting round-trips beats shaving milliseconds. The round-trip work that framing motivated has largely shipped:sequence, act-and-observethen=(includingthen='ui'), and the accessibility-tree tools (ui,marks,click_ui) that target controls by name with no screenshot. What remains:

- In-process wlr-screencopy over the raw wire (as input already works): drop grim's fork floor from small captures and add damage-tracked wait-for-stable that returns the instant the screen settles.

- Semantic screen diff: after an action, return only what changed (window topology from the event stream, or a bounded changed-region crop) instead of a full frame. The socket2 event expansion behindwait_foralready tracks most of the topology; the missing piece is folding it into a post-action delta.

- Wider accessibility coverage: close the gaps theuiandmarkstools hit today, chiefly GTK's newer combo boxes that publish neither their text nor selection (so a rendered dropdown value still needs a screenshot), plus AT-SPI value-change events sothen='ui'can report a control settling without a poll.
- Notifications: read recent desktop-notification content and history, not just wait for the popup to appear (wait_foralready matcheslayer_openon the notification namespace).

- recordtool: a scoped GIF or mp4 of the agent driving the desktop, via wf-recorder (a wlroots-family binary like grim), a visual companion to thejournal.

- sway / niri support: the wire client already speaks the wlr protocols; what remains is an IPC layer alongsidehyprctl.py(contributions welcome).
- Headless end-to-end tests in CI: needs a QEMU virtio-gpu VM, since Hyprland's aquamarine backend requires a real GPU render node that hosted runners lack.

- Zoom-loop precision benchmark: measure click accuracy of the coarse-to-fine loop against known targets.
- End-to-end task-success benchmark on Hyprland (OSWorld-style): the deferred bigger sibling of the zoom-loop microbenchmark, scoring full multi-step tasks through the real MCP surface so the a11y-versus-vision and round-trip work is judged on task completion, not latency alone.

hypruse ships no OCR engine; its universal precision mechanism is the coarse-to-fine zoom loop: screenshot a window, re-capture the target region at native resolution, click through the exact coordinate mapping. That choice follows what the GUI-agents field converged on. Anthropic's computer use grounds clicks from raw pixels and ships azoomaction as the documented fix for small text; its troubleshooting guidance for near-miss clicks prescribes zooming and region cropping, never OCR [1]. OpenAI's CUA is likewise pure pixel grounding under resolution discipline, with no OCR layer at all [2]. Zoom is also the measured lever: training-free iterative zooming roughly doubles high-resolution grounding accuracy (OS-Atlas-7B, 18.9 → 49.7 on ScreenSpot-Pro) [3], and the benchmark's official harness implements a dozen grounding-model adapters plus four zoom/crop strategies, but zero OCR baselines [4]. Vision-only agents match or beat agents that additionally consume HTML or accessibility trees [5], substrates Wayland doesn't guarantee anyway, and state-of-the-art native agents run from screenshots alone [7]. OCR was rejected because it is blind to icons, the element class every grounding model handles worst (SeeClick: 30-52% on icons vs 56-78% on text) [6]; where OCR survives in modern stacks it is a text-disambiguation sidecar, not the targeting mechanism [8].

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.