はじめに
Claude Codeでデスクトップアプリを作っていると、こんな終わり方によく出会います。
実装しました。お手数ですが、アプリで動作確認をお願いします。
単体テストも型チェックも緑です。それでも実際にアプリを起動すると、ボタンが反応しない、文言が変わっていない、画面が真っ白、ということがあります。Webアプリなら Playwright MCP などでAIに確認させる方法が広まってきました。一方、デスクトップアプリで「作る → 実機で確かめる → 直す」をAIに回させる実例は、まだ多くありません。
筆者は Tauri 製の Markdown エディタ MDium を Claude Code で開発しています。Windows のデスクトップアプリをAIに操作させる MCP サーバー winuse も自作しました。最初は「winuse で確認して」と毎回指示していました。ですが、指示を忘れれば確認は行われません。
そこで Claude Code の Hooks を使い、UIを変えたら、winuse で実機確認するまで「完了」と言えない・コミットできない ようにするプラグイン winuse-gate を作りました。この記事では、その導入手順と、MDium で試したときの実際の動きを紹介します。
対象は Windows のデスクトップアプリです(winuse が Windows 専用のため)。UI Automation で読める Win32 / WPF / WinForms、WebView2 を使う Tauri / Electron 系、Java Swing のアプリを操作できます。
なぜ computer use ではなく winuse なのか
画面を操作させるだけなら computer use(スクリーンショットを撮って座標をクリックする方式)でもできます。違うのは「確認したことを機械的に判定できるか」です。
| computer use | winuse | |
|---|---|---|
| 画面の読み方 | スクリーンショット(画像) | UI Automation のツリー、WebView2 の DOM、Java Swing の部品(テキスト) |
| 操作の指定 | 画面の座標 | 部品の参照(u12 など)や DOM セレクタ。座標も使える |
| 確認の結果 | モデルが画像を見た感想 |
assert_text などが passed: true/false を返す |
| 繰り返し | 毎回モデルが操作する | 手順をシナリオ(JSON)に保存し、AIなしで再生できる |
Hooks で「確認したか」を判定するには、フックが読める証拠が必要です。スクリーンショットとモデルの感想からは、「確認した」のか「眺めただけ」なのかを区別できません。winuse なら passed: true を数えるだけで判定できます。
部品のツリーはテキストなので、毎回画像を送るより速く、トークンも安く済みます。部品を名前で指定するので、ウィンドウの位置や解像度が変わっても壊れにくいという利点もあります。部品を公開していない画面(VMのコンソールなど)向けには、winuse にもスクリーンショットで位置を決める仕組みがあります。
全体像
-
winuse:アプリを操作し、
assert_*で結果を確かめる MCP サーバー -
winuse-gate:Claude Code のプラグイン。次の4種類のフックとスキル1つで構成されます
- セッション開始時(SessionStart):開始時点の未コミットの変更を記録する。もともとあった利用者の変更は対象外にするため
-
ツール実行後(PostToolUse):winuse の
assert_*が通ったら、そのときのUIファイルの内容を「確認済み」として記録する - ターン終了時(Stop):このセッションで変えたUIファイルが未確認なら、ターンの終わりを1回止める
-
コミット前(PreToolUse):未確認のUIファイルを含む
git commitを拒否する -
スキル
winuse-e2e:何をどこまで確認すればよいかを Claude に教える
導入手順
1. winuse を入れる
- .NET 10 ランタイムを入れます。
-
Releases から
winuse-<版>-win-x64.zipを取ってきて、適当なフォルダに展開します。 - Claude Code に MCP サーバーとして登録します。
claude mcp add winuse -- C:\tools\winuse\winuse.exe --mcp
assert_* やクリック、入力、部品ツリーの取得は、これだけで使えます。意図を言葉で渡して操作させる act / find / check を使う場合だけ、判定用モデルのキー(JEV_API_KEY、または CLOUDFLARE_ACCOUNT_ID と CLOUDFLARE_API_TOKEN)をユーザー環境変数に登録します。winuse doctor で環境を確認できます。
ファイル系のツール(read_text など)を使うなら、操作してよいフォルダを WINUSE_ALLOWED_ROOTS(; 区切り)で指定します。
2. プラグインを入れる
Claude Code のセッションで次を実行します。
/plugin marketplace add straygizmo/winuse
/plugin install winuse-gate@winuse
フックの実行に、Windows 標準の PowerShell 5.1 と git を使います。筆者は Claude Code 2.1.310 で確認しました。
3. プロジェクトに設定ファイルを置く
対象リポジトリのルートに .claude/winuse-gate.json を置き、コミットします。MDium では次のようにしています(launch は一部省略)。
{
"ui": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.css", "src-tauri/src/**/*.rs", "src/shared/i18n/locales/**/*.json"],
"exclude": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**"],
"launch": "Start the dev app with `npm run tauri dev` from the repository root. The window title matches ^MDium. The UI is WebView2: inputs often have no accessible name, so take element rects from get_ui_tree and click by coordinates."
}
-
ui(必須):「画面に関わる」とみなすファイルの glob パターン。文言だけの修正も対象にするため、i18n のファイルも入れています。 -
exclude:uiから除くパターン。テストファイルなど。 -
launch:アプリの起動方法やウィンドウの見つけ方を、Claude 向けに自由に書きます。
このファイルが無いリポジトリでは、ゲートは何もしません。そのことはセッション開始時に Claude に一言伝わります。
.gitignore で .claude をまるごと無視していると、この設定ファイルをコミットできません。新しい worktree にも入りません。MDium も最初はこれに引っかかりました。次のように書き換えてください。
.claude/*
!.claude/winuse-gate.json
これで準備は完了です。フックはセッション開始時に読み込まれるので、新しいセッションを開いてください。
実際の動き(MDium での試運転)
ステータスバーの区切り文字(0 L / 0 C の /)を変える小さな修正で試しました。
未確認のまま「動作確認をお願いします」で終わろうとすると
Claude がわざとこう書いてターンを終えようとすると、
ステータスバーの区切り文字を変更しました。お手数ですが、アプリでスモークテストをお願いします。
Stop フックが止めて、次の指示を返します。
winuse-gate: 1 UI file(s) changed in this session have not been verified with winuse:
- src/app/components/StatusBar.tsx
Look at your last message:
- If it reports the work as done, or asks the user to run, check or smoke-test it: do not hand that to the user. Verify it yourself with winuse (...) until at least one winuse assert_* call passes on the changed behavior, then report again. Keep the check proportional to the change.
- If it is a genuine question for the user, or a progress or blocked report, you may end the turn as it is.
- Ask the user only for subjective review (look and feel), only after the functional assertions pass, and attach a winuse screenshot.
ポイントは、「完了報告」と「動作確認の依頼」を同じ扱いにしていることです。機能面の確認は Claude 自身の仕事にします。利用者に頼んでよいのは、余白の感じなど好みが入る評価だけで、それもアサーションが通った後に、スクリーンショットを添えて頼む決まりです。
未確認のままコミットしようとすると
PreToolUse:Bash hook error: winuse-gate: this commit includes 1 UI file(s) changed in this session that have not been verified with winuse:
- src/app/components/StatusBar.tsx
Verify the change with winuse first (follow the winuse-e2e skill) until at least one winuse assert_* call passes, then commit again.
git commit -am、cd src && git commit(Bash)、Set-Location src; git commit(PowerShell)のどれも拒否されました。
winuse で確認すると通る
止められた Claude は、自分で npm run tauri dev を起動します。ウィンドウを待ち、get_ui_tree で区切り文字の部品を見つけて確認しました。
mcp__winuse__assert_text {"ref": "u215", "expected": "L · ", "mode": "contains"}
→ {"step_id":"s10","passed":true,"actual":" L · ","reason":null}
この1回のアサーションで「確認済み」になり、ターンを普通に終えられました。文言の修正なら assert_text 1回、新しいダイアログならその部品を assert_exists、というように、確認の量は変更の大きさに合わせるようスキルで指示しています。
本当の質問なら止まれる
未確認の変更がある状態で Claude が「AとBのどちらにしますか?」と聞いて終わろうとすると、1回は止められます。フックは発言の中身を読まないからです。ただ、指示文に「本当の質問ならそのまま終えてよい」とあるので、Claude はそのまま質問で終わり直し、2回目は通ります。
仕組みの要点
「確認済み」は時刻ではなくファイルの内容で判定する
アサーションが通った時点の、各UIファイルの内容のハッシュ(git の blob id)を記録します。現在の内容がそれと一致すれば確認済みです。確認後にもう一度編集すれば自動的に未確認に戻ります。git add やコミットをしただけでは状態は変わりません。
「このセッションで変えたファイル」だけを見る
セッション開始時に、その時点の未コミットの変更を基準として記録します。利用者が手で編集していたファイルや、別のセッションの残りは、Claude がさらに手を加えない限り対象外です。worktree など別のリポジトリでの編集も、Edit / Write の実行のたびに非同期で記録して対象に含めます。
止めるのは「同じ変更に1回だけ」
「小さな修正なのに何度も止められて、うっとうしいから外したい」とならないことを最優先にしました。同じ内容の未確認の変更に対して、ブロックは1回だけです。どの発言で終わったかの判断は、止められた Claude 自身に任せています。
コミット前のゲートが最後の砦
ターン終了時のゲートは1回しか止めないので、すり抜ける余地があります(次の節)。コミット前のゲートは、未確認のUIファイルがコミットに含まれる限り毎回拒否します。本当に自動で確認できないときだけ、コミットメッセージに winuse-gate: skip <理由> を書けば通ります。理由は git の履歴に残ります。
制限と割り切り
- ブロック直後に言い直せば通る:止められた直後に Claude が何もせず「完了しました」と言い直すと、ターンは終わります。試運転ではわざと試して、実際に通りました。塞ぐには別のモデルに発言を分類させる必要があり、コストと待ち時間が増えます。運用で甘さが目立ったら検討する、という判断にしました。
- 確認の十分さは保証しない:ゲートが見るのは「アサーションが1件以上通ったか」だけです。的外れなアサーションでも通ってしまいます。
- シェルコマンドだけでの編集は拾わない:別リポジトリでの編集の検出は、Edit / Write 系のツールによる編集だけが対象です。
- Windows 専用です。社内PCなどで PowerShell スクリプトの実行が禁止されていると、ゲートは無効になります(エラーを出すだけで作業は止めません)。
まとめ
- デスクトップアプリでも、winuse を使えば Claude に実機で操作・確認させられます。
- 「確認させる」を指示や CLAUDE.md に頼らず、Hooks で強制できます。鍵は、フックが読める証拠(
assert_*のpassed: true)を残すことです。 - うっとうしくないこと(同じ変更には1回だけ、確認は変更に見合った量だけ)と、すり抜けにくいこと(コミット前は毎回)のバランスを取りました。
winuse と winuse-gate は GitHub で公開しています。デスクトップアプリを Claude Code で作っている方は、ぜひ試してみてください。