Adoption that depends on somebody remembering a per-machine install is adoption that stops at the person who set it up. winwright ships as a Claude Code plugin: two commands in the repository wire it, every clone is wired, and nothing is added to any path.
None of this is a defect in the tools it names. Every one of them is what a general-purpose automation library does to somebody who has to answer, at the end of a run, whether the thing they were checking was actually checked.
Assert.True(found is not null)DEGRADED (exit 2) - 1 unchecked (all the desk's)2 — a number CI can act on without anybody reading a word of the output.FindFirst(TreeScope.Children, new AndCondition(...))Window#main > Pane > Button#saveSetForegroundWindow(h); MoveTo(x, y); Click()Act.Invoke(subject)Assert.Equal("Relatório mensal", label)"covers": "report.labels"CopyFromScreen(bounds); bmp.Save(path)1 window(s) stand over 376x166 at 62,90, taking 15360 of its 62416 pixel(s): 'Update available' (pid 14820) over 240x64 at 194,92// twenty-seven copies of one runnerreport.cases.jsonWhich is the difference between a refusal and a guess: an agent that writes a case field by field is corrected at insertion, where the fix costs a retry, rather than at run time, where it costs a red run somebody has to read.
The split is the whole reason the last one is a separate tool. Whether a file parses is a claim nothing about the machine can change; whether it passed is not — and a desk that cannot observe answers a hole, naming which condition is missing, rather than a red.
Run once, in the repository that drives the application:
claude plugin marketplace add alegauss/winwright --scope projectclaude plugin install winwright@alegauss --scope projectBoth write into that repository's .claude/settings.json, so committing that file wires every clone. The server and the guard are .NET processes the plugin launches, so they are built once — dotnet build -c Release in the plugin's own clone. Skip it and you are told, not left guessing: each is wired through a launcher that looks for its assembly, and where there is none it writes the missing surface and the build command to stderr and exits 1. Never 2 — denying every write because a build is missing would put the guard in front of everything instead of in front of a harness script.
A hand-written harness script is always available and always faster in the moment, and that is exactly how a 2,732-line one happens. So the plugin registers a PreToolUse hook: a write whose content names the engine's acting, locating or asserting namespaces is denied, and the refusal names the case file and the tool that replace it. It arrives before the work rather than after it — the difference between being asked to write the other thing and being asked to delete what you just wrote.
It loads when a window is in play rather than on every turn, and what it says is which loop answers which question — which is the whole of what an agent needs in order to reach the right tool instead of the nearest one.
winwright is the substrate; the intelligence is the caller's. It reads trees, presses controls through their own patterns and assembles a verdict — it does not decide what is worth asserting.
It calls no LLM.
It stores none.
Clicks never become a case.
The tool never writes the test.
It drives the one you are on.
Nothing is measured or sent.