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.1.9" />The version is asked of nuget.org when the page loads, so it is what you can restore rather than what this site was built with. With JavaScript off you get the version of the last deploy, which may be a release behind.
-
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.1.9" />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@winwright --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.
Where the server and the guard come from
Section titled “Where the server and the guard come from”The server and the guard are .NET processes the plugin launches, and each release carries them
built. The first session after an install or an update downloads them for the version the
plugin declares from its GitHub Release, checks the zip against the SHA-256 published beside it,
and unpacks it into %LOCALAPPDATA%\winwright\plugin\<version>, outside the plugin’s clone. An
update brings a new version, so the next session downloads again and removes the old folder.
The machine needs the Windows Desktop Runtime 10, not the SDK: the binaries are framework-dependent and the engine uses WPF. The SDK carries the runtime, so a machine that builds .NET 10 already has it.
- Behind a proxy or offline, set
WINWRIGHT_RELEASESto a folder or a mirror holding the release’swinwright-plugin-<version>-win-x64.zipand its.sha256. - Working on winwright itself, build the plugin’s clone with
dotnet build -c Release. A local build, Release first and then Debug, is always preferred over a downloaded one.
The guard never downloads, because it runs on every write under a ten-second timeout: it reads the folder the server filled. Where nothing is there, because the download failed, the runtime is missing, or the first session has only just started, each launcher writes the missing surface and the way round it to stderr and exits 1. It exits 1 and never 2, because denying every write over a missing binary 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.