peekaboo · git:20260718.80085c9 · 2026-07-18 · sha256 9d2698a6905658a0
peekaboo git:20260718.80085c9A
Immutable. This exact content never changes and is served at /api/v1/blob/9d2698a6905658a0.
---
name: peekaboo
description: "Drive, observe, and critique a running native macOS app (SwiftUI/AppKit) end-to-end via Peekaboo — the macOS-native equivalent of Playwright + visual-regression review. Use when the user mentions the Mac app's UI, screens, screenshots, appearance, layout, accessibility identifiers, clicking, typing, or menus — or says 'see what it looks like', 'show me the UI', 'click X', 'take a screenshot of the app', 'critique the design', 'drive the app'. Use after a SwiftUI/AppKit edit or rebuild, and before claiming a UI feature done. NOT for Electron or web apps (use the electron-playwright validator), NOT for iOS simulators (use the Maestro validator), NOT for purely backend work. Screenshots go to disk and each one is read by a separate same-model sub-agent, so the driver's context never fills with pixels; no external AI keys are used."
homepage: https://peekaboo.sh
metadata:
os: ["darwin"]
requires:
bins: ["peekaboo"]
permissions: ["Screen Recording", "Accessibility", "Event Synthesizing for background keyboard input"]
install:
- kind: brew
formula: steipete/tap/peekaboo
---
# Peekaboo - drive, observe, and critique macOS apps
The job is not just "is this clickable?" Verify that the workflow works and
that every meaningful state looks intentional.
Invoke `peekaboo` directly. Build orchestration belongs to the project's task
runner; UI observation and driving belong here.
**No external AI keys, and never pixels in your own context.** You are the
LLM, but do not `view` screenshots yourself: every image you load is re-sent
on every later turn and blows up the context window. Save each screenshot with
`--path`, then hand its path to a **separate sub-agent running your same
model** — one sub-agent per image — which opens it and returns a short text
report. Do not call `peekaboo agent`, `peekaboo analyze`, or any `--analyze`
mode. Deterministic CLI commands, parsed JSON, visible postconditions,
retained artifacts, and delegated visual reads are the verification contract.
## When to invoke
- After modifying a SwiftUI/AppKit view or interaction.
- When asked to show, inspect, click through, screenshot, or critique a native
Mac app.
- When debugging a native UI failure or validating a user journey.
- Before claiming a native macOS UI feature complete.
- Skip purely backend work, Electron/web apps, and iOS simulator flows.
## Prerequisites
```bash
peekaboo --version # require >= 3.4
peekaboo permissions status --all-sources --json
```
Use the latest stable Peekaboo on macOS 15.0+. Install it when missing:
```bash
brew install steipete/tap/peekaboo
```
Require Screen Recording and Accessibility on the **selected runtime** shown
by `permissions status`. Event Synthesizing is additionally required for
background typing, hotkeys, key presses, paste, coordinate clicks, and
synthetic click fallback:
```bash
peekaboo permissions request-event-synthesizing
```
In Claude Code, Copilot CLI, SSH, LaunchAgents, or other subprocess hosts, the
selected runtime may be Peekaboo's reusable daemon or app Bridge rather than
the terminal itself.
## Core loop
Create one explicit artifact directory:
```bash
ARTIFACT_DIR="${PEEKABOO_ARTIFACT_DIR:-$PWD/.artifacts/peekaboo}"
mkdir -p "$ARTIFACT_DIR"
```
1. **Build** with the project's existing task runner.
2. **Launch/drive** the app into the state under test.
3. **Observe** with `see` to get pixels, AX metadata, a snapshot ID, and
opaque element IDs.
4. **Act once** using the most semantic reliable action.
5. **Observe again** after the mutation; never keep using an old snapshot.
6. **Delegate the annotated PNG to a one-shot same-model sub-agent** (see
below) and critique from its text report, not just the command output.
7. **Fix and repeat** until both behavior and visible quality pass.
Read [agent-runtime guidance](references/agent-runtime.md) for host selection,
permissions, background delivery, coordinate behavior, version-specific
gotchas, and recovery. Read
[visual-verification guidance](references/visual-verification.md) for the
critique rubric, state coverage, responsive checks, and evidence format.
## Read screenshots without blowing up context
Never open a screenshot in your own context — not `view`, not `Read`, not an
inline image. Each pixel buffer you load is resent on every later turn, and
that is exactly what makes long UI sessions unusable.
For **every** PNG you need to judge:
1. Keep the file on disk (always capture with `--path`).
2. Spawn a **fresh sub-agent running the same model you are** and give it just
that one image path plus a specific question. It reads the pixels in its own
disposable context and returns a short text report — verbatim on-screen
text, element positions, and a scored critique.
3. **One image per sub-agent.** Never batch several PNGs into one sub-agent,
and never reuse a sub-agent for the next image — each screenshot gets its
own, so no pixels ever accumulate anywhere.
4. Act on the returned text.
Host mechanics:
- **Copilot CLI:** `task` tool → `agent_type: "explore"` (it has `view`),
`model:` set to the model you are currently running, `prompt:` =
"view `<abs path>` and report …".
- **Claude Code:** `Task` tool → a `general-purpose` sub-agent that `Read`s the
image path and returns text; run it on your model.
- Only if the host has no sub-agent/Task capability at all: `view` exactly one
cropped image, then move on — never in a loop.
This keeps the driver's context text-only no matter how many states you capture.
**Never do this:**
- Do not `view`, `Read`, or otherwise open a screenshot (or any image) in the
driver's own context — one loaded PNG is resent on every later turn and is
exactly what blows up the window.
- Do not put more than one image in a sub-agent, and do not reuse a sub-agent
for a second image. One image → one fresh sub-agent → discard.
- Do not loop capture → view → capture in the driver to "keep looking".
- Do not call `peekaboo agent`, `peekaboo analyze`, or any `--analyze` mode —
that is a separate model loop, not this workflow.
**Always do this:**
- Capture to a file with `--path`, hand that one path to one same-model
sub-agent, and act only on the text it returns.
- After acting, re-observe structured state (`see` / `inspect-ui`) instead of
taking another screenshot.
## Find and target the app
Prefer the stable bundle ID over a display name or Xcode scheme:
```bash
peekaboo list apps --json \
| jq '.data.applications[]
| select(.name | test("YourApp"; "i"))
| {name,bundleIdentifier,processIdentifier}'
peekaboo app launch --bundle-id "$BID" --wait-until-ready
```
Running process names often use `CFBundleName`, which can differ from the
scheme. Cache confirmed bundle/display-name caveats in the project's
`AGENTS.md` or `CLAUDE.md`.
## Observe efficiently
Use the cheapest observation that answers the question:
```bash
# AX metadata only; no screenshot artifact
peekaboo inspect-ui --app-target "$BID" --json
# Default validator observation: screenshot + AX metadata
peekaboo see --app "$BID" --json --annotate \
--path "$ARTIFACT_DIR/state.png" > "$ARTIFACT_DIR/state.json"
# Pixels only for AX-opaque custom rendering
peekaboo image --mode window --app "$BID" --retina \
--path "$ARTIFACT_DIR/raw.png"
```
Always pass `--path` when the agent must inspect or retain a screenshot.
JSON-only `see` without a path can keep the image solely in managed snapshot
storage and return empty direct screenshot path fields.
Treat every element ID as an opaque string:
```bash
SID=$(jq -r .data.snapshot_id "$ARTIFACT_DIR/state.json")
ID=$(jq -r \
'.data.ui_elements[]
| select(.identifier=="header.utility.settings")
| .id' "$ARTIFACT_DIR/state.json")
peekaboo click --on "$ID" --snapshot "$SID" --app "$BID"
```
Never generate, increment, or infer meaning from an ID. After `click`, `type`,
`hotkey`, `paste`, menu/dialog/app/window operations, or any other mutation,
run `see` or `inspect-ui` again before the next ID-based action.
## Choose the action
Prefer this order:
1. `set-value` for a settable, non-secure field when direct replacement is the
intended behavior.
2. `click --on "$ID" --snapshot "$SID"` for normal button activation.
3. `perform-action` for a specific AX action such as `AXShowMenu`,
`AXIncrement`, or `AXDecrement`.
4. A label/query when no stable identifier exists.
5. Coordinates only for custom-drawn or AX-invisible surfaces.
```bash
peekaboo set-value "hello" --on "$FIELD_ID" --snapshot "$SID"
peekaboo perform-action --on "$STEPPER_ID" \
--action AXIncrement --snapshot "$SID"
peekaboo click "Settings" --app "$BID" --wait-for 8000
```
Targeted `click`, `type`, `press`, `hotkey`, and `paste` use background
delivery by default. Preserve that non-interference. Add `--foreground` only
when the app needs a focused key window, a real mouse event, a Space switch,
or a double-click.
With an app/PID/window target, `--coords x,y` is relative to that window.
Use `--global-coords` only for intentional screen coordinates. Normalize and
verify window geometry before coordinate fallback.
Do not default to `--no-remote` or a forced capture engine. Background agent
sessions can report valid local TCC state yet capture wallpaper/redacted
pixels; prefer a permissioned daemon/Bridge.
### Native surfaces worth using
Do not rebuild these flows from coordinate clicks:
| Surface | Prefer |
|---|---|
| App lifecycle | `peekaboo app launch`, `app quit`, `app focus` |
| Menus | `peekaboo menu list`, then `menu click --path "View > Sidebar"` |
| Open/save panels | `peekaboo dialog list`, `dialog input`, `dialog file` |
| Window geometry | `peekaboo window set-bounds`, then read back and verify |
| Clipboard | `peekaboo clipboard get/set/clear`, then `paste` |
| URLs/deep links | `peekaboo open "scheme://..."` |
| Long scenarios | `peekaboo run <scenario>` when the project already owns one |
Use live `peekaboo <command> --help` for current flags. Keep product-specific
targeting and bundle-ID facts in the app repository, not in this generic skill.
### Recovery ladder
Recover from explicit evidence instead of retrying the same command:
1. On `ELEMENT_NOT_FOUND`, stale snapshot, or changed focus, capture a fresh
`see`/`inspect-ui` result and resolve the target again.
2. If JSON reports a truncated AX tree, increase `--max-depth`,
`--max-elements`, or `--max-children` only enough for the target surface.
3. If background delivery reports that the action needs real input, retry once
with `--foreground` and verify the target app became frontmost before input.
4. If capture is blank, wallpaper-only, or redacted, inspect selected-source,
Bridge, and daemon diagnostics before changing engines.
5. If AX cannot expose custom rendering, use a raw image, normalize window
geometry, and perform one verified coordinate fallback.
6. If a command reports success but the postcondition did not change, record
it as a failure. Do not turn a no-op into success through repeated clicks.
## Instrument the app for stable validation
Tag interactive SwiftUI/AppKit controls with stable
`.accessibilityIdentifier(...)` values:
```swift
Button("Settings") { showSettings = true }
.accessibilityIdentifier("header.utility.settings")
```
Use a documented namespace such as `header.tab.sessions`,
`panel.settings.save`, or `row.<id>.delete`. If a copy-based click becomes
repeated, add an identifier instead of preserving brittle text targeting.
Avoid applying one identifier to a composite parent: descendants can inherit
duplicates. Tag the precise child or use
`.accessibilityElement(children: .contain)`. Give icon-only buttons an
`.accessibilityLabel(...)`.
These identifiers also improve XCUITest.
## Verify pixels and behavior
After every meaningful state:
1. Parse the JSON envelope and assert `success`.
2. Assert a product postcondition in the fresh AX tree or app output.
3. Hand `$ARTIFACT_DIR/state_annotated.png` to its own same-model sub-agent
and read back its text report (never `view` it yourself).
4. Critique intent, hierarchy, alignment, spacing, contrast, copy, and edge
states.
5. Rank concrete fixes and repeat after changes.
A successful click is not a successful feature. Cover relevant initial,
loading, empty, populated, error, disabled, overflow, focus, modal, and
responsive states.
For a transition or flaky sequence, record the command itself:
```bash
peekaboo capture action --app "$BID" --duration-limit 10 \
--post-roll-ms 800 --path "$ARTIFACT_DIR/action" \
--video-out "$ARTIFACT_DIR/action.mp4" --json -- \
peekaboo hotkey --keys "cmd,b" --app "$BID"
```
Parse the child exit details and inspect `contact.png`, `metadata.json`, and
the MP4. Artifact creation alone does not prove the expected transition.
## Reliability and safety
- Prefer state assertions over blind sleeps. If animation has no AX-visible
completion state, use one short bounded settle and recapture.
- If a fresh `see` still shows a just-mutated tree, wait briefly: rapid
captures can reuse a short-lived AX cache.
- If capture or input works in Terminal but not the agent, inspect:
```bash
peekaboo permissions status --all-sources --json
peekaboo bridge status --verbose --json
peekaboo daemon status --json
```
- Custom-rendered terminals/canvases can be AX-opaque. Verify their pixels
directly.
- `set-value` rejects secure fields. Keep secrets out of shell arguments,
JSON, screenshots, and recordings. Require user mediation for credentials
and explicit confirmation before irreversible actions.
- Each `see` creates `~/.peekaboo/snapshots/<UUID>/`. Clean old entries with
`peekaboo clean --older-than 24`.
## When to use XCUITest
Use Peekaboo for interactive product validation: "does this workflow work and
look right?" Use XCUITest for deterministic regression gates. They complement
each other; do not replace durable automated tests with an agent-only flow.
## Live discovery
Prefer current CLI help over copied command catalogs:
```bash
peekaboo learn
peekaboo <command> --help
peekaboo permissions status --all-sources --json
peekaboo bridge status --verbose --json
peekaboo daemon status --json
```