desktop-controller — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited desktop-controller (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Automate any Windows desktop application using a combination of Win32 API, Playwright (for web/Electron apps), and screenshot-based visual feedback. Inspired by OpenAI's playwright-interactive Codex skill, adapted for Claude Code.
CRITICAL DECISION: Before any action, determine which engine to use:
User Request → Is it a web/browser/Electron app?
├── YES → Playwright (CSS selectors, instant, precise)
│ Examples: email webmail, Slack, browser tasks, web forms
│ ✅ page.click('#compose') — instant, 100% accurate
│
└── NO → Win32 API (native desktop automation)
Examples: WeChat, QQ, DingTalk, Notepad, File Explorer
✅ SetCursorPos + mouse_event — works for any native appWhy this matters: For the Tsinghua email (a web app), using Playwright CSS selectors is 10-50x faster than screenshot→analyze→click. OpenAI's playwright-interactive proved this: DOM selectors beat vision-based coordinate guessing every time for web content.
| Engine | Speed | Precision | Use For |
|---|---|---|---|
| Playwright | <1s per action | 100% (DOM selector) | Web apps, Electron apps, browser tasks |
| Win32 API | 2-3s per action | 95% (coordinate-based) | Native desktop apps (WeChat, QQ, etc.) |
| Screenshot+Vision | 10-15s per action | 80% (AI guessing) | Last resort / unknown UI |
┌──────────────────────────────────────────────────┐
│ Desktop Controller Skill │
├──────────────┬──────────────┬────────────────────┤
│ Win32 Layer │ Playwright │ Visual Feedback │
│ (Native) │ (Web/Electron)│ (Screenshot+AI) │
├──────────────┼──────────────┼────────────────────┤
│ FindWindow │ Browser │ CaptureScreen │
│ SendKeys │ Page.click │ CaptureWindow │
│ SetCursorPos │ Page.fill │ → Claude Vision │
│ mouse_event │ Page.goto │ → Verify State │
│ Clipboard │ Locator │ → Decide Next Step │
└──────────────┴──────────────┴────────────────────┘Best for: WeChat, QQ, DingTalk, WeCom, Feishu, Notepad, File Explorer, etc.
How it works:
Best for: Email webmail, Slack, Discord, Teams (web), VS Code, Notion, any browser-based or Electron app.
How it works:
page.click(), page.fill(), page.goto() for all interactions⚠️ CRITICAL RULE: NEVER kill/restart Chrome (`taskkill /IM chrome.exe`). This destroys all user's open tabs!
The user's Chrome is configured to ALWAYS start with CDP enabled:
--remote-debugging-port=9222 flagsStep 1: Check if CDP is available (ALWAYS do this first)
curl --noproxy localhost -s http://localhost:9222/json/versionIf CDP responds → Great! Go to Step 2.
If CDP does NOT respond (Chrome running without CDP, or Chrome not running):
"Chrome 没有开启调试端口。请关闭 Chrome,然后双击桌面的 Chrome-CDP.bat 重新打开。"
MSYS_NO_PATHCONV=1 "/c/Program Files/Google/Chrome/Application/chrome.exe" \
--remote-debugging-port=9222 --user-data-dir="C:\\ChromeCDP" \
--profile-directory=Default --restore-last-session > /dev/null 2>&1 &Step 2: Connect with Playwright
// CRITICAL: Must bypass proxy for localhost
process.env.NO_PROXY = 'localhost,127.0.0.1';
const { chromium } = require('playwright');
const browser = await chromium.connectOverCDP('http://localhost:9222', { timeout: 60000 });
const ctx = browser.contexts()[0];
const page = await ctx.newPage(); // or ctx.pages()[0] for existing tabStep 3: Automate anything
await page.goto('https://example.com');
await page.click('#button');
await page.fill('input[name="email"]', '[email protected]');
await page.screenshot({ path: 'verify.png' });Coremail (Tsinghua email mails.tsinghua.edu.cn):
/^写\s*信$/ in page.evaluatekeyboard.type(email) → Enterinput[name="subject"] — set .value + dispatchEvent(new Event('input'))document.body.contentEditable === 'true' → set innerHTMLj-tbl-sendGeneral pattern for complex web apps:
// When CSS selectors fail, use page.evaluate to walk the DOM
const result = await page.evaluate(() => {
const els = document.querySelectorAll('span, button, div');
for (const el of els) {
if (/^发\s*送$/.test(el.textContent?.trim()) && el.offsetParent !== null) {
el.click();
return true;
}
}
return false;
});process.exit(0) insteadNO_PROXY=localhost,127.0.0.1 before Node.js commandsD:\cc-workspace\node_modules\playwright (already installed)Chrome-CDP.bat, NEVER force-kill| App | Process Name | Mode | Search Key | Notes |
|---|---|---|---|---|
| Weixin | Win32 | Ctrl+F | Tested and verified | |
| WeCom | WXWork | Win32 | Ctrl+F | Enterprise WeChat |
| DingTalk | DingTalk | Win32 | Ctrl+K | Alibaba's chat |
| Feishu/Lark | Feishu | Win32 | Ctrl+K | ByteDance's chat |
| Win32 | Ctrl+F | Tencent QQ | ||
| Telegram | Telegram | Win32 | Ctrl+K | - |
| Slack | slack | Playwright | Ctrl+K | Electron app |
| Teams | ms-teams | Win32/Playwright | Ctrl+E | - |
| VS Code | Code | Playwright | Ctrl+P | Electron app |
| Any browser | chrome/msedge/firefox | Playwright | - | Via CDP |
# Use the app registry to determine process name and mode
python scripts/app_registry.py identify "WeChat"
# Output: { "process": "Weixin", "mode": "win32", "search_key": "Ctrl+F" }For Win32 native apps:
# Send a message to a contact in a chat app
python scripts/desktop_control.py send-message --app weixin --contact "张三" --message "你好"
# Click at specific coordinates
python scripts/desktop_control.py click --app weixin --x 500 --y 400
# Type text into the focused app
python scripts/desktop_control.py type --app weixin --text "Hello World"
# Take a screenshot of a specific app window
python scripts/desktop_control.py screenshot --app weixin --output screenshot.pngFor Playwright/Electron apps:
# Interact with a web page
python scripts/desktop_control.py web-click --url "http://localhost:3000" --selector "#submit-btn"
# Fill a form field
python scripts/desktop_control.py web-fill --url "http://localhost:3000" --selector "input[name=email]" --text "[email protected]"After every action, optionally capture a screenshot and analyze it to verify the action succeeded. This is the key insight from OpenAI's playwright-interactive: always verify visually.
# Take screenshot and return for Claude to analyze
python scripts/desktop_control.py screenshot --app weixin --output verify.png
# Claude reads the screenshot and decides next stepsThe universal pattern for sending messages in any chat app:
1. FindProcess(process_name) → window handle
2. ShowWindow(handle, SW_RESTORE) + SetForegroundWindow(handle)
3. SendKeys(search_shortcut) # Open search (Ctrl+F, Ctrl+K, etc.)
4. Clipboard.SetText(contact_name) # Set contact name
5. SendKeys(Ctrl+V) # Paste contact name
6. Sleep(2000) # Wait for search results
7. SendKeys(Enter) # Select contact
8. Sleep(2500) # Wait for chat to load
9. ClickAt(input_area_x, input_area_y) # CRITICAL: Mouse click to focus input
10. Clipboard.SetText(message) # Set message
11. SendKeys(Ctrl+V) # Paste message
12. SendKeys(Enter) # SendCritical insight: After search+Enter selects a contact, the message input area does NOT automatically get keyboard focus. You MUST use mouse click automation to click on the input area. This was discovered empirically with WeChat and applies to most chat apps.
while not task_complete:
1. Execute action (click, type, etc.)
2. Take screenshot
3. Analyze screenshot (Claude vision)
4. Determine if action succeeded
5. If failed → adjust and retry
6. If succeeded → next actionFor Chinese/CJK text, always use Unicode code points to avoid encoding issues:
def text_to_char_codes(text):
return ",".join(str(ord(c)) for c in text)
# In PowerShell, reconstruct from codes:
# [string]::new([char[]]@(20320,22909)) → "你好"function Set-ClipboardSafe($text) {
for ($i = 0; $i -lt 5; $i++) {
try {
[System.Windows.Forms.Clipboard]::Clear()
Start-Sleep -Milliseconds 100
[System.Windows.Forms.Clipboard]::SetText($text)
return $true
} catch {
Start-Sleep -Milliseconds 300
}
}
return $false
}Click coordinates are calculated relative to the app window:
# Get window rect
$rect = New-Object Win32.RECT
GetWindowRect($hwnd, [ref]$rect)
$winW = $rect.Right - $rect.Left
$winH = $rect.Bottom - $rect.Top
# Chat app input area is typically at bottom-center
$inputX = $rect.Left + [int]($winW * 0.65)
$inputY = $rect.Bottom - [int]($winH * 0.12)| Operation | Recommended Delay |
|---|---|
| After ShowWindow/SetForeground | 1000ms |
| After opening search | 600ms |
| After pasting search text | 2000ms |
| After selecting contact (Enter) | 2500ms |
| After clicking input area | 1000ms |
| After pasting message | 800ms |
| Between clipboard Clear and Set | 100ms |
| Issue | Solution |
|---|---|
| Window not found | Check process name with Get-Process |
| Window won't come to foreground | App may be in system tray; activate manually first |
| Clipboard errors | Use retry loop; close other clipboard-heavy apps |
| Wrong contact selected | Use more specific search term (full name/remark) |
| Input area not focused | Adjust click coordinates for the specific app |
| Chinese text garbled | Use Unicode char code arrays instead of literal strings |
| Screenshot is black | Some apps use hardware acceleration; try with software rendering |
To add support for a new app:
Get-Process | Where-Object { $_.MainWindowTitle -like "*AppName*" }scripts/app_registry.py~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.