# `cu-helper` — Integration Guide A native Swift Computer Use helper for claude-code-haha. It replaces the `runtime/mac_helper.py` (pyautogui + mss) bridge with a single, code-signed `arm64` executable that: - **never moves the real OS cursor** — every mouse/keyboard event is posted to a *specific* application process via `CGEvent.postToPid(...)`, and the on-screen motion the user sees is a **virtual cursor overlay** we draw ourselves; - **animates** that virtual cursor (ease-out-cubic glide, ~2000 px/s capped at 0.5 s) and paints a **glowing capture overlay** around the app the AI is driving — both of which need a persistent main-thread run loop; - keeps **stdout clean** so the existing Node bridge can `JSON.parse` exactly one line per command. > **Status of this document.** This file *describes* the integration. It does > **not** apply any TypeScript or Tauri change. Section 3 lists, verbatim, the > files that *would* change and how — nothing here edits them. The Swift package > itself is self-contained and builds/links independently (see > [`build.sh`](./build.sh)). > **TCC honesty.** Real cross-app clicking/typing requires **Accessibility** and > real screen capture requires **Screen Recording**, both granted *to this > binary* by the user in **System Settings ▸ Privacy & Security**. A CI job or a > background agent **cannot** grant these. Everything in §6.1–§6.4 is verifiable > without any TCC grant; everything that touches another app's input or the > framebuffer is a **manual** checklist in §6.5. --- ## 1. Dual-mode invocation The binary is **one executable, two modes**. This is forced by two facts that pull in opposite directions: 1. **The existing bridge is per-command.** `src/utils/computerUse/pythonBridge.ts` (`callPythonHelper`, lines 159–183 — **MUST NOT be edited**) spawns ` --payload ''`, reads exactly **one** stdout line `{ok, result?, error?}`, and the process exits. Tolerant of a nonzero exit code **only when stdout is empty** (`if (code !== 0 && !stdout.trim())`). 2. **The overlays are inherently stateful.** `CADisplayLink`/`CABasicAnimation` only tick while a live `CFRunLoop` runs on the main thread, and the *virtual* cursor's logical position (the source of truth for `cursor_position`, for `drag` with an implicit `from`, and for decomposed `mouse_down`/`mouse_up`) must survive *between* commands. A one-shot process that writes a line and dies cannot animate anything and remembers nothing. So the **daemon** holds everything stateful/visual and the **CLI** degrades gracefully: ### 1a. CLI one-shot — `cu-helper --payload ''` Runs against the **unchanged** bridge **today**, for **every** command. Builds a `CommandRouter` with a **headless** `VirtualCursor` (no `NSWindow`, no animation), dispatches the single command, prints **exactly one** `{"ok":...}\n` line, and `exit(0)`. - **Fully correct in CLI mode** (no run loop, no shared state needed): `displays` (`get_display_size`, `list_displays`, `find_window_displays`), `screenshot`, `zoom`, `resolve_prepare_capture`, all keyboard ops (`key`, `hold_key`, `type`), `click`, `scroll`, clipboard (`read_clipboard`, `write_clipboard`, `paste_clipboard`), app queries (`frontmost_app`, `app_under_point`, `list_installed_apps`, `list_running_apps`, `open_app`), and `check_permissions`. - **Degraded in CLI mode** (cursor-stateful): `move_mouse`/`drag` post events **instantly** (no glide); `cursor_position` reads a best-effort disk-persisted point at `~/.claude/.runtime/cu-helper.cursor.json` and `move_mouse` writes it; `mouse_down`/`mouse_up` post immediately. A half-finished drag **cannot** span two one-shot processes — documented limitation. - **On *any* thrown error**, CLI mode prints `{"ok":false,"error":{"message":...}}` and **still `exit(0)`** (never nonzero — we always emit JSON, and a nonzero code with JSON on stdout would be misread by the bridge's empty-stdout branch). - Critically, CLI mode writes its one line and exits **before** AppKit spins up any window, so os_log/CoreGraphics chatter never reaches stdout. ```bash cu-helper list_displays --payload '{}' cu-helper screenshot --payload '{"targetWidth":1280,"targetHeight":800,"jpegQuality":0.75}' cu-helper click --payload '{"x":640,"y":400,"button":"left","count":1,"modifiers":[]}' cu-helper type --payload '{"text":"héllo 世界"}' cu-helper check_permissions --payload '{}' ``` ### 1b. Daemon — `cu-helper daemon --socket ` Long-lived. The primary engine. `NSApplication.shared` with `.setActivationPolicy(.accessory)` (no Dock icon, never steals focus, but *can* own windows), and `app.run()` drives the single main-thread run loop. It: - creates the `VirtualCursorOverlay` window(s) and `CaptureGlowOverlay` window(s) **once** and keeps them warm (hidden between turns); - reads NDJSON request lines off an `AF_UNIX` `SOCK_STREAM` socket on a background `DispatchQueue`, marshals each decoded (`Sendable`) request onto `@MainActor` (`Task { @MainActor in router.handle(...) }`), then writes the `{ok,...}` response line back on the IO queue; - holds the virtual cursor's logical position, the held mouse button, and held keys **in memory** as the single source of truth. **Why a UNIX socket, not stdio.** A GUI/AppKit process leaks os_log and CoreGraphics warnings to stdout/stderr, which would corrupt the bridge's `JSON.parse`. The daemon therefore reserves **stdout for exactly one readiness line** and serves the request/response stream over the private socket. ### 1c. How the two modes relate to the file lock (orthogonal) The per-session file lock (`~/.claude/computer-use.lock`, `src/utils/computerUse/computerUseLock.ts`) decides **which Node session** may use Computer Use. The daemon is *that session's* execution engine. The two are independent: the lock is a session mutex; the daemon is a rendering+injection process. If the daemon dies mid-session, the bridge falls back to per-command CLI spawns (§1a) and respawns the daemon on the next fresh lock acquisition. **v1 deliberately skips** lock-screen autonomy, auto-unlock, and any guardian process. --- ## 2. NDJSON socket protocol (daemon mode) Transport: `AF_UNIX` / `SOCK_STREAM` at the `--socket` path (default `~/.claude/.runtime/cu-helper..sock`; the runtime dir is `getClaudeConfigHomeDir()` + `/.runtime`, created with `mkdir -p`). Framing: **one JSON object + `'\n'` per message**, both directions (NDJSON). ### 2a. Readiness line (stdout, exactly once) Immediately after the socket is bound + listening, the daemon writes **one** line to **stdout** and nothing else ever again: ```json {"ready":true,"pid":12345,"proto":1} ``` The caller awaits this line (with a timeout) **before** connecting to the socket, which avoids a connect race against `bind()`/`listen()`. ### 2b. Request ```json {"id":"","cmd":"","payload":{ ... }} ``` - `id` — optional; echoed back verbatim for response matching. - `cmd` — any command from the table below (the same set the CLI accepts) **or** a daemon-only control verb (§2d). - `payload` — the command payload object (same shape the CLI passes after `--payload`). ### 2c. Response ```json {"id":"","ok":true,"result":} {"id":"","ok":false,"error":{"message":"...","code":""}} ``` `result` is the command's result value (object, array, string, boolean, or `{x,y}` — see §4). `error.code` is one of the canonical codes from `CUError`: `no_target`, `not_trusted`, `screen_recording_denied`, `display_not_found`, `window_not_found`, `event_alloc`, `unknown_key`, `secure_input`, `bad_payload`, `bad_command`, `capture_failed`, `encode_failed`. > **Envelope parity.** The CLI's single stdout line and the daemon's per-request > response carry the **same** `{ok, result?, error?}` semantics, so the TS layer > unwraps both identically (`parsed.ok ? parsed.result : throw error.message`). > The only wire difference is the daemon's extra `id` field for multiplexing. ### 2d. Control verbs (daemon-only — never reach `CommandRouter`) | Verb | Effect | `result` | |----------------|-----------------------------------------------------------------------------------------|----------| | `overlay_show` | `cursor.show()` + `glow.show(over: frontmost app)`. Reveals the virtual cursor + glow. | `true` | | `overlay_hide` | Parks the cursor and resets turn-owned AX/input/focus state. The keyed `SCStream` remains warm until target/config change, disconnect, or daemon teardown. | `true` | | `ping` | Liveness probe. | `"pong"` | | `shutdown` | Returns `true`, then `NSApp.terminate(nil)` for a graceful exit. | `true` | These verbs are **not** reachable via the CLI (a one-shot process has no overlay and no run loop to host one). Every other `cmd` is routed through the shared `CommandRouter` — the identical dispatcher the CLI uses — so each command has **exactly one** implementation. ### 2e. Connection lifecycle - One client connection at a time is expected (one Node session holds the CU lock). On client **disconnect**: hide the overlay, keep the daemon running idle (warm overlays preserved). - On `shutdown`: terminate the `NSApplication`. ### 2f. Minimal client transcript ```text # 1. spawn $ cu-helper daemon --socket /tmp/cu.sock {"ready":true,"pid":12345,"proto":1} # <- the ONE stdout line # 2. connect to /tmp/cu.sock, then write NDJSON requests: --> {"id":"1","cmd":"ping"} <-- {"id":"1","ok":true,"result":"pong"} --> {"id":"2","cmd":"overlay_show"} <-- {"id":"2","ok":true,"result":true} # virtual cursor + glow appear; REAL mouse unmoved --> {"id":"3","cmd":"move_mouse","payload":{"x":900,"y":500}} <-- {"id":"3","ok":true,"result":true} # overlay GLIDES; REAL mouse stays free --> {"id":"4","cmd":"cursor_position"} <-- {"id":"4","ok":true,"result":{"x":900,"y":500}} --> {"id":"5","cmd":"shutdown"} <-- {"id":"5","ok":true,"result":true} # then the process exits ``` --- ## 3. Proposed TypeScript / Tauri changes (described, **NOT applied**) The Swift package ships independently. To wire it into the app, the following changes would be made. **None of them are in this commit.** The guiding constraint is that `pythonBridge.ts` (the per-command primitive) stays **untouched** so the CLI fallback path remains byte-for-byte the behavior the bridge already expects. ### 3.1 New file — `src/utils/computerUse/daemonClient.ts` A new module owning the daemon lifecycle and the socket client. Proposed surface: ```ts // All paths under ~/.claude/.runtime (getClaudeConfigHomeDir() + '/.runtime'). export async function ensureDaemon(): Promise // - resolve the cu-helper binary path (see §3.2); // - spawn `cu-helper daemon --socket /cu-helper..sock`; // - await the single readiness line `{"ready":true,...}` with a timeout; // - connect the AF_UNIX socket; memoize the connection for the session. // - idempotent + self-healing: if a prior daemon died, respawn. export async function callDaemon(cmd: string, payload?: unknown): Promise // - write one NDJSON request line with a fresh `id`; // - resolve with `result` on `{ok:true}`, reject with `error.message` on `{ok:false}`; // - on socket error / daemon-dead: fall back to a one-shot CLI spawn // (`cu-helper --payload ...`) so the turn still completes. export async function overlayShow(): Promise // callDaemon('overlay_show') export async function overlayHide(): Promise // callDaemon('overlay_hide') — best-effort export async function shutdownDaemon(): Promise // callDaemon('shutdown') — best-effort, then forget connection ``` `callDaemon`'s fallback-to-CLI is what makes the daemon an *optimization*, not a hard dependency: a dead daemon degrades to exactly the current per-command behavior. ### 3.2 `src/utils/computerUse/executor.ts` — binary selection Today the executor talks to Python via `callPythonHelper` (which reads `helperFileName`/`helperPath` from `pythonBridge.ts`). The proposed change points the executor at the **`cu-helper` binary** instead — for **stateful** ops it calls `callDaemon(...)` (animated cursor, persistent state), and it keeps a **CLI/one-shot** path for the rest. The unit of change is the *helper command invocation*, swapping the Python interpreter + `mac_helper.py` for the signed `cu-helper` binary resolved from: - **dev:** `native/cu-helper/.build/release/cu-helper`; - **bundled (Tauri):** the sidecar resolved from `binaries/cu-helper` (§3.5). `pythonBridge.ts` is **not** edited; the Windows path (`win_helper.py`) is untouched (this helper is macOS-only — `Package.swift` targets `.macOS("14.4")`). ### 3.3 `src/utils/computerUse/wrapper.tsx` — `acquireCuLock` fresh branch `wrapper.tsx` builds the per-call `ComputerUseSessionContext`. Its `acquireCuLock` callback (currently lines ~209–228) already detects the **fresh** acquisition (`tryAcquireComputerUseLock()` → `{kind:'acquired', fresh:true}`) and fires the Esc-hotkey registration + the "Claude is using your computer" notification. The proposed addition, **inside that same `r.fresh` branch**: ```ts if (r.fresh) { // ...existing escHotkey + sendOSNotification... await ensureDaemon() // spawn + await readiness (best-effort; CLI fallback if it fails) await overlayShow() // reveal virtual cursor + glowing capture overlay } ``` This is the only place the daemon is started, and it happens **after** the session wins the lock — so at most one daemon-owning session exists at a time. ### 3.4 `src/utils/computerUse/cleanup.ts` — `overlay_hide` + `shutdown` `cleanupComputerUseAfterTurn` runs at natural turn end and on abort, and already gates on `isLockHeldLocally()` before releasing the lock. Proposed additions: - **at turn end (best-effort):** `await overlayHide().catch(() => {})` — hide the cursor and fade the glow between turns, before releasing the lock. The daemon stays warm. - **on session archive / stop:** `await shutdownDaemon().catch(() => {})` — terminate the daemon process. (Session-archive cleanup site, not every turn.) Both are best-effort and never block lock release: a hung `overlayHide` must not wedge an abort, mirroring the existing `UNHIDE_TIMEOUT_MS` guard. ### 3.5 `desktop/src-tauri/tauri.conf.json` — `externalBin` Add the signed binary as a Tauri sidecar alongside the existing one: ```jsonc "externalBin": [ "binaries/claude-sidecar", "binaries/cu-helper" // <- added ] ``` Tauri resolves `externalBin` entries with a target-triple suffix (`binaries/cu-helper-aarch64-apple-darwin`); `build.sh`'s output binary is copied to that location by the desktop packaging step. The binary must already be **code-signed with the stable identity** (§4) *before* Tauri bundles it, so the app's notarized package preserves the TCC-stable signature. --- ## 4. Command contract & result shapes Same dispatch table for CLI and daemon (`CommandRouter.handle(cmd:payload:)`). Result shapes match `src/vendor/computer-use-mcp/executor.ts` and how `src/utils/computerUse/executor.ts` normalizes them. **Coordinates are logical top-left points already scaled by the TS layer — used verbatim; never re-multiply by `scaleFactor`.** | `cmd` | Payload | `result` | TCC needed | |-------|---------|----------|------------| | `prepare_for_action` | `{}` | `[]` (v1) | — | | `preview_hide_set` | `{}` | `[]` (v1) | — | | `get_display_size` | `{displayId?}` | `DisplayGeometry` (primary if `displayId` null) | none | | `list_displays` | `{}` | `DisplayGeometry[]` | none | | `find_window_displays` | `{bundleIds:[…]}` | `[{bundleId, displayIds:[…]}]` | none | | `resolve_prepare_capture` | `{preferredDisplayId?, targetWidth, targetHeight, jpegQuality}` | `ResolvePrepareCaptureResult` | **Screen Recording** | | `screenshot` | `{displayId?, targetWidth, targetHeight, jpegQuality}` | `ScreenshotResult` | **Screen Recording** | | `zoom` | `{x, y, width, height, targetWidth, targetHeight}` | `{base64, width, height}` | **Screen Recording** | | `key` | `{keySequence:"cmd+shift+a", repeat}` | `true` | **Accessibility** | | `hold_key` | `{keyNames:[…], durationMs}` | `true` | **Accessibility** | | `type` | `{text}` | `true` | **Accessibility** | | `paste_clipboard` | `{}` | `true` | **Accessibility** | | `read_clipboard` | `{}` | `string` | none | | `write_clipboard` | `{text}` | `true` | none | | `click` | `{x, y, button, count, modifiers}` | `true` | **Accessibility** | | `mouse_down` | `{}` | `true` | **Accessibility** | | `mouse_up` | `{}` | `true` | **Accessibility** | | `cursor_position` | `{}` | `{x, y}` (virtual cursor; never the OS cursor) | none | | `drag` | `{from?, to}` | `true` (`from` defaults to virtual-cursor pos) | **Accessibility** | | `move_mouse` | `{x, y}` | `true` (moves the **virtual** cursor only) | none (hover post best-effort) | | `scroll` | `{x, y, deltaX, deltaY}` | `true` | **Accessibility** | | `frontmost_app` | `{}` | `{bundleId, displayName}` \| `null` | none | | `app_under_point` | `{x, y}` | `{bundleId, displayName}` \| `null` | none | | `list_installed_apps` | `{}` | `InstalledApp[]` (`{bundleId, displayName, path, iconDataUrl?}`) | none | | `list_running_apps` | `{}` | `RunningApp[]` (`{bundleId, displayName}`) | none | | `open_app` | `{bundleId}` | `true` | none | | `check_permissions` | `{}` | `{accessibility, screenRecording}` | none | **Shape notes (cross-checked against the TS normalizer):** - `DisplayGeometry` emits **both** `id` *and* `displayId`, and **both** `name` *and* `label` — `normalizeDisplayGeometry` falls back `displayId ?? id` and `label ?? name`, but emitting both keeps every consumer happy. `scaleFactor = CGDisplayPixelsWide / CGDisplayBounds.width`. - `ScreenshotResult` / `ResolvePrepareCaptureResult` carry the **flat** geometry fields (`width`, `height`, `displayWidth`, `displayHeight`, `displayId`, `originX`, `originY`) **and** a nested `display: DisplayGeometry`. `width`/ `height` are read back from the produced `CGImage` (actual output pixels); `displayWidth`/`displayHeight` are the source display's logical points. - `ResolvePrepareCaptureResult` additionally has `hidden: []` (v1 hides nothing) and `resolvedDisplayId == displayId` (v1 does not chase displays). The TS layer passes `autoResolve`/`doHide`; in v1 they are **ignored** (Python parity). - All keyboard injection targets `NSWorkspace.frontmostApplication.pid`; all coordinate ops hit-test `CGWindowList` under the logical point → owner → pid. Every event is posted with **`CGEvent.postToPid(targetPid, …)`** — never `CGEventPost(.cghidEventTap, …)`, never any cursor-warp API. --- ## 5. Stable signing for TCC persistence macOS keys Accessibility + Screen Recording grants to a binary's **code-signing identity** (its designated requirement / cdhash lineage). An **ad-hoc** signature (`codesign -s -`) or a per-build throwaway cert **rotates that identity on every rebuild**, forcing the user to re-grant *both* permissions after every `swift build`. To keep grants alive, [`build.sh`](./build.sh) **always** signs with a **stable cert** and a **constant `--identifier`**, and **never** falls back to ad-hoc. ```bash ./build.sh # 1) swift build -c release --arch arm64 --package-path # 2) codesign --force --options runtime \ # --identifier dev.cchaha.cu-helper \ # --sign "Apple Development: 524134442@qq.com (F8ZSJJ78S7)" \ # .build/release/cc-haha-computer-use # 3) wraps and signs .build/release/cc-haha-computer-use.app # 4) prints: built: ``` - **Identity resolution.** `build.sh` prefers the real `Apple Development: …` identity if present (verified available on this machine as `Apple Development: 524134442@qq.com (F8ZSJJ78S7)`); then a release/CI `Developer ID Application: …` identity; then a self-signed `cu-helper-dev` cert. With none available it **stops** and prints one-time instructions. It **never** silently ad-hoc signs. Override with `CU_HELPER_IDENTITY` / `CU_HELPER_BUNDLE_ID`. - **Timestamp policy.** Developer ID builds use `--timestamp` and verify that a secure `Timestamp=` exists because electron-builder deliberately preserves this nested app signature. Apple Development/self-signed builds default to offline `--timestamp=none`. `CU_HELPER_TIMESTAMP_MODE=secure` makes CI fail closed if the timestamp service cannot produce a distribution-ready signature. - **`--options runtime`** (Hardened Runtime) keeps it dev-safe and notarization-ready for the Electron bundle. - **Stability acceptance test** — build twice and confirm the signature is unchanged: ```bash codesign -dv --verbose=4 .build/release/cc-haha-computer-use.app 2>&1 | grep -E 'Identifier|Authority|Timestamp' # Identifier=dev.cchaha.cu-helper <- constant across rebuilds # Authority=Apple Development: 524134442@qq.com (F8ZSJJ78S7) ``` Identical `Identifier` + `Authority` across two builds ⇒ TCC grants survive a rebuild. If they differ, the user will be re-prompted — that's the bug this whole section exists to prevent. --- ## 6. Verification ### 6.1 Build + sign (agent-runnable) ```bash ./native/cu-helper/build.sh # expect: built: .../native/cu-helper/.build/release/cc-haha-computer-use.app ``` ### 6.2 TCC-free CLI smoke (agent-runnable — **no permissions needed**) Each must print **exactly one** clean JSON line: ```bash B=native/cu-helper/.build/release/cc-haha-computer-use.app/Contents/MacOS/cc-haha-computer-use "$B" list_displays --payload '{}' # DisplayGeometry[] with id+displayId+name+label "$B" get_display_size --payload '{}' # primary display (isPrimary:true) "$B" frontmost_app --payload '{}' # {bundleId,displayName} | null "$B" check_permissions --payload '{}' # {accessibility:bool, screenRecording:bool} "$B" list_installed_apps --payload '{}' # InstalledApp[] "$B" list_running_apps --payload '{}' # RunningApp[] "$B" cursor_position --payload '{}' # {x,y} (disk-persisted in CLI mode) ``` Pipe any of them through `python3 -m json.tool` (or `jq .`) to confirm the line is valid JSON and there is no os_log/CoreGraphics leakage on stdout. ### 6.3 Self-test injection without cross-app TCC (agent-runnable) `ensurePostable` permits posting to **`getpid()`** without Accessibility (the self-test exemption). Posting a synthesized key to our own pid and reading it back via an in-process responder exercises the full `CGEvent` construction + `postToPid` path **without** needing Accessibility on another app — this proves event *plumbing* is correct even where end-to-end cross-app delivery can't be tested in CI. ### 6.4 Daemon smoke (agent-runnable — **no TCC**) ```bash B=native/cu-helper/.build/release/cu-helper "$B" daemon --socket /tmp/cu.sock # prints {"ready":true,"pid":N,"proto":1} # connect /tmp/cu.sock, then: # {"id":"1","cmd":"ping"} -> {"id":"1","ok":true,"result":"pong"} # {"id":"2","cmd":"overlay_show"} -> overlay appears; PHYSICAL cursor does NOT move # {"id":"3","cmd":"move_mouse","payload":{"x":900,"y":500}} -> overlay GLIDES; real mouse free # {"id":"4","cmd":"shutdown"} -> {"ok":true,"result":true}, process exits ``` `overlay_show` + `move_mouse` are the **eyeball test** for the core invariant: the virtual cursor glides while the real mouse stays free. ### 6.5 Manual TCC checklist (**cannot** be done by a background agent) Requires the user in **System Settings ▸ Privacy & Security**. Grant **Accessibility** *and* **Screen Recording** to the signed `cu-helper` binary, then verify — watching the **physical** mouse the whole time: - [ ] `screenshot --payload '{"targetWidth":1280,"targetHeight":800,"jpegQuality":0.75}'` → non-empty base64; decoded image matches the display. - [ ] Focus TextEdit, `type --payload '{"text":"héllo 世界"}'` → text appears (layout-independent unicode), **physical cursor unmoved**. - [ ] `click` a button in another app → it activates, **physical cursor unmoved** (the virtual cursor glides to it in daemon mode). - [ ] Double- / triple-click a word → word / line selection (click-state 1..N). - [ ] `cmd+a` then `cmd+c` in a focused field → selects all, copies. - [ ] `scroll` a long page → it scrolls. **Confirm the sign vs. natural-scroll** on a real scroll view and flip `deltaX`/`deltaY` sign if inverted (open question — cannot be settled without a real view + Accessibility). - [ ] `drag` a selection → drag-select / move works (daemon overlay glides the path). - [ ] `hold_key` shift+arrow then release → extends selection, then releases. - [ ] **Rebuild + re-sign**, then repeat a `screenshot` and a `type` **without re-prompting** → confirms the stable-identity TCC persistence (§5). --- ## 7. Open questions (deferred for v1) These are intentionally punted; each is safe for v1 per Python parity, but listed so the manual tester knows what to watch: 1. **Scroll sign/axis** — `wheel1` = vertical, `wheel2` = horizontal; natural scroll inverts the sign. Verify on a real view (§6.5) and flip if needed. 2. **`resolve_prepare_capture`** is a straight display capture with `hidden:[]` and `resolvedDisplayId == displayId`. The full co-location / app-chase display resolver and app-hiding are deferred; the TS `autoResolve`/`doHide` flags are ignored (matches the current Python helper). 3. **CLI `cursor_position` cold start** — reads a disk-persisted point; if the *first* CU command of a session is `cursor_position` or a `drag` with no `from` before any `move_mouse`, it defaults to last-known / `(0,0)`. A non-issue in daemon mode (in-memory). The agent loop moves before dragging (`drag` passes an explicit `from` when available). 4. **`move_mouse` hover event** — whether to also post a real `.mouseMoved` `CGEvent` (improves hover-state fidelity, needs Accessibility + a resolved pid). Proposed: best-effort, swallowed if not trusted. 5. **Glow target selection** — v1 follows the **frontmost** app on `overlay_show`. Following the specific window under the last action point (richer, needs `windowID` tracking via `WindowFrameTracker`) is deferred. 6. **`iconDataUrl` cost** — `list_installed_apps` encodes a PNG data URL per app (`NSWorkspace.icon` → PNG → base64), adding latency. Proposed: include it (the desktop UI renders it); the optional `getAppIcon(path)` executor method exists as a lazier alternative if the cost proves too high.