Your coding agent edits a component, runs the build, and reports the UI is now "pixel-perfect." It never opened a browser. It's guessing.
I got tired of that, so I built hwatu, a browser designed specifically for the agent verification loop.
The loop
hwatu --headless http://localhost:3000 # open, no focus steal
hwatu wait-load # block until loaded
hwatu eval 'document.title' # DOM-level checks
hwatu shot /tmp/check.png # screenshot
hwatu diff --id 1 --baseline before.png # pixel diff: 97.49% match + heatmap
hwatu close 1
The whole loop, screenshot included, runs in about 87 ms on my laptop. Cheap enough that the agent can verify after every edit instead of claiming success blind.
Design choices
- Headless by default in agent environments. Windows never steal focus while you keep typing. When you want to look, the agent hands you the same live window as a normal browser.
-
No Chromium. One static Rust binary plus your distro's
webkitgtk-6.0. No 170 MB download per project. -
Three protocols. MCP (
hwatu mcp), plain CLI, and a 1-line JSON socket. Works with Claude Code, Cursor, Jcode, or any harness. - Real numbers. The diff command reports an actual match percentage with a heatmap, so "pixel-perfect" becomes a measurable claim.
Measured, not estimated
Every benchmark in the repo was measured on real runs with scripts you can rerun: window spawn medians of 13-16 ms, ~87 ms full verification loop, memory measured via PSS across all WebKit child processes. Methodology in docs/benchmarks.md.
Try it
curl -fsSL https://raw.githubusercontent.com/hongnoul/hwatu/main/scripts/install.sh | bash
hwatu setup # detects Claude Code, Cursor, Jcode, or generic MCP
MIT licensed. Linux (WebKitGTK). Feedback and issues welcome: https://github.com/hongnoul/hwatu
Top comments (3)
Forcing the agent to produce a number instead of an adjective is the right instinct - it is the same reason we make models report confidence against a held-out set instead of saying "looks good". One caveat from doing visual diffs: a single global similarity score hides where the error is. 97.49% can mean a uniform antialiasing difference nobody would notice, or a button in the wrong place with everything else identical. Region-weighted scores, or at least reporting the worst tile rather than the mean, makes the metric much harder to game.
Fully agree, and this bit hwatu's design early: a mean similarity score is trivially gamed by large unchanged backgrounds. So the diff output isn't just the global percentage. It clusters differing cells into bounding boxes and reports them worst-first (regions with the most differing pixels lead), plus an optional heatmap PNG so you can see exactly where the mismatch lives. A moved button shows up as one dense region even when the global score reads 99%+, while uniform antialiasing noise spreads thin and clusters weakly. There's also a per-pixel tolerance knob to keep AA noise out of the regions entirely. Worst-tile-instead-of-mean as the headline number is a fair push though, the global % is definitely the more quotable but less honest of the two.
That separation between a headline metric and a diagnostic metric is the useful part. I would gate on the worst changed region above a minimum area, plus the number of significant regions, while keeping the global score for trend reporting. Otherwise one tolerated glyph can fail the build. Keeping antialiasing tolerance separate from region significance also makes the threshold explainable in review.