macOS Harness
Setup
The skill ships with aster; the CLI it drives does not. Install it once with
uv tool install macos-harness, then run macos-harness doctor to check
permissions without prompting (--request only with user approval).
Surface
The CLI entry points are doctor, apps, see <app>, state <app>,
repl, skill, telemetry. The real harness is the Python session: stdin
programs preload mac, browser, Path, and subprocess.
macos-harness <<'PY'
print(mac.see("Finder")["path"])
PY
Verified bindings (checked against the installed CLI):
mac.see(app) -> dict with path (screenshot file), app, bounds,
focus.target_is_frontmost. Captures without focusing the app.
mac.key / mac.type / mac.click / mac.drag / mac.scroll /
mac.move: PID-targeted input; the physical cursor stays untouched.
mac.ax is an object: .dump(app) (dict: app, nodes, text,
windows, screenshot), .query, .get, .set, .perform,
.actions, .at.
mac.script(applescript) for Apple Events; mac.windows(app),
mac.list_apps(), mac.snapshot(), mac.get_app_state(app) for
discovery and state.
browser.connect(name) / browser.wait(name) for CDP into the user's
real logged-in browser. Chrome shows an Allow remote debugging? sheet
on first connect; approve it once there.
- Plain
Path and subprocess for everything else.
Minimize round trips
- Bundle deterministic, reversible steps into one program, then verify once.
Opening search, typing a query, and capturing the results is one burst,
not three calls.
- Stop at a genuine decision boundary: ambiguous identity, new coordinates,
an irreversible action, or unexpected state. Inspect once, then run the
next burst.
- Do not screenshot merely to confirm that a known shortcut opened a text
field before typing. Let the final screenshot verify the whole sequence.
- Poll exact AX or Apple Events state inside the same Python program when
possible; do not make the model repeatedly ask whether a transition
finished.
- Use the cheapest strong end-state check: one screenshot for visible state
or one exact API/AX query for semantic state; both only when they prove
different things.
Use the small surface
Think in six verbs: see, key, type, click, ax, script.
frame = mac.see("Spotify")
mac.key("cmd+k", app="Spotify")
mac.type("Alessia Cara", app="Spotify")
mac.click(640, 420, app="Spotify")
item = mac.ax.at(640, 420, app="Spotify")
mac.ax.perform(item["element_index"], "AXPress")
mac.script('tell application "Spotify" to play')
Use ordinary Python for local context and one-off logic. Do not add
app-specific helpers when a short program can resolve the task.
Choose the lowest useful mode
- When identity depends on local context (
my, friend, or prior
activity), inspect that context and correlate stable fields; a loose text
hit is not enough.
- Use
mac.script() for a known exact, focus-safe app command.
- Otherwise use
mac.see(app) and vision.
- Prefer a known keyboard route; use a verified coordinate for a visible,
low-risk target.
- Use targeted
mac.ax only when semantic identity or state matters. Do
not dump a full AX tree before trying the direct route.
After a failed verified burst, switch mode or stop. Never repair uncertainty
with repeated keys, clicks, deletion loops, or bulk input.
Keep the invariants
- Input targets an already-running app PID and never requests activation or
raise.
- A background target becoming frontmost raises
FocusChangedError; never
manipulate focus to restore it.
mac.click() is raw PID-targeted input. It never guesses an AX action.
- The animated pointer is click-through and never moves the physical cursor.
mac.move() moves only that pointer; it cannot produce native hover.
- Inactive apps may reject raw clicks. After one verified failure, switch
mode.
- Never launch a closed app or use a custom URL scheme when focus is
forbidden.
- Screenshot coordinates come from the latest
mac.see() and preserve
window bounds and Retina scaling.
Secondary primitives are mac.move, drag, scroll, show_pointer, and
hide_pointer. mac.ax.query() returns compact matches and bounds fallback
traversal; lower max_nodes for especially large apps.
Browser and permissions
Use browser for DOM, tabs, network, downloads, and uploads. Do not
substitute AX for CDP inside a web page. While Browser Harness connects,
macOS Harness accepts Chrome's exact Allow remote debugging? sheet through
system-wide AX. It never activates Chrome or emits a mouse event.
Run macos-harness doctor to inspect permissions without prompting. Run
macos-harness doctor --request only with user approval. Accessibility,
screen recording, and event posting are global; Apple Events Automation is
per target.
1---2name: macos-harness3description: Control a whole Mac from one persistent Python session with screenshots, PID-targeted input, an animated virtual pointer, targeted Apple Accessibility, Apple Events, Browser Harness CDP, and filesystem access. Use for native, Electron, browser, dialog, file, or cross-app tasks without moving the physical cursor or forcing apps into the foreground.4---56# macOS Harness78## Setup910The skill ships with aster; the CLI it drives does not. Install it once with11`uv tool install macos-harness`, then run `macos-harness doctor` to check12permissions without prompting (`--request` only with user approval).1314## Surface1516The CLI entry points are `doctor`, `apps`, `see <app>`, `state <app>`,17`repl`, `skill`, `telemetry`. The real harness is the Python session: stdin18programs preload `mac`, `browser`, `Path`, and `subprocess`.1920```bash21macos-harness <<'PY'22print(mac.see("Finder")["path"])23PY24```2526Verified bindings (checked against the installed CLI):2728- `mac.see(app)` -> dict with `path` (screenshot file), `app`, `bounds`,29 `focus.target_is_frontmost`. Captures without focusing the app.30- `mac.key` / `mac.type` / `mac.click` / `mac.drag` / `mac.scroll` /31 `mac.move`: PID-targeted input; the physical cursor stays untouched.32- `mac.ax` is an object: `.dump(app)` (dict: `app`, `nodes`, `text`,33 `windows`, `screenshot`), `.query`, `.get`, `.set`, `.perform`,34 `.actions`, `.at`.35- `mac.script(applescript)` for Apple Events; `mac.windows(app)`,36 `mac.list_apps()`, `mac.snapshot()`, `mac.get_app_state(app)` for37 discovery and state.38- `browser.connect(name)` / `browser.wait(name)` for CDP into the user's39 real logged-in browser. Chrome shows an `Allow remote debugging?` sheet40 on first connect; approve it once there.41- Plain `Path` and `subprocess` for everything else.4243## Minimize round trips4445- Bundle deterministic, reversible steps into one program, then verify once.46 Opening search, typing a query, and capturing the results is one burst,47 not three calls.48- Stop at a genuine decision boundary: ambiguous identity, new coordinates,49 an irreversible action, or unexpected state. Inspect once, then run the50 next burst.51- Do not screenshot merely to confirm that a known shortcut opened a text52 field before typing. Let the final screenshot verify the whole sequence.53- Poll exact AX or Apple Events state inside the same Python program when54 possible; do not make the model repeatedly ask whether a transition55 finished.56- Use the cheapest strong end-state check: one screenshot for visible state57 or one exact API/AX query for semantic state; both only when they prove58 different things.5960## Use the small surface6162Think in six verbs: `see`, `key`, `type`, `click`, `ax`, `script`.6364```python65frame = mac.see("Spotify")66mac.key("cmd+k", app="Spotify")67mac.type("Alessia Cara", app="Spotify")68mac.click(640, 420, app="Spotify")6970item = mac.ax.at(640, 420, app="Spotify")71mac.ax.perform(item["element_index"], "AXPress")7273mac.script('tell application "Spotify" to play')74```7576Use ordinary Python for local context and one-off logic. Do not add77app-specific helpers when a short program can resolve the task.7879## Choose the lowest useful mode80811. When identity depends on local context (`my`, `friend`, or prior82 activity), inspect that context and correlate stable fields; a loose text83 hit is not enough.842. Use `mac.script()` for a known exact, focus-safe app command.853. Otherwise use `mac.see(app)` and vision.864. Prefer a known keyboard route; use a verified coordinate for a visible,87 low-risk target.885. Use targeted `mac.ax` only when semantic identity or state matters. Do89 not dump a full AX tree before trying the direct route.9091After a failed verified burst, switch mode or stop. Never repair uncertainty92with repeated keys, clicks, deletion loops, or bulk input.9394## Keep the invariants9596- Input targets an already-running app PID and never requests activation or97 raise.98- A background target becoming frontmost raises `FocusChangedError`; never99 manipulate focus to restore it.100- `mac.click()` is raw PID-targeted input. It never guesses an AX action.101- The animated pointer is click-through and never moves the physical cursor.102- `mac.move()` moves only that pointer; it cannot produce native hover.103- Inactive apps may reject raw clicks. After one verified failure, switch104 mode.105- Never launch a closed app or use a custom URL scheme when focus is106 forbidden.107- Screenshot coordinates come from the latest `mac.see()` and preserve108 window bounds and Retina scaling.109110Secondary primitives are `mac.move`, `drag`, `scroll`, `show_pointer`, and111`hide_pointer`. `mac.ax.query()` returns compact matches and bounds fallback112traversal; lower `max_nodes` for especially large apps.113114## Browser and permissions115116Use `browser` for DOM, tabs, network, downloads, and uploads. Do not117substitute AX for CDP inside a web page. While Browser Harness connects,118macOS Harness accepts Chrome's exact `Allow remote debugging?` sheet through119system-wide AX. It never activates Chrome or emits a mouse event.120121Run `macos-harness doctor` to inspect permissions without prompting. Run122`macos-harness doctor --request` only with user approval. Accessibility,123screen recording, and event posting are global; Apple Events Automation is124per target.