本家はこちら

CodexでAppShotを自律発火する

Some visuals are licensed via Canva Pro (includes commercial rights).
Usage complies with Canva’s license terms at the time of use.
License policy: canva.com/policies/content-license-agreement

X
     この記事はプロモーションを含みます

CodexのComputer Useを触っていると、「画面を見て、クリックして、入力する」だけでは足りない場面が出てきます。

今回ひっかかったのは、macOS版CodexのAppShotでした。AppShotは、前面アプリのウィンドウを画像と取得可能なテキストとしてCodexスレッドへ渡す機能です。公式ドキュメントでも、前面ウィンドウをCodexに渡す文脈共有手段として説明されています。

普通に使うなら、人間が両方のCommandキー、または設定したAppShotホットキーを押せばいい。ところが、AIエージェントにGUI操作を自動で任せるなら話が変わります。

アプリを操作する。画面が変わる。そこでまた画面状態を観測し、次の判断をする。このループを作るには、AppShotも必要なタイミングでCodex側から自律的に発火できないといけません。

クリックだけできても、画面が変わったあとに見直せないと、ただの一発芸なんですよね。

Content

なぜAppShotを自律発火したかったのか

AppShotを手動スクリーンショットではなくGUI操作中の再観測手段として使う流れを、白髪の案内キャラクターとノートPC、観測ループのパネルで表した画像。

今回の出発点は、AppShotを単発の手動スクリーンショットとしてではなく、GUI操作中の再観測手段として使いたい、というところにありました。

操作後の画面をもう一度見る必要があった

Computer Useは、Macアプリを操作するためのかなり強い手段です。クリック、キー入力、スクロール、アクセシビリティツリーの確認などができるので、アプリのUI操作をかなり自然に扱えます。

ただし、GUI自動化では「操作」と同じくらい「再観測」が重要です。

たとえばCleanMyMacのスマートケアを自動化するとします。最初にアプリを起動し、トップ画面を見て、スキャンボタンを押す。すると画面は当然変わります。次に必要なのは、変化後の画面をもう一度観測して、次に押すべきボタンや確認すべき状態を判断することです。

このときAppShotが効きます。AppShotは単なるスクリーンショットではなく、取得可能なテキスト情報もCodex側へ渡せます。つまり、Computer Useの「操作」とAppShotの「観測」を組み合わせると、AIエージェントが次の画面状態を踏まえて判断し直せるようになります。

ここが面白いところです。AIエージェントのGUI操作は、クリック精度だけの問題ではありません。見て、動かして、また見る。このサイクルをどれだけ安定して回せるかが実用性を決めます。

この記事のポイント

Computer Useは操作の手段、AppShotは再観測の手段です。両方をつなぐと、AIエージェントは「押したあとに見直して判断する」流れへ近づきます。

Computer UseだけではCmd+Cmdを押せなかった

Computer Useでは左右Commandキーだけの入力が通らない制限を、キーボードと発火しない修飾キーのモチーフで表した画像。

最初は当然、Computer UseでそのままAppShotホットキーを押せば済むと思っていました。ところが、ここで入力レイヤーの制限にぶつかります。

修飾キーだけの入力が制限に当たった

最初に試したのは素直な方法でした。Computer UseでAppShotのホットキー、つまり左右のCommandキーを押せばいいのではないか、という発想です。

しかし、ここで制限に当たりました。Computer Useのキー入力は、通常のキーや修飾キー付きショートカットには使えますが、左右Commandキーだけを同時に押すような「bare modifier」系の入力は弾かれました。

keyPressIncludedNoNonModifierKeys

これは雑に言えば、「修飾キーだけのキー入力は扱わない」という制限です。AppShotのデフォルト操作はまさにそこに当たります。

この制限自体は理解できます。Computer Useの通常キー入力としては、何かしらの非修飾キーを含むショートカットを扱うほうが自然です。問題は、AppShot側のUIが両Commandキーのような修飾キーのみのホットキーを採用していることでした。

つまり、Computer Useから見れば入力できない。AppShotから見ればそれが正規の起動方法。ここに小さな断絶がありました。

この断絶を見落とすと、「権限はあるのに、なぜか自律化できない」というかなり嫌な詰まり方をします。

失敗した経路は捨てる

失敗した検証ルートを整理し、実働確認できた経路だけを残す判断を、分岐した道筋と案内キャラクターで表した画像。

回避策を作るときに重要なのは、試行錯誤の全部を残すことではありません。自律実行に使うなら、失敗した経路を再選択しない形に整理する必要があります。

再利用するなら失敗ルートを残さない

この手の問題で怖いのは、「それっぽく動きそうな経路」を残してしまうことです。

今回も、いくつかの経路を試しました。修飾キー状態の変化として投げる方法、bare modifierを監視するような検証経路などです。しかし、AppShotを実際に発火させる経路としては不採用にしました。

重要なのは、失敗した経路をコメントや予備ルートとして残さなかったことです。

GUI自動化では、たまたま動くように見える回避策があとで事故の種になります。特にエージェントが自律的に再利用するコマンドでは、「どの経路が正本なのか」が曖昧だと、将来のセッションでまた失敗経路へ戻ってしまう。

だから今回は、動かなかった経路をソースから消し、実働確認できた経路だけを残すことにしました。

失敗した検証経路を残すと、将来のCodexがそこへ戻る可能性があります。自律運用に使う helper では、成功経路だけを正本化する方が安全です。

動いた経路はCGEventのkeyDown/keyUpだった

CGEventのkeyDown/keyUpでAppShotホットキーを発火する流れを、低レベル入力の光のパルスとカメラ風モチーフで表した画像。

最終的に採用したのは、対象アプリを前面化したうえで、macOSの低レベル入力イベントとして左右Commandキーを押す経路です。

対象アプリを前面化してから低レベル入力を投げる

最終的に動いたのは、低レベルのCGEventで左右CommandキーのkeyDown/keyUpを投げる方法でした。

流れはシンプルです。ただし、単にキーイベントを投げるのではなく、対象アプリの確認を前段に置きます。

STEP
対象アプリを前面化する

bundle idで対象アプリをactivateし、少し待ってからfrontmost appが想定通りか確認します。

STEP
左右CommandのkeyDown/keyUpを投げる

Swift helperからCGEventを使い、左Command down、右Command down、右Command up、左Command upの順でAppShotホットキーを再現します。

実際のヘルパーは、毎回その場でSwift断片を生成するのではなく、正本コマンドとして置きました。

/Users/suzukimakoto/.codex/bin/codex-appshot-trigger

中身の役割は、wrapper側で対象アプリを前面化して確認し、Swift側で左右CommandのCGEventを投げることです。まずwrapper側の核はこうです。

APP_ID=""
WINDOW_TITLE=""
WAIT_MS="500"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HELPER_SOURCE="$SCRIPT_DIR/codex-appshot-trigger.swift"
HELPER_BIN="$SCRIPT_DIR/.codex-appshot-trigger-helper"

if [[ "$REBUILD_HELPER" == "1" || ! -x "$HELPER_BIN" || "$HELPER_SOURCE" -nt "$HELPER_BIN" ]]; then
  swiftc "$HELPER_SOURCE" -o "$HELPER_BIN"
  chmod +x "$HELPER_BIN"
fi

osascript - "$APP_ID" <<'APPLESCRIPT'
on run argv
  set bundleId to item 1 of argv
  tell application id bundleId to activate
end run
APPLESCRIPT

sleep "$(awk "BEGIN { printf \"%.3f\", $WAIT_MS / 1000 }")"

FRONTMOST_ID="$(osascript <<'APPLESCRIPT'
tell application "System Events"
  set frontProc to first application process whose frontmost is true
  return bundle identifier of frontProc
end tell
APPLESCRIPT
)"

if [[ "$FRONTMOST_ID" != "$APP_ID" ]]; then
  echo "Frontmost app mismatch: expected $APP_ID, got ${FRONTMOST_ID:-<none>}" >&2
  exit 3
fi

"$HELPER_BIN"

そして、AppShotホットキーを実際に発火しているSwift側はかなり小さいです。ここで重要なのは、失敗した flagsChanged 経路ではなく、通常の keyDown/keyUp イベントだけを使っている点です。

import CoreGraphics
import Foundation

let source = CGEventSource(stateID: .hidSystemState)

func postKey(key: CGKeyCode, down: Bool) throws {
    guard let event = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: down) else {
        throw NSError(domain: "codex-appshot-trigger", code: 1)
    }
    event.post(tap: .cghidEventTap)
}

do {
    // 55 = left command, 54 = right command on macOS virtual key codes.
    try postKey(key: 55, down: true)
    usleep(80_000)
    try postKey(key: 54, down: true)
    usleep(250_000)
    try postKey(key: 54, down: false)
    usleep(80_000)
    try postKey(key: 55, down: false)
} catch {
    fputs("Failed to post AppShot hotkey CGEvents: \(error)\n", stderr)
    exit(5)
}
じぴ子

実装としては小さいですが、対象アプリ確認とkeyDown/keyUp経路を正本化したことで、次のセッションでも同じ判断を再利用できます。

使い方はこうです。

/Users/suzukimakoto/.codex/bin/codex-appshot-trigger --app com.macpaw.CleanMyMac5 --window-title CleanMyMac
/Users/suzukimakoto/.codex/bin/codex-appshot-trigger --app md.obsidian --window-title Obsidian

このヘルパーは、AppleScriptで対象アプリを前面化し、System Eventsでfrontmost bundle idを確認し、そのうえでSwift側のCGEvent発火へ進みます。ポイントは、単にキーイベントを投げるだけではなく、対象アプリが本当に前面にいるかを確認してからAppShotホットキーを発火することです。

ここを省くと、「別アプリのAppShotを撮ってしまう」「意図しない画面をCodexへ渡してしまう」という事故が起きます。AppShotは観測手段なので、観測対象の取り違えは操作ミスよりも危険です。

じぴ子

AppShotは「見える化」ではなく「Codexへ渡す観測」なので、対象アプリ確認はかなり大事です。

これは制限の破壊ではなく、レイヤーを分けた回避

Computer Use、AppShot、補助コマンドの役割を分けて扱う構成を、重なったレイヤーと案内キャラクターで表した画像。

ここでやったことは、Computer Useの制限を無視することではなく、役割を分けることでした。通常操作と特殊なホットキー発火を同じ道具に押し込まない、という整理です。

Computer Useに不得意な入力を押し付けない

今回やったことは、Computer Useの制限を無理やり壊したわけではありません。

Computer Useの通常キー入力では扱えない入力があった。そこで、Computer Useにその入力をやらせるのではなく、macOSの低レベル入力イベントを投げる専用ヘルパーに責務を分けました。

つまり、こういう分担です。

役割担当すること
Computer Use画面を見ながらクリック、入力、スクロールする主操作
AppShot画面状態を画像とテキストでCodexに渡す観測
codex-appshot-triggerAppShotホットキーを再現可能に発火する補助コマンド
AGENTS.mdどの条件でどの経路を使うかを定義する運用契約

この分担にしたことで、Computer Useに不得意なことを押し付けずに済みます。そして、毎回その場の思いつきでSwiftを書かず、検証済みのコマンドを再利用できます。

地味ですが、ここがかなり大事です。AIエージェントにとって「前にうまくいった手順を、次のセッションでも自然に選ぶ」ことは、単発の成功より価値があります。

今回の肝は、低レベル入力そのものではなく、役割を分けて再利用可能な正本コマンドにしたことです。

AGENTS.mdに昇格したこと

AppShot自律発火の条件と停止条件をAGENTS.mdの運用契約へ昇格する様子を、チェックリスト風のボードと案内キャラクターで表した画像。

動いた手順は、その場のノリで終わらせると次のセッションで失われます。そこで今回は、使う条件と停止条件をAGENTS.mdへ明文化しました。

単発の成功を次回も使える運用契約にした

今回の知見は、その場限りのメモではなく、AGENTS.mdに昇格しました。

書いた内容の要点は次の通りです。

  • CMPComputer Use の略称として扱う
  • macOS GUIアプリ操作では、主操作手段をComputer Useにする
  • AppShotは、前面ウィンドウを画像と取得可能テキストとしてCodex threadへ渡す観測手段として扱う
  • 対象アプリと作業目的が明示されているGUI workflowでは、必要に応じてCodexが自律的にAppShotを再取得してよい
  • AppShot発火には /Users/suzukimakoto/.codex/bin/codex-appshot-trigger を使う
  • 失敗確認済みの flagsChanged 経路や bare-modifier-monitor 検証経路へ戻さない
  • frontmost appが想定bundle idと違う場合、対象アプリが変わる場合、認証、権限昇格、外部送信、削除、課金、不可逆変更に入る場合は停止して確認する

ここで大事なのは、「自律的にAppShotを撮ってよい」と「何でも勝手に撮ってよい」は違う、という点です。

対象アプリと目的が明示されていて、その作業範囲内の再観測として必要な場合は、毎回ユーザーに確認しない。これは実用性のために必要です。一方で、対象が変わる、権限や課金や削除に入る、不可逆操作に入る場合は止まる。これは境界管理です。

じぴ子

「毎回確認しない」と「何でも勝手にやる」は別物です。ここを分けないと、実用性か安全性のどちらかが壊れます。

Computer UseとAppShotを組み合わせると何が変わるか

AppShotで観測し、Computer Useで操作し、再び観測して次の判断へ進むループを、複数の抽象パネルで表した画像。

Computer UseとAppShotを分けて考えると、GUI自動化の見え方が少し変わります。操作だけでなく、操作後に状態を取り直す流れを作れるからです。

クリック代行から観測ループへ変わる

Computer Use単体でもGUI操作はできます。しかし、AppShotを必要なタイミングで再取得できるようになると、操作の意味が変わります。

単発のクリック代行から、観測と判断を含むループへ近づきます。

たとえば、次のような流れが現実的になります。

  1. 対象アプリを起動する
  2. AppShotで現在状態を取得する
  3. Computer Useでボタンを押す
  4. 画面遷移後にAppShotを再取得する
  5. 新しい画面状態を読んで次の操作を判断する
  6. 目的達成まで繰り返す

これは「画面を見ているふり」ではありません。状態が変わったら、観測も更新する。エージェントがGUIアプリを扱うなら、この再観測の仕組みがないとすぐに脆くなります。

今回のAppShot helperは、そのループの中で「観測を取り直す」ための小さな部品です。でも、その小さな部品がないと、全体の自律性が止まります。

GUI自動化を安定させるには、操作ログよりも「画面変化後に再観測できること」が効いてきます。

もちろん万能ではない

AppShotの自律利用には権限、対象範囲、停止条件が必要であることを、安全境界と注意サイン風のモチーフで表した画像。

ただし、この方法は何でも自律化できる魔法ではありません。AppShotは強い観測手段なので、使う範囲と止まる条件をセットで考える必要があります。

自律化するほど停止条件が重要になる

この方法は万能ではありません。

macOSの権限設定、Codex appのバージョン、AppShotのホットキー仕様、対象アプリの前面化の挙動によって変わる可能性があります。すべての環境で同じように動くとは書けません。

また、AppShotは強い観測手段です。画面に出ている内容をCodexへ渡すので、対象に機密情報が含まれる場合は注意が必要です。

だから、今回のAGENTS.mdでも停止条件を入れています。対象アプリが違う、作業範囲を越える、認証や権限昇格や外部送信や削除に入る。そういう場面では、自律実行を続けずに止まるべきです。

自律化で重要なのは、何でも進めることではありません。進めてよい範囲を明確にし、その範囲内では毎回余計な確認を挟まずに進めることです。

Q. この方法はどのMac環境でも動く?

断定はできません。Codex appのバージョン、macOS側の権限、AppShotホットキー仕様、対象アプリの前面化挙動に依存します。

Q. Computer Useだけでやるべきでは?

通常のクリックや入力はComputer Useが主役です。ただし、左右Commandだけのようなbare modifier入力は別レイヤーのhelperへ分けた方が安定します。

まとめ

GUI自動化では操作だけでなく観測を取り直す経路が重要であることを、観測、操作、再観測の流れで表したまとめ画像。

今回の話を一言でまとめるなら、AIエージェントにGUIを触らせるには、操作だけでなく観測を取り直す経路まで必要だということです。

GUI自動化で効くのは観測を取り直せること

今回の発見は、派手な新機能というより、AIエージェント運用の地味な接続部分でした。

Computer UseはGUIを操作できる。AppShotはMacアプリの状態をCodexへ渡せる。しかし、AppShotを人間の手動ホットキーに依存したままだと、操作後の再観測が自律化できない。

そこで、AppShotホットキーを発火する専用ヘルパーを作り、検証済みのkeyDown/keyUp経路だけを残し、AGENTS.mdに運用ルールとして昇格しました。

結果として、Codexは次のような動線を持てるようになりました。

  • 対象アプリを前面化する
  • AppShotで状態を観測する
  • Computer Useで操作する
  • 画面変化後にAppShotを再取得する
  • 新しい状態に基づいて次の操作を決める

この流れが自然に回るようになると、AIエージェントのGUI操作はかなり実用に近づきます。

たぶん、同じことを考える人はこれから増えるはずです。クリックできるだけでは足りない。見直せること、判断し直せること、そしてその運用ルールが次のセッションにも残ること。AIエージェントのGUI自動化で本当に効いてくるのは、こういう地味な接続部分だと思います。

  • URLをコピーしました!

この記事を書いた人

makotoのアバター makoto Blogger&YouTuber

サーバー管理者として17年ほど仕事でサーバー触ってました。
www,mail,dns,sql各鯖をすべてFreeBSDで運用してましたが現世ではかなりレアなタイプになるみたいですね笑

viやシェルスクリプトとかperlとかgccとかFreeBSDとか実はbashよりtcshが好きとか時々寝ぼけるのは
その名残でしょう。

今まで縁の下の力持ち的な他人のためにプログラムを書き他人のためにサーバー構築し他人のためにWEBサイトを創る的な世界から
自分の好きなことに集中できる環境は実に気持ち良いですね。
現役は引退済みなので難しいことはやりませんしやれません。

現在 ほぼ自由人。

Content