# Show HN: Shotlist – Make your AI agent prove its work with real screenshots

> Source: <https://github.com/varmabudharaju/shotlist>
> Published: 2026-06-26 04:05:25+00:00

**Screenshots for your docs — as code.** One committed shot list captures your
web pages, your *real* terminal windows, and stateful CLI sessions — and
regenerates them all with a single command.

[The problem](#the-problem)[Quickstart](#quickstart)[One shot list, four kinds of shot](#one-shot-list-four-kinds-of-shot)[Use cases](#use-cases)[Proof reports & pipelines](#proof-reports--pipelines)[Why shotlist, and not the others](#why-shotlist-and-not-the-others)[How it works](#how-it-works)[Use with Claude](#use-with-claude)[Commands](#commands)[Develop](#develop)

Documenting a feature means launching the app, clicking to the right state,
screenshotting, naming the file, and embedding it — **every time the UI changes.**
The screenshots drift out of date the moment you ship, and nobody notices until
they're embarrassingly wrong.

`shotlist`

makes them **reproducible**: describe *how to start your app* and *what
to shoot* once, in a committed `.shotlist.yaml`

, then regenerate the whole set on
demand — locally or in CI. Same config + same app state → same screenshots.

```
pip install shotlist             # installs the `shotlist` command
playwright install chromium      # one-time browser download

shotlist init        # writes a starter .shotlist.yaml
shotlist run         # boots your app, captures every shot, tears it all down
output:
  dir: docs/screenshots
  readme: README.md            # optional: splice <img> snippets straight into the README

app:                           # optional — omit for static sites or pure-CLI shots
  command: "npm run dev"
  ready: { url: http://localhost:5173, timeout: 30 }   # never shoot a half-booted app

shots:
  - { name: dashboard, kind: web, url: http://localhost:5173/dashboard, full_page: true, alt: "Dashboard" }
  - { name: cli-help,  kind: cli, command: "mytool --help", alt: "Top-level help" }
```

| Kind | Captures | How |
|---|---|---|
`web` |
a browser page — with optional click/fill/wait steps first | Playwright / Chromium |
`cli` · `native` (macOS default) |
a real screenshot of your Terminal.app window — your font, your theme |
AppleScript + `screencapture` |
`cli` · `rendered` (any OS, CI-safe) |
the command's output drawn as a styled terminal card | PTY → ANSI→HTML → Chromium |
`session` |
a stateful, multi-command flow in one persistent terminal — one shot per step |
one Terminal window, captured after each step |

A `session`

is how you screenshot a flow whose later steps depend on earlier ones —
the shell state (cwd, env, background processes) carries across. Background a
long-running process with `&`

and a small `wait_ms`

, keep capturing, and the
session tears it down on close.

`shotlist`

fits anywhere a screenshot would otherwise go stale:

**README & docs screenshots**— the core: regenerate the whole set on every UI change.** Test-evidence / proof**— capture a feature flow step by step (a`session`

) and share the generated`index.html`

as proof it works.**CI drift-checking**—`shotlist check`

fails the build when a screenshot changes unexpectedly (with a visual`--diff`

).**Blog posts & tutorials**— polished web*and*CLI shots from one config.**Onboarding & demo galleries**— versioned sets you keep across releases.** Long-running processes**— background a dev server with`&`

+`wait_ms`

and shoot it live.

Each one has a complete, copy-paste `.shotlist.yaml`

in the recipes cookbook,
.

`docs/recipes.md`

Every `shotlist run`

also writes, next to the PNGs:

— a self-contained gallery you can open and share as a`index.html`

**proof report**;— a machine-readable record of the run (a pipeline artifact).`manifest.json`

Attach `manifest.json`

to a CI job, or open `index.html`

as test-evidence. Gate CI
with ** shotlist check** — it re-captures and fails when a screenshot drifts from
the committed baseline (

`shotlist check --update`

to accept intended changes; add
`--diff DIR`

to render baseline·current·diff images) — or drop in the bundled
**GitHub Action**. Turn the report off with

`--no-report`

(or `output.report: false`

). Details in **.**

`docs/pipeline.md`

The pieces exist in isolation; `shotlist`

is the one tool that does all of it under
a single committed config.

| web pages | real terminal | CLI sessions | README auto-embed | reproducible / CI | |
|---|---|---|---|---|---|
shotlist |
✅ | ✅ | ✅ | ✅ | ✅ |
| shot-scraper | ✅ | ❌ | ❌ | ❌ | ✅ |
| freeze / carbon | ❌ | synthetic | ❌ | ❌ | ✅ |
| Percy / Chromatic | ✅ | ❌ | ❌ | ❌ | ✅ (cloud, paid) |
| doing it by hand | 😖 | 😖 | 😖 | ❌ | ❌ |

No cloud, no paid services, no special OS permissions for web/rendered shots. (Native Terminal capture needs macOS Screen-Recording permission; everything else needs nothing.)

```
.shotlist.yaml ─► load + validate ─► [ boot app, wait until ready ] ─► one engine
                                                                        routes each
                                                                        shot by kind:
        web ───────► Playwright / Chromium
        cli·native ► a real Terminal.app window
        cli·render ► PTY → ANSI→HTML → Chromium
        session ───► one persistent Terminal, a shot per step
                                                                      ─► NN-name.png
                                                                         + README splice
```

The clever part is what *isn't* here: **no AI runs at capture time.** Claude's only
job is to *author* the `.shotlist.yaml`

once by reading your repo; after that the
engine is a plain, deterministic program — fast, free, and re-runnable in CI with
no model in the loop. See the full design in [ docs/design.md](/varmabudharaju/shotlist/blob/main/docs/design.md).

**Robust by design.** The readiness probe (HTTP / TCP port / log line) means you
never screenshot a half-booted app, and the app is launched in its own process
group and torn down — even on a crash or Ctrl-C — so a shotlist run never leaves an
orphaned dev server behind.

This repo dogfoods itself: the shots below are produced by running `shotlist run`

on its own [ .shotlist.yaml](/varmabudharaju/shotlist/blob/main/.shotlist.yaml) and spliced in automatically.

`shotlist`

ships an optional Claude integration in [ integrations/claude/](/varmabudharaju/shotlist/blob/main/integrations/claude):

- a
that inspects your repo (routes,`/shotlist`

skill`--help`

, README), writes the`.shotlist.yaml`

for you, and runs it; - an optional
**auto-snapshot hook** that drops a raw snapshot when a dev server starts (the honest "dumb snapshot"; the curated set always comes from`shotlist run`

).

| Command | What it does |
|---|---|
`shotlist init` |
Scaffold a starter `.shotlist.yaml` |
`shotlist validate` |
Check the shot list is well-formed |
`shotlist run` |
Capture every shot and write outputs |
`shotlist run --only dashboard` |
Capture a single shot by name |
`shotlist run --version v2` |
Write into a versioned subfolder |
`shotlist check` |
Fail if a screenshot drifted from the committed baseline |
`shotlist check --update` |
Re-shoot and accept the current screenshots as the baseline |
`shotlist check --diff DIR` |
Also render baseline·current·diff images for changed shots |

```
git clone https://github.com/varmabudharaju/shotlist && cd shotlist
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
playwright install chromium
pytest                       # the suite is fully offline
```

CI runs ruff, mypy, and pytest on Python 3.11 and 3.12. A separate
** verify-action** workflow dogfoods the bundled GitHub Action on every PR —
running

`shotlist run`

then `shotlist check`

on a Linux runner — so a regression in
the action is caught before it ships. Releases publish to PyPI automatically via
Trusted Publishing.The hero GIF is itself reproducible — [ demo.tape](/varmabudharaju/shotlist/blob/main/demo.tape) +

`vhs demo.tape`

.MIT © Varma Budharaju
