Skip to content
Back to skills

Peekaboo

ASecurity

Drive, observe, and critique any running macOS app end-to-end via Peekaboo — the macOS-native equivalent of Playwright + visual-regression review for SwiftUI/AppKit. Use proactively whenever the user mentions UI, screens, screenshots, the app's appearance, layout, spacing, alignment, copy/labels, sidebar, panels, settings, theme, accessibility identifiers, clicking, typing, menus, file pickers, responsive sizing, or animations — or says any of: 'verify it works', 'see what it looks like', 'sh...

  • 11 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
developmentgoswiftbashdebuggingapibackendsecurity

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add johnkozaris/jko-claude-plugins --skill peekaboo --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Peekaboo?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Peekaboo
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/johnkozaris-peekaboo/badge)](https://www.skillsdirectory.com/skills/johnkozaris-peekaboo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: peekaboo
description: "Drive, observe, and critique any running macOS app end-to-end via Peekaboo — the macOS-native equivalent of Playwright + visual-regression review for SwiftUI/AppKit. Use proactively whenever the user mentions UI, screens, screenshots, the app's appearance, layout, spacing, alignment, copy/labels, sidebar, panels, settings, theme, accessibility identifiers, clicking, typing, menus, file pickers, responsive sizing, or animations — or says any of: 'verify it works', 'see what it looks like', 'show me the UI', 'click X', 'take a screenshot', 'critique the design', 'is this still balanced', 'drive the app'. Use after any SwiftUI/AppKit edit, after a rebuild, when fixing a UI bug, or before claiming a UI feature done — even when the user does not explicitly ask. The agent reads pixels itself via `view`; no external AI keys are used."
homepage: https://peekaboo.boo
metadata:
  os: ["darwin"]
  requires:
    bins: ["peekaboo"]
    permissions: ["Screen Recording", "Accessibility"]
  install:
    - kind: brew
      formula: steipete/tap/peekaboo
---

# Peekaboo — drive, observe, and **critique** any macOS app

> **The job is not "is this clickable". The job is "does it work, *and* does
> it look right". Every screenshot deserves a critique pass.**

Peekaboo is a single Swift CLI that combines screenshot, AX-tree inspection,
synthetic input, and change-aware video capture against macOS apps. It's
the macOS-native equivalent of Playwright + visual-regression review.

You invoke `peekaboo` directly — it's a full toolkit, not a fixed menu of
recipes. Build orchestration belongs to your project's task runner
(Justfile, Makefile, `xcodebuild`, etc.); UI driving is your domain.

**No external AI keys. Ever.** You are the LLM. When you need to "look at"
the screenshot, `view` the PNG yourself. Skip `peekaboo agent`,
`peekaboo see --analyze`, `peekaboo image --analyze`, `peekaboo analyze`
— they all require an external API key.

## When to invoke this skill

- ✅ User says "see what it looks like", "show me the UI", "verify it works",
  "click X", "type Y", "take a screenshot", "drive the app", "test it
  end-to-end"
- ✅ After modifying any SwiftUI/AppKit view — verify it renders, works, and
  *still looks intentional*
- ✅ When debugging a UI bug — capture before/after + critique each
- ✅ Before claiming a UI feature done in autopilot mode
- ❌ Skip if the change is purely backend / non-UI

## Prerequisites

```bash
peekaboo --version                           # ≥ 3.0
peekaboo list permissions --json             # both must be granted
```

If peekaboo is not installed:
```bash
brew install steipete/tap/peekaboo
```

Then grant **Screen Recording** AND **Accessibility** to your terminal in
System Settings → Privacy & Security. Both are required.

## The loop (copy this checklist into your response and tick items off)

- [ ] **Build** the app — use the project's task runner
- [ ] **Drive** — get the app to the state under test
- [ ] **Snapshot** — `peekaboo see --json --annotate --path /tmp/k.png`
- [ ] **CRITIQUE** — `view /tmp/k_annotated.png` inline; score it against the
      rubric. Optionally chain a design skill (`critique`/`polish`/`layout`)
- [ ] **Fix** any findings
- [ ] **Re-verify** — repeat snapshot + critique; diff against previous PNG

---

## Discovering the target app

Before running anything, you need the **bundle ID** (preferred) or app name:

```bash
# Find a running app's bundle ID
peekaboo list apps --json | jq '.data.applications[]|select(.name|test("YourApp";"i"))|{name,bundleIdentifier,processIdentifier}'

# Always prefer --bundle-id over --app <DisplayName>:
#   - bundle ID is stable (e.g. com.example.myapp)
#   - the running process registers as CFBundleName, which often differs
#     from the Xcode scheme. Using the scheme name fails APP_NOT_FOUND.
```

Cache the bundle ID and any display-name caveats in your project's
`AGENTS.md` / `CLAUDE.md` so future runs don't re-discover them.

---

## Terminology (one-to-one with peekaboo's command names)

| Word           | Means                                                    | Command                  |
|----------------|----------------------------------------------------------|--------------------------|
| **Snapshot**   | Annotated PNG **plus** AX-tree JSON (the "DOM+screenshot") | `peekaboo see`         |
| **Screenshot** | Raw PNG only, no AX tree                                 | `peekaboo image`         |
| **Recording**  | Change-aware MP4 video                                   | `peekaboo capture live`  |

Default to **snapshot** (`peekaboo see`) — the AX JSON lets the next step
click by element ID without re-parsing the screen.

---

## Step 1 — Drive the UI

The full driving surface (run `peekaboo <cmd> --help` for details, or
`peekaboo learn` for the comprehensive guide):

| Need                                  | Command                                                  |
|---------------------------------------|----------------------------------------------------------|
| Launch / quit app                     | `peekaboo app launch --bundle-id $BID --wait-until-ready`<br>`peekaboo app quit --app $BID` |
| Inspect UI (annotated PNG + JSON)     | `peekaboo see --app $BID --json --annotate --path /tmp/k.png > /tmp/k.json` |
| Click by AX identifier (most robust)  | See "Click by `.accessibilityIdentifier`" recipe below   |
| Click by visible label                | `peekaboo click "Settings" --app $BID`                   |
| Click by element ID from `see`        | `peekaboo click --on elem_42 --snapshot $SID --app $BID` |
| Type text                             | `peekaboo type "hello" --app $BID`<br>(append `--return` to press Enter) |
| Single key                            | `peekaboo press return` / `peekaboo press escape`        |
| Hotkey                                | `peekaboo hotkey --keys "cmd,b" --app $BID`              |
| **Menu bar**                          | `peekaboo menu list --app $BID` (discover)<br>`peekaboo menu click --app $BID --path "View > Toggle Sidebar"` |
| **System file/save dialog**           | `peekaboo dialog list --app $BID`<br>`peekaboo dialog click --button "Open" --app $BID`<br>`peekaboo dialog input --text "..." --field "..." --app $BID`<br>`peekaboo dialog file --path "/Users/me" --select "Open" --app $BID` |
| **Scroll inside a list / pane**       | `peekaboo scroll --direction down --amount 5 --on elem_N` |
| **Drag-and-drop**                     | `peekaboo drag --from elem_A --to elem_B --app $BID`     |
| **Resize/reposition window**          | `peekaboo window set-bounds --app $BID --x 0 --y 0 --width 1024 --height 640` |
| **Read/write clipboard**              | `peekaboo clipboard get` / `peekaboo clipboard set --text "x"` |
| **Paste from clipboard**              | `peekaboo paste --app $BID`                              |
| **Open URL / deep link**              | `peekaboo open "x-myapp://..."`                          |
| **Replay a saved scenario**           | `peekaboo run scripts/onboarding.peekaboo.json`          |

Throughout this skill, `$BID` is the target app's bundle ID
(`com.example.myapp`).

### Click by `.accessibilityIdentifier` — the killer pattern

If your SwiftUI / AppKit code tags interactive views with
`.accessibilityIdentifier(...)`, Peekaboo surfaces those exact strings as
`ui_elements[].identifier`. Queries are then immune to copy changes:

```bash
peekaboo see --app $BID --json --annotate --path /tmp/k.png > /tmp/k.json

SID=$(jq -r .data.snapshot_id /tmp/k.json)
ID=$(jq -r '.data.ui_elements[]|select(.identifier=="header.utility.settings").id' /tmp/k.json)

peekaboo click --on "$ID" --snapshot "$SID" --app $BID
```

If you reach for a copy-based click (`peekaboo click "Some Label"`) more
than once on the same control, **add a stable identifier** in your project's
chosen namespace (e.g. `header.tab.sessions`, `panel.settings.save`,
`row.<id>.delete`). Both peekaboo queries and any XCUITest become resilient
to copy changes:

```swift
Button("Settings") { … }
    .accessibilityIdentifier("header.utility.settings")
```

Document the namespace in your project's `AGENTS.md` so additions stay
consistent.

---

## Step 2 — Capture (anatomy of `peekaboo see`)

```bash
peekaboo see --app $BID --json --annotate --path /tmp/k.png > /tmp/k.json
```

This writes **two PNGs**:
- `/tmp/k.png` — raw screenshot
- `/tmp/k_annotated.png` — same image overlaid with `elem_N` markers
  (this is the one you usually want to `view`)

…and prints a JSON envelope `{success, data, debug_logs}` where `data` has:

| Field                  | Meaning                                                |
|------------------------|--------------------------------------------------------|
| `snapshot_id`          | UUID — pass to `--snapshot` for click stability        |
| `screenshot_raw`       | path to `/tmp/k.png`                                   |
| `screenshot_annotated` | path to `/tmp/k_annotated.png`                         |
| `application_name`     | display name (CFBundleName)                            |
| `window_title`         | currently focused window                               |
| `capture_mode`         | e.g. `window`                                          |
| `is_dialog`            | true if the captured surface is a modal dialog         |
| `element_count`        | total AX elements                                      |
| `interactable_count`   | how many are `is_actionable`                           |
| `ui_elements`          | array of elements (see fields below)                   |
| `ui_map`               | absolute path to a richer cache `snapshot.json`        |

Each `ui_elements` entry exposes:

```json
{
  "id": "elem_42",
  "role": "button",
  "role_description": "button",
  "title": "Settings",
  "label": "Settings",
  "description": "Open settings panel",
  "help": "",
  "identifier": "header.utility.settings",
  "is_actionable": true,
  "keyboard_shortcut": ""
}
```

`identifier` is the SwiftUI / AppKit `.accessibilityIdentifier(...)`.
Bounding boxes live on disk (`ui_map` path) under
`uiMap.<id>.frame = [[x,y],[w,h]]` if you need pixel-precise targeting.

Other capture commands:

```bash
# Just a PNG, retina, window-only — no AX overhead
peekaboo image --mode window --app $BID --retina --path /tmp/shot.png

# Animation/transition: change-aware MP4, 5s
peekaboo capture live --app $BID --duration 5 \
  --video-out /tmp/anim.mp4 --threshold 1.0 --highlight-changes
```

The snapshot cache lives at `~/.peekaboo/snapshots/<UUID>/`
(`raw.png`, `annotated.png`, `snapshot.json`). Default keeps 24h;
`peekaboo clean --older-than 24` purges. Each `see` call creates a new UUID.

---

## Step 3 — **CRITIQUE** (mandatory, not optional)

Click-verification proves a button is hooked up. It does not prove the UI
is good. After every meaningful snapshot, critique it.

**You are the LLM — read the pixels yourself.**

```
1. peekaboo see --app $BID --json --annotate --path /tmp/k.png > /tmp/k.json
2. view /tmp/k_annotated.png              # read it into your context
3. (optional) chain a design skill — see catalog below
4. Write the critique using the rubric template at the end of this section
```

### Design skills available for deeper passes

When the snapshot exposes a specific weakness, chain to the matching skill
instead of writing the whole critique from scratch:

| Concern surfaced by the snapshot              | Skill to chain |
|-----------------------------------------------|----------------|
| Visual quality, hierarchy, anti-pattern smell | `critique`     |
| Accessibility, theming, perf, anti-patterns   | `audit`        |
| Final pre-ship pass: alignment, spacing nits  | `polish`       |
| Layout/grid/spacing rhythm specifically       | `layout`       |
| Typography hierarchy, sizing, readability     | `typeset`      |
| Color, palette, contrast                      | `colorize`     |
| Microcopy clarity                             | `clarify`      |
| The SwiftUI *code* producing the issue        | `swift-critique` / `swiftui-expert` |

Apply the recommended fixes, then loop back to Step 1.

### What a critique pass answers

- **Did the change achieve its intent?** (e.g. user asked for "more breathing
  room" — does the screenshot actually have more breathing room?)
- **Visual hierarchy**: where does the eye land first? Is that the primary
  action?
- **Alignment & spacing**: are elements on a consistent grid? Inconsistent
  gaps between siblings?
- **Contrast & legibility**: WCAG AA on critical text? Disabled states still
  readable?
- **Copy clarity**: any jargon, truncation, ambiguous labels?
- **Anti-patterns**: AI-slop tells, generic gradient buttons, hero-stat
  layouts that don't fit the data?
- **Edge cases captured?** Empty state, loading, error, overflow.

If "did the change achieve intent" is "I don't know without seeing it",
snapshot more states (hover, focus, error, empty) and re-critique.

### Example: what a finished critique reads like

A good critique appended to your response is concrete, scored, and ranked.
Match this shape:

> **Critique of `/tmp/k_annotated.png` (Settings panel, 1440×900)**
>
> | Dimension          | Score | Note                                            |
> |--------------------|------:|-------------------------------------------------|
> | Visual hierarchy   |   4/5 | Primary action ("Save") clearly leads.          |
> | Alignment          |   3/5 | Section labels off the 8px grid by 2–4px.       |
> | Spacing rhythm     |   4/5 | Consistent except around the toggle row.        |
> | Contrast           |   5/5 | All copy ≥ AA on the dark surface.              |
> | Copy clarity       |   3/5 | "Auto-stage" reads as a verb but sits as label. |
> | AI-slop tells      |   ✓   | None — no gradient text, glassmorphism, etc.    |
>
> **Top 3 fixes ranked by impact**
> 1. Snap section labels to the 8px grid (alignment 3 → 5).
> 2. Rewrite "Auto-stage" → "Stage new sessions automatically" (copy 3 → 5).
> 3. Tighten the toggle/description gap from 14px to 8px so the toggle
>    visually owns its description.
>
> **Did the change achieve intent?** Yes — the new spacing reads calmer than
> the before/after diff in `/tmp/before.png`.

This shape is the contract. Anything less detailed is undercooked.

---

## Step 4 — Fix and re-verify

After fixing, re-run Steps 1–3 and diff:

```bash
open /tmp/before.png /tmp/after.png            # eyeball
compare /tmp/before.png /tmp/after.png /tmp/diff.png   # ImageMagick
```

---

## Responsive verification (test at multiple sizes)

Don't assume the layout works at 1440×900 just because it works at your
window size:

```bash
for size in "1024 640" "1280 800" "1440 900" "1920 1200"; do
  W=${size%% *}; H=${size##* }
  peekaboo window set-bounds --app $BID --x 0 --y 0 --width $W --height $H
  sleep 0.3
  peekaboo see --app $BID --json --annotate \
    --path /tmp/k-${W}x${H}.png > /tmp/k-${W}x${H}.json
done
# Then `view` each annotated PNG and critique inline.
```

---

## Common scenarios

### Verify a tab switch + critique the resulting view

```bash
peekaboo click "Friends" --app $BID
sleep 0.5
peekaboo see --app $BID --json --annotate \
  --path /tmp/friends.png > /tmp/friends.json
# `view /tmp/friends_annotated.png` and critique the empty state inline.
```

### Drive a system file picker

```bash
# Trigger the action that opens NSOpenPanel/NSSavePanel, then:
peekaboo dialog file --path "$HOME/Documents" --select "Open" --app $BID
sleep 0.5
peekaboo see --app $BID --json --annotate --path /tmp/post-pick.png > /tmp/post-pick.json
```

### Verify a "Copy …" button actually copies

```bash
peekaboo clipboard clear
peekaboo click "Copy session ID" --app $BID
sleep 0.2
peekaboo clipboard get   # should print the expected ID
```

### Capture a transition for review

```bash
peekaboo capture live --app $BID --duration 4 \
  --video-out /tmp/transition.mp4 --threshold 1.0 --highlight-changes &
sleep 0.3
peekaboo hotkey --keys "cmd,b" --app $BID  # trigger the transition
wait
# Open the MP4, look for jank, compare frame timing.
```

---

## Pitfalls

- **Bundle ID vs display name vs Xcode scheme**: the running process
  registers as `CFBundleName`, which often *differs* from the Xcode scheme.
  Always use `--bundle-id` or the actual `CFBundleName`. Cache this in
  `AGENTS.md`.
- **Composite views broadcast their identifier**: applying
  `.accessibilityIdentifier("panel.settings")` to a view whose body contains
  many controls makes several AX elements share that identifier. Fine for
  presence checks; for child-specific clicks, give the child its own
  identifier or use `.accessibilityElement(children: .contain)`.
- **Icon-only buttons** (`Image(systemName: …)` inside a `Button`) often
  expose no AX label by default. Add `.accessibilityLabel("…")` so peekaboo
  / XCUITest can target them.
- **Animation settle time**: SwiftUI `withAnimation(...)` durations need a
  matching `sleep` after triggering — usually 0.3–0.5s — before the next
  click, or use `peekaboo click --wait-for 1500`.
- **Custom-rendered panes** (e.g. terminals, canvases) render into views
  whose contents are opaque to AX. For text verification, take a
  `peekaboo image` and `view` the PNG — read the rendered text directly.
- **Snapshot UUIDs accumulate**: each `see` creates a new
  `~/.peekaboo/snapshots/<UUID>/` (≈ 100–500 KB). Run
  `peekaboo clean --older-than 24` periodically.

---

## When to use XCUITest instead

Peekaboo is for **interactive iteration** ("did my change work, and does it
look right?"). XCUITest is for **deterministic regression gates** ("does
this still work?"). Both coexist; identifiers added for peekaboo make
XCUITest queries simpler too.

---

## Discovery / health checks

```bash
peekaboo --version                                                     # ≥ 3.0
peekaboo list permissions --json | jq '.permissions[]|{name,isGranted}'
peekaboo list apps --json | jq '.data.applications[]|{name,bundleIdentifier}|select(.name|test("YourApp";"i"))'
peekaboo menu list --app $BID                                          # menu bar
peekaboo learn                                                         # full reference
peekaboo <cmd> --help                                                  # any subcommand
```

If a project keeps wanting the same recipe, *don't bake it into the task
runner* — recipes constrain the agent. Document the pattern in the project's
`AGENTS.md` instead and let the agent invoke `peekaboo` directly.

Files in this skill

  • SKILL.md18.3 KB
  • evals/evals.json2.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.