Skip to main content

shotlist

CI verify-action Python 3.11+ License: MIT

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 old way: dragging Screen Shot 2026-... files into ever-more-cursed filenames, then shipping a UI tweak that makes them all stale. The shotlist way: one `shotlist run`.

Contents

The problem

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.

Quickstart

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

Features at a glance

Everything is driven by one committed .shotlist.yaml; each row links to the docs that go deeper.

You want to… Use Where it's explained
Screenshot a web page (after clicks/fills/waits) kind: web + steps: four kinds of shot
Screenshot your real Terminal.app window kind: cli (style: native, macOS) four kinds of shot
Terminal shots that run anywhere, incl. CI style: rendered four kinds of shot
Capture a stateful multi-step flow, one image per step kind: session (add style: rendered for CI) four kinds of shot
Boot the app first and never shoot it half-ready app: + ready: (url / port / log line) how it works
Auto-embed the images in your README output.readme: README.md proof reports
Share a proof gallery / test-evidence doc index.html (automatic), output.evidence proof reports
Fail CI when a screenshot drifts shotlist check (+ bundled GitHub Action) catch drift, docs/pipeline.md
Get the drift verdict as a PR comment Action input pr-comment: "true" docs/pipeline.md
Tolerate sub-pixel rendering jitter check.max_diff_pixel_ratio: 0.001 docs/pipeline.md
Hide flaky page regions / scrub timestamps & PIDs mask: (web), scrub: (cli & session) deterministic by default
Re-attempt a flaky capture retries: 2 on the shot robust by design
Finish a partial run instead of dying on one failure shotlist run --keep-going robust by design
Keep committed PNGs small output.optimize: true (+ Git LFS) recipes #9
Keep versioned sets across releases shotlist run --version v2 docs/recipes.md

One shot list, four kinds of shot

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 native Terminal window (macOS) or rendered terminal cards (any OS, CI-safe), one capture per 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. Off macOS — or with style: rendered — the whole session is drawn as terminal cards from a persistent PTY instead of a real Terminal window: no OS permissions, CI-safe, and the same everywhere.

Use cases

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-checkingshotlist 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.

Proof reports & pipelines

Every shotlist run also writes, next to the PNGs:

  • index.html — a self-contained gallery you can open and share as a proof report;
  • manifest.json — a machine-readable record of the run (a pipeline artifact).
The generated index.html gallery: a title with the shot count and timestamp, then a card per shot showing the screenshot, its name, a kind badge, its alt text, and the command or URL that produced it.

Attach manifest.json to a CI job, or open index.html as test-evidence. Set output.title to relabel the gallery heading, and output.evidence to also splice a captioned Markdown test-evidence doc — its own file, distinct from output.dir (where the PNGs land). Turn the report off with --no-report (or output.report: false). Set output.optimize: true to losslessly re-encode every written PNG through Pillow — smaller files, identical pixels, off by default so existing baselines never drift (pairs well with Git LFS; see recipes #9).

Catch drift before your users do

Gate CI with shotlist check — it re-captures and fails when a screenshot drifts from the committed baseline, telling you exactly how much moved:

shotlist check output: service-status changed (1.14% pixels differ) with an arrow to its diff image, queue-drain unchanged, and a pointer to check-report.html

Drift comes with receipts. --diff DIR renders a baseline·current·diff 3-up per changed shot, plus a check-report.html that lists every shot with a status badge — open it locally or grab it from the CI artifact the bundled GitHub Action uploads (along with a step summary on the run page):

check-report.html: service-status flagged CHANGED (1.14% pixels differ) with its baseline, current, and highlighted-diff images inline; queue-drain badged UNCHANGED

Bless intended changes with shotlist check --update (or --update --only NAME for one shot), set check.max_diff_pixel_ratio to tolerate sub-pixel jitter, and script against check --json. The whole loop, end to end:

Flow diagram: shotlist run creates the baseline; PNGs and manifest.json are committed; CI runs shotlist check on every PR — no drift merges, drift opens check-report.html; intended changes are re-blessed with check --update, real regressions get fixed and re-checked

Details in docs/pipeline.md.

Why shotlist, and not the others

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.)

How it works

One deterministic engine: load and validate the shot list, boot your app and wait until it's actually ready, then route every shot to the right backend — and tear everything down afterwards, even on a crash:

Flow diagram: the engine routes each shot by kind — web goes to Playwright/Chromium; cli goes to a rendered terminal card (PTY, scrub, ANSI to HTML, Chromium) or a real Terminal.app window depending on style; session drives one persistent Terminal window — all paths produce PNG bytes written as NN-name.png

The clever part is what isn't here: no AI runs at capture time. An AI assistant'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 (and no tokens) in the loop.

Want the full picture? docs/how-it-works.md walks every stage with flow diagrams — the run pipeline, how shots route to backends, what one run does step by step, the check drift loop, and the determinism layers that make the same config produce the same pixels. The design rationale lives in 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. A single failed shot stops the run with one clean error line (no traceback); shotlist run --keep-going instead captures everything it can and reports captured N shot(s), M failed at the end (exit 1 on any failure). Either way the manifest, gallery, and README splice come from the successful shots only. Give a flaky web or cli shot retries: N (0–5, default 0) to re-attempt a failed capture before it counts.

Deterministic by default. Web shots can mask flaky regions (mask: [selector, ...]) and always capture with CSS animations disabled; CLI shots — and now rendered session shots — can scrub non-deterministic text (durations, timestamps, PIDs) with a regex before rendering; and rendered CLI cards embed JetBrains Mono. A session with style: rendered (the default off macOS) runs in a persistent PTY and draws each step as a terminal card, so its steps are deterministic and drift-checkable too, not just single cli shots. Baselines now match byte-for-byte across macOS and Linux CI, not just on the machine that made them.

shotlist, captured by shotlist

This repo dogfoods itself: the shots below are produced by running shotlist run on its own .shotlist.yaml and spliced in automatically.

The shotlist CLI

shotlist --help showing the init, validate, run, and check commands

Run options

shotlist run options: --config, --only, and --version

session export

Step 1: set a variable in the session shell

session echo

Step 2: a later command sees it — one persistent shell

Works with any AI agent — or none

shotlist is not tied to any AI tool. The config is plain YAML and the engine is a plain CLI, so a human can write the shot list by hand — and any coding agent that can run shell commands can author and drive it. Optional integrations ship in integrations/:

Your tool Integration How
Claude Code integrations/claude/ a /shotlist skill that inspects the repo, writes the .shotlist.yaml, and runs it; plus an optional dev-server auto-snapshot hook
Codex (and any AGENTS.md-reading agent) integrations/agents/AGENTS.md paste the snippet into your repo's AGENTS.md — teaches the same author-validate-run-check workflow
Cursor integrations/cursor/shotlist.mdc copy to .cursor/rules/shotlist.mdc — applied whenever screenshots come up
No AI shotlist init scaffolds the config; the recipes are copy-paste-complete

Whichever authors the config, the result is identical: capture is deterministic, local, and token-free, so shotlist run/check behave exactly the same from a terminal, a CI job, or any agent's shell.

Commands

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 --keep-going Continue past a failed shot and report all failures at the end (exit 1 if any)
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
shotlist check --json Emit the drift report as JSON on stdout (human output moves to stderr)
shotlist check --update --only NAME Re-bless just one shot in place (repeatable)

Develop

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 — with an 85% coverage gate — on Ubuntu (Python 3.11, 3.12) and macOS (Python 3.12), so native Terminal capture stays covered too. A separate verify-action workflow dogfoods the bundled GitHub Action on every PR two ways: verify-release smoke-tests the shipped @v0.4.0 action + PyPI package, and verify-source runs the PR's own action.yml against its own source (package: -e .) — so a regression in either is caught before it ships. Releases publish to PyPI automatically via Trusted Publishing.

The hero GIF is itself reproducible — demo.tape + vhs demo.tape.

License

MIT © Varma Budharaju

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shotlist-0.4.0.tar.gz (283.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shotlist-0.4.0-py3-none-any.whl (237.7 kB view details)

Uploaded Python 3

File details

Details for the file shotlist-0.4.0.tar.gz.

File metadata

  • Download URL: shotlist-0.4.0.tar.gz
  • Upload date:
  • Size: 283.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for shotlist-0.4.0.tar.gz
Algorithm Hash digest
SHA256 25fa639a5476fab8c561cd3cba1175e310a6109c6ff95c19faf36a11deba0853
MD5 7a7dccecf799ae3e87b24696e97057f3
BLAKE2b-256 c9f8501c96e7cf182c29f661e38b6f84b24e0a71c56f3c25c327ae565b0ccce9

See more details on using hashes here.

Provenance

The following attestation bundles were made for shotlist-0.4.0.tar.gz:

Publisher: publish.yml on varmabudharaju/shotlist

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shotlist-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: shotlist-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 237.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for shotlist-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b5104d5290533d7e0dfa1dc8d19a4156a44ad19eea7862bcee50f543f79b5b9
MD5 10cd320057ccd440a716b4d2014dc98f
BLAKE2b-256 72d0fbb163f47296142fa6c50f52939092110003ff848c9cad598572ae5da39d

See more details on using hashes here.

Provenance

The following attestation bundles were made for shotlist-0.4.0-py3-none-any.whl:

Publisher: publish.yml on varmabudharaju/shotlist

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page