shotlist
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.
Contents
- The problem
- Quickstart
- Features at a glance
- One shot list, four kinds of shot
- Use cases
- Proof reports & pipelines
- Why shotlist, and not the others
- How it works
- Works with any AI agent — or none
- Commands
- Develop
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 generatedindex.htmlas proof it works. - CI drift-checking —
shotlist checkfails 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_msand 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).
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:
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):
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:
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:
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
Run options
session export
session echo
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25fa639a5476fab8c561cd3cba1175e310a6109c6ff95c19faf36a11deba0853
|
|
| MD5 |
7a7dccecf799ae3e87b24696e97057f3
|
|
| BLAKE2b-256 |
c9f8501c96e7cf182c29f661e38b6f84b24e0a71c56f3c25c327ae565b0ccce9
|
Provenance
The following attestation bundles were made for shotlist-0.4.0.tar.gz:
Publisher:
publish.yml on varmabudharaju/shotlist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shotlist-0.4.0.tar.gz -
Subject digest:
25fa639a5476fab8c561cd3cba1175e310a6109c6ff95c19faf36a11deba0853 - Sigstore transparency entry: 2101931850
- Sigstore integration time:
-
Permalink:
varmabudharaju/shotlist@c17fe5e52962cf1b58aa2aafbb52eaa3f358f5e9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/varmabudharaju
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c17fe5e52962cf1b58aa2aafbb52eaa3f358f5e9 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b5104d5290533d7e0dfa1dc8d19a4156a44ad19eea7862bcee50f543f79b5b9
|
|
| MD5 |
10cd320057ccd440a716b4d2014dc98f
|
|
| BLAKE2b-256 |
72d0fbb163f47296142fa6c50f52939092110003ff848c9cad598572ae5da39d
|
Provenance
The following attestation bundles were made for shotlist-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on varmabudharaju/shotlist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shotlist-0.4.0-py3-none-any.whl -
Subject digest:
1b5104d5290533d7e0dfa1dc8d19a4156a44ad19eea7862bcee50f543f79b5b9 - Sigstore transparency entry: 2101932067
- Sigstore integration time:
-
Permalink:
varmabudharaju/shotlist@c17fe5e52962cf1b58aa2aafbb52eaa3f358f5e9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/varmabudharaju
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c17fe5e52962cf1b58aa2aafbb52eaa3f358f5e9 -
Trigger Event:
release
-
Statement type: