Installing it
There is nothing to install on the machine. winwright is two NuGet packages and, if you drive your application from Claude Code, a plugin that is installed into a repository rather than into a user profile. No path is added, no service runs, and nothing differs by whose desk it is.
Before you start
Section titled “Before you start”- Windows, and a desk you may drive. A run synthesises input and takes the foreground, so a machine somebody is using is a machine that will produce holes rather than results. See running somewhere other than your own desk.
- The .NET 10 SDK. Everything here targets
net10.0-windows. - An application you can start from a path, or one already running that a test may attach to.
The driving half
Section titled “The driving half”-
Make the project that drives the application a project of its own.
It is a
net10.0-windowsproject like any other, and it usually goes in a folder underneath the application. It is not the application, and it is not a folder inside the application’s own project — which is a distinction MSBuild will make for you in the least helpful way possible if you skip the next step.<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0-windows</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup></Project> -
Reference the engine. One package, and no path into anything else.
<PackageReference Include="Winwright" Version="1.0.0" /> -
If your application’s
.csprojsits at the repository root, exclude the folder you just made.<!-- In the application's own project, not in the driving one. --><DefaultItemExcludes>$(DefaultItemExcludes);tests\**</DefaultItemExcludes>Every default glob the SDK applies —
Compile, and underUseWPFalsoPage,ResourceandEmbeddedResource— walks the whole tree below the project file. A project at the repository root therefore reaches the folder you just put beside it, and compiles the driving project’s sources and itsobj\into the application.
The in-app half, and when you do not need it
Section titled “The in-app half, and when you do not need it”Winwright.InApp goes in the application under test, never in the project that drives it,
and it is optional. Every locator, every reading and every pattern act works against an
application that references nothing at all.
<PackageReference Include="Winwright.InApp" Version="1.0.0" />Take it when you want the readings only the process itself can take:
- A render of the application’s own visual tree — which is what turns a capture from a copy of the screen into a picture of a window. A screen copy carries whatever was standing in front of it; a render cannot.
- Whether this process’s idea of the display is trustworthy, in a sentence a report prints. A picture drawn by a system-aware process on a scaled display has a size that does not mean what it says, and nothing else about the file would ever say so.
- A popup’s own tree, including one nobody has clicked. A popup is layered for the shadow it draws, so the soft edge of a screen copy is the desktop behind it — and a closed popup has no window to copy at all.
It needs <UseWPF>true</UseWPF> in the application, and it reports nothing and writes no file
unless the run that started the application asked it to. That is what makes it safe to leave in
a release.
Wiring Claude Code
Section titled “Wiring Claude Code”Two commands, 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. There is no per-machine install and no instruction that differs by whose desk it
is: a clone that has the file has the plugin.
What the tools answer
Section titled “What the tools answer”The plugin wires an MCP server, so the case format arrives as a schema rather than as prose an agent loaded once and then typed a key out of:
| Tool | What it answers |
|---|---|
winwright_format |
Every field of a file, a case, a step and a fixture, whether it is required, and the closed list of what it accepts. |
winwright_vocabulary |
Every act, what each one needs said beside it, and whether the engine may repeat it. |
winwright_check |
A case read back before the file exists. Its input schema is the loader’s schema, so a misspelled key is not something the caller can send. |
winwright_run |
The cases a selection asks for, run: the verdict, a line per case that ran and per case it left alone, the exit code, and what outlived the run. |
It also registers a PreToolUse hook that denies a write whose content names
Winwright.Acting, Winwright.Locating or Winwright.Asserting, and names the case file and
winwright_check that replace it. 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. The refusal arrives
before the work rather than after it.
The one step the two commands do not cover
Section titled “The one step the two commands do not cover”The server and the guard are .NET processes the plugin launches, so they need building once —
dotnet build -c Release in the plugin’s own clone.
Skip it and you are told rather than left guessing: both are wired through a launcher that looks for its assembly, Release first and then Debug, and where there is none it writes the missing surface and the build command to stderr and exits 1. It exits 1 and never 2, because denying every write over a missing build would put the guard in front of everything instead of in front of a harness script.
Checking that it took
Section titled “Checking that it took”There is no --version to run, because there is no executable to run it on. What you check is
that the driving project builds and resolves:
dotnet build path\to\driving\Driving.csprojA restore that reached the packages and a build that compiled against them is the whole of the installation. The next thing to write is the four calls that load and run a case, which is where your first case starts.