Human-in-the-loop review for agent-driven screenshot-test workflows.
When an AI agent implements a UI change, it runs your screenshot tests,
regenerates the snapshots that failed, and leaves you with a working tree full
of diffed PNGs. snapdiff sits between the agent and you: the agent blocks on a
single CLI command, you triage the diffs in a web UI — approve, or reject with a
comment — and the command exits with a JSON verdict per file that the agent can
act on (commit, iterate, move on).
![]() |
![]() |
![]() |
| Triage index — diffs grouped by axis | Per-diff review with verdicts | Read-only baseline gallery |
What makes it small and predictable:
- Git-driven. Diffs are discovered by comparing the working tree against
HEAD(or a configured base ref). No manifest, no framework adapters. - Git is the state. No database, no persistence layer. Approved files are
left dirty for you to commit; rejected files are reverted with
git checkout. - No auth code. The binary trusts the network it's on — front it with Tailscale or Cloudflare (see Remote access).
- Single static binary. Pure Go, cross-compiled, zero runtime dependencies.
- Framework-agnostic. It only cares about PNG files matching globs you configure — works with any test framework that writes PNG snapshots.
Download the archive for your platform from the
latest release,
verify it against checksums.txt, extract snapdiff, and put it on your PATH.
Builds are published for macOS, Linux, and Windows (amd64/arm64; Windows amd64
only).
go install github.com/alephao/snapdiff/cmd/snapdiff@latestmake build # produces ./snapdiffBuilding from source needs the templ code generator — see
CONTRIBUTING.md for the full setup.
Place a snapdiff.toml at your repo root. Minimal example:
[snapshots]
# Globs for snapshot PNG files, relative to repo root.
globs = ["**/__Snapshots__/*.png"]
# Named capture groups become axis names used for grouping and bulk actions
# in the review UI. Axis names are free-form.
axis_regex = '(?P<test>[^/_]+)__(?P<state>[^/_]+)__(?P<device>[^/.]+)\.png'| Field | Required | Default | Meaning |
|---|---|---|---|
snapshots.globs |
yes | — | Globs (relative to repo root) selecting snapshot PNGs. |
snapshots.axis_regex |
yes | — | Regex whose named groups become filter/bulk-action axes. |
snapshots.base_ref |
no | HEAD |
Git ref the working tree is diffed against. |
server.bind |
no | 0.0.0.0:7777 |
Address the web UI binds to. |
server.linger_seconds |
no | 60 |
Seconds serve stays up after Finalize so the tab can render. |
See snapdiff.toml.example for the fully annotated
version.
After your agent has run tests and regenerated snapshots:
snapdiff awaitawait scans for diffs and:
- If there are none, it prints
{"verdicts": []}and exits immediately. - Otherwise it starts the review server, prints the review URL to stderr, and blocks. Open the URL, triage the diffs, and hit Finalize.
On Finalize the command applies your verdicts, prints the verdict JSON to stdout, and exits — so an agent can capture stdout while the URL on stderr goes to the human:
{
"verdicts": [
{ "path": "tests/__Snapshots__/Login__empty__iphone.png", "status": "approved" },
{ "path": "tests/__Snapshots__/Login__error__iphone.png", "status": "rejected", "comment": "logo is cropped" }
]
}snapdiff serveSame UI, but it doesn't emit verdict JSON and lingers for linger_seconds
after Finalize instead of exiting instantly.
snapdiff galleryA read-only browser for every baseline PNG in the working tree — no review session, no verdicts applied.
All three commands accept:
--repo <dir>— repo directory (default: current directory)--config <path>— path tosnapdiff.toml(default:<repo>/snapdiff.toml)
Set SNAPDIFF_NO_BROWSER to stop await/gallery from auto-opening a browser.
- Five diff modes per file: side-by-side, swipe, toggle, pixel-diff overlay, and onion.
- Axis grouping & bulk actions. The named groups from
axis_regex(e.g.device,theme,lang) become chips you can filter by and act on in bulk — every action still records one verdict per file. - Per-file verdicts. Approve, or reject with an optional comment.
- Finalize applies everything: approved files are left dirty in the working
tree for you to
git commit; rejected files are reverted (newly-added files are deleted, modified/deleted files are restored viagit checkout <base_ref> -- <path>).
snapdiff ships with no auth code. Front it with one of:
- Tailscale Serve (recommended) — devices on your tailnet reach the UI by identity, no public exposure. See tailscale.com/kb/1242/tailscale-serve.
- Cloudflare Access — public hostname gated by your IdP at the tunnel edge.
snapdiff runs git diff --name-status <base_ref> (plus git ls-files for
untracked files) to find changed PNGs, and git show <base_ref>:<path> to fetch
each baseline. The review session lives entirely in memory for the lifetime of
the command; git is the only durable state. See docs/spec.md
for the design rationale and docs/adr/ for the decision log.
Contributions are welcome. See CONTRIBUTING.md for dev prerequisites, the repo layout, build/test commands, and how design decisions are recorded as ADRs.
Early / pre-1.0. The core loop — git-driven diff discovery, the five-mode review
UI, bulk actions, and JSON verdicts — is implemented and covered by unit,
integration, and acceptance tests. Deliberately out of scope for now:
persistent run history, notifications, a multi-repo daemon, in-binary auth, and
non-PNG image formats. See docs/spec.md for the full V-Model
breakdown of scope and verification.
MIT © Aleph Retamal


