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

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

Metadata

Release files for shotlist 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shotlist 0.4.0
File Size Uploaded
shotlist-0.4.0.tar.gz 283.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shotlist 0.4.0
File Interpreter ABI Platform
shotlist-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 521.0 kB

Release files / shotlist-0.4.0.tar.gz

Download URL shotlist-0.4.0.tar.gz
Size 283.3 kB
Tags Source
SHA-256 checksum
How to use checksums
25fa639a5476fab8c561cd3cba1175e310a6109c6ff95c19faf36a11deba0853
BLAKE2b-256 checksum
How to use checksums
c9f8501c96e7cf182c29f661e38b6f84b24e0a71c56f3c25c327ae565b0ccce9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release files / shotlist-0.4.0-py3-none-any.whl

Download URL shotlist-0.4.0-py3-none-any.whl
Size 237.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1b5104d5290533d7e0dfa1dc8d19a4156a44ad19eea7862bcee50f543f79b5b9
BLAKE2b-256 checksum
How to use checksums
72d0fbb163f47296142fa6c50f52939092110003ff848c9cad598572ae5da39d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page