WPF Driver Skill
Overview
This skill enables Claude Code to autonomously control WPF applications through the WpfPilot.CLI daemon process. The daemon uses DLL injection to attach to running WPF applications and provides a JSON-based protocol for UI inspection and interaction.
Prerequisites
The WpfPilot.CLI.exe binary must be built and available in the output directory:
WpfPilot.CLI\bin\Release\net9.0-windows\WpfPilot.CLI.exe
Starting the Daemon
Option 1: Attach to Running Process by Name (Recommended)
Use this when the WPF application is already running and you know its process name:
WpfPilot.CLI.exe attach --name MyApp.exe
This is the recommended method as it automatically finds the correct process ID.
Option 2: Attach to Running Process by PID
Use this when you need to attach to a specific process by ID:
WpfPilot.CLI.exe start --pid 12345
Option 3: Launch New Application
Use this to launch a WPF application and attach automatically:
WpfPilot.CLI.exe launch --path "C:\Path\To\App.exe"
Daemon Lifecycle
- •Startup: The daemon initializes the AppDriver (DLL injection takes 2-5 seconds)
- •Ready Signal: When ready, it outputs:
{"status": "ready"} - •REPL Loop: Enter the JSON command processing loop
- •Shutdown: Terminate the process or close stdin to disconnect
Communication Protocol
Sending Commands
Send JSON commands to the daemon's STDIN. Each command must be on a single line.
Node.js Example:
child.stdin.write('{"cmd": "inspect"}\n');
child.stdin.write('{"cmd": "click", "id": "SubmitBtn"}\n');
Python Example:
process.stdin.write(b'{"cmd": "inspect"}\n')
process.stdin.write(b'{"cmd": "click", "id": "SubmitBtn"}\n')
PowerShell Example:
$process.StandardInput.WriteLine('{"cmd": "inspect"}')
$process.StandardInput.WriteLine('{"cmd": "click", "id": "SubmitBtn"}')
Reading Responses
Read responses from the daemon's STDOUT. Each response is a single JSON object followed by a newline.
Node.js Example:
child.stdout.on('data', (data) => {
const response = JSON.parse(data.toString());
console.log(response);
});
JSON Protocol Commands
inspect
Get a filtered list of visible UI elements. Non-visible elements and layout containers (unless they have AutomationId) are automatically excluded.
{"cmd": "inspect"}
Response:
{
"status": "success",
"elements": [
{
"ControlType": "Button",
"ClassName": "System.Windows.Controls.Button",
"AutomationId": "SubmitBtn",
"Name": "Submit",
"IsEnabled": true,
"IsOffscreen": false,
"BoundingRectangle": "100,200,80,30"
}
]
}
click
Click an element. Specify either id (AutomationId) or name (Name property).
{"cmd": "click", "id": "SubmitBtn"}
or
{"cmd": "click", "name": "Submit"}
Response:
{"status": "success"}
Error Response:
{"status": "error", "message": "Element not found: id=SubmitBtn, name="}
type
Type text into a text input element (TextBox, ComboBox, etc.).
{"cmd": "type", "id": "UsernameBox", "text": "john.doe"}
or
{"cmd": "type", "name": "Username", "text": "john.doe"}
Response:
{"status": "success"}
Error Response:
{"status": "error", "message": "Element not found: id=UsernameBox, name="}
wait
Wait for a specified number of milliseconds. Use sparingly - prefer to verify UI state explicitly.
{"cmd": "wait", "ms": 1000}
Complete Example: Node.js
const { spawn } = require('child_process');
// Start the daemon
const child = spawn('WpfPilot.CLI.exe', ['start', '--pid', '12345']);
let buffer = '';
child.stdout.on('data', (data) => {
buffer += data.toString();
// Process complete JSON objects
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
console.log('Response:', response);
} catch (e) {
console.error('Failed to parse:', line);
}
}
});
// Send commands after ready signal
setTimeout(() => {
child.stdin.write('{"cmd": "inspect"}\n');
}, 3000);
// Clean up
child.on('close', (code) => {
console.log(`Daemon exited with code ${code}`);
});
Complete Example: PowerShell
# Start the daemon
$process = Start-Process -FilePath "WpfPilot.CLI.exe" -ArgumentList "start","--pid","12345" -PassThru -NoNewWindow -RedirectStandardInput "in.txt" -RedirectStandardOutput "out.txt"
# Write commands to input file
'{"cmd": "inspect"}' | Out-File -FilePath "in.txt" -Encoding UTF8
# Read responses (simplified - in production use async reading)
Get-Content "out.txt" -Wait | ForEach-Object {
if ($_ -match '^{.*}$') {
$response = $_ | ConvertFrom-Json
Write-Host "Response: $($response | ConvertTo-Json -Compress)"
}
}
Error Handling
All error responses follow this format:
{"status": "error", "message": "Error description"}
Always check the status field before processing results.
Troubleshooting
Daemon fails to start
- •Verify the target process is a WPF application
- •Ensure you have permissions to attach to the process
- •Try running as Administrator
Elements not found
- •The element may not be visible (check IsVisible in inspect output)
- •The AutomationId or Name may not match exactly
- •The element may be in a different window/frame
DLL injection timeout
- •WPF applications take time to initialize after launch
- •Add a delay before attaching to newly launched apps
- •The ready signal indicates when injection is complete