Screenery

Docs / Reference

CLI reference

The screenery CLI publishes screenshots from any CI, and from your own machine. You run it with npx, so there is nothing to install.

Running it

npx screenery push ./screenshots --project acme/dashboard

The CLI needs Node 20 or newer. npx screenery help lists the commands, and any command run with no arguments prints its own usage. npx screenery --version prints the version.

This page describes CLI 0.4.0. That is the current release on npm, and the version the screenery/push@v1 GitHub Action runs. A few commands and flags are marked Requires CLI 0.4.0. They are not in 0.3.0, and running them there fails with an unknown command or flag.

Every command exits 0 on success and 1 on failure, unless its section says otherwise. URLs go to stdout. Progress, warnings and errors go to stderr. Inside GitHub Actions, warnings and errors also become annotations on the run. gallery with JSON output and report add no annotations, so their stdout is only JSON. In 0.3.0, gallery in GitHub Actions can print a warning annotation line before the JSON.

push

Uploads a directory of screenshots as one build, and moves the channels that build belongs to. This is the command the GitHub Action runs. See the GitHub Action for the Action's inputs, and How @latest moves for what a push moves.

npx screenery push <dir> --project <org>/<project> [flags]

Each media file becomes an asset. Its name is the file's path inside <dir> without the extension, so checkout/payment-form.png becomes checkout/payment-form. A path that is not a legal asset name is an error, and nothing is pushed. The CLI never rewrites a name to make it legal. The one rename is a theme: hero.dark.png beside hero.png is stored as the dark version of hero (Light, dark and other themes). Files that are not PNG, JPEG, WebP, AVIF, GIF, WebM or MP4 are skipped and listed. See URLs and asset names.

FlagWhat it does
--project <org>/<project>Required. The project to publish to.
--branch <name>The branch channel to move. Inferred in CI.
--pr <n>The pull request, which moves @pr-N. Inferred in CI.
--omit-branchDo not move the branch channel. On a pull request, only @pr-N moves. Cannot be combined with --branch.
--base-branch <name>The branch a pull request would merge into. Inferred in CI. It moves nothing.
--commit <sha>The commit the screenshots came from. Inferred in CI. With the Action's default credential (GitHub OIDC), the build records whether this is the commit GitHub signed for the job: the console shows commit verified, or commit not verified when you name another one. A different commit is allowed, never refused.
--commit-timestamp <time>An RFC 3339 time that orders this build. Needed outside CI.
--run-number <n>The CI run number that orders this build. Inferred in GitHub Actions.
--run-attempt <n>The CI run attempt. Inferred in GitHub Actions.
--dry-runPrint each file's asset name, hash and size, then stop. Sends nothing and needs no credential.
--forceMove channels even when they already point at a newer build. Use it to roll a channel back on purpose.
--concurrency <n>Uploads in flight at once, 1 to 99. Default 4.
--diff-baseline <dir>Compare against a baseline directory first, and fail before anything is sent if it changed. See Visual testing.
--diff-threshold <0..1>Sensitivity of that comparison. Default 0.1.
--commentAlways post the pull-request comment.
--no-commentNever post it.
--gallery-out <file>Write the gallery Markdown for this build.
--readme-out <file>Write the @latest README snippets.
--comment-out <file>Write the pull-request comment Markdown.
--publish-json <file>Write the build's gallery document, for screenery publish later.
--partialRequires CLI 0.4.0. Mark this push as one part of the project, so it removes no other names from the channels it moves.
--diff-fail-on any|minor|moderate|major|neverRequires CLI 0.4.0. Which change grade fails that comparison. Default any, or diff.failOn in screenery.config.json. See Graded changes.
--diff-out <dir>Requires CLI 0.4.0. Keep that comparison's views and viewer in this directory instead of deleting them. It must be outside the pushed directory.
--playwright-snapshotsRequires CLI 0.4.0. Publish and compare only Playwright goldens: files inside a directory whose name ends in -snapshots. Other media is listed as ignored. See Playwright.
--no-change-gradeRequires CLI 0.4.0. Skip grading each screenshot against the version it replaces, for this push. Nothing is downloaded, no grade is stored, and alertAbove and failAbove do not apply.
--fail-on changed,removed,addedRequires CLI 0.4.0. After the build is published, exit 2 when this build's comparison has a screenshot with that status, as report does. The versions are already stored.
--report-out <file>Requires CLI 0.4.0. Write that report as JSON to a file.
--tags <tag>[,<tag>…]Requires CLI 0.4.0. Add these tags to every screenshot in the push, on top of any sidecar. See Descriptions and tags.
--keep-retried-failuresRequires CLI 0.5.0. Push screenshots that a failed, retried test attempt took. By default they are skipped, and the push keeps their previous versions. See Which test took it.
--tracesRequires CLI 0.5.0. Upload the Playwright trace each screenshot's sidecar names. Traces are never public: only members with the publisher role or higher can download them. See Playwright traces.

CI inference. In GitHub Actions the CLI reads the commit, branch, pull request, run number and run attempt from the job's environment. A pull-request run takes its branch from the pull request's head branch and its number from the ref. If the CLI does not recognise the ref, it sets no branch and says so, rather than guess. Every flag above overrides what it inferred.

Ordering. Channels only move forward. Every build needs something that orders it: a CI run number or a commit timestamp. Outside CI, pass one:

npx screenery push ./screenshots --project acme/dashboard \
  --commit-timestamp "$(git show -s --format=%cI HEAD)"

A channel that already points at a newer build does not move. The push warns and still exits 0, because the versions were stored.

Branch names. A channel name cannot contain /, so a branch such as feature/login moves the channel feature-login. The CLI prints that mapping beside the channel.

Channel limit. Branch and @pr-N channels count toward your plan's channels-per-project limit (100 on Free, 500 on Pro). When a push would create a new channel past that limit, the channel is not created: the push warns with channel_limit, still moves the channels that already exist, and exits 0. To make room, delete channels you no longer need, or let a prune rule remove pull-request channels after they close.

Retries. Network failures, timeouts, 408, 429 and 5xx answers are retried with backoff, and a Retry-After header is honoured. Every attempt reuses one idempotency key, so a retry never creates a second build. If some uploads did not arrive, the CLI asks for fresh upload URLs and sends only the missing files, up to three rounds. Other 4xx answers are not retried.

Output. stdout carries the delivery URLs, so npx screenery push … | tee urls.txt keeps your links. stderr shows progress and ready-to-paste @latest README snippets. On a pull request in GitHub Actions the push also posts or updates one comment showing what changed. See Pull-request comments.

Descriptions and tags. Requires CLI 0.4.0. A {name}.meta.json beside a screenshot says what it shows and tags it:

test-results/checkout/payment-form.png
test-results/checkout/payment-form.meta.json

{ "description": "Card form on a phone, filled in.", "tags": ["major", "android"], "label": "Payment form" }

All three keys are optional, and any other key stops the push before anything is uploaded. A tag is lowercase letters, digits, ., _ and -, starts with a letter or digit, and is at most 32 characters; a screenshot takes at most 16. A description is one line of at most 1000 characters. Each push replaces the description and tags of every screenshot it carries, so send the sidecars on every push. A tag named major is your label, not a change grade.

Tags set in Screenery. You can also tag and describe a screenshot after it is pushed: on its page in the console, with the MCP tool screenery_tag_asset, or with PATCH /v1/projects/{org}/{project}/assets/{asset} and a body of {"tags": ["social"], "description": "…"}. These are kept on the screenshot, and a push never changes them. Every read adds them to the sidecar's tags, and a description set in Screenery is shown instead of the sidecar's. The project page filters by either kind, and so do the manifest's ?tags= and screenery_manifest. Tagging needs the publisher role or higher.

Requires CLI 0.5.0. A sidecar may also hold a capture block that says which test took the screenshot. The Playwright helper writes it for you:

{ "capture": {
    "test": { "file": "tests/checkout.spec.ts", "line": 42, "title": "pays by card" },
    "browser": { "name": "chromium", "version": "131.0.6778.33" },
    "viewport_size": { "width": 1280, "height": 720 },
    "color_scheme": "dark",
    "page_url": "http://localhost:3000/checkout",
    "retry": 0, "retries": 2, "status": "passed" } }

Every key is optional, but a test needs file, line and title. The file path is relative to the repository root, with / between folders. A page address may not hold a user name or password. As with the other keys, a mistake stops the push before anything is uploaded and names the file. The screenshot manifest then lists each screenshot's test, and summary.untested counts the screenshots that have none.

Requires CLI 0.5.0. The same sidecar can also take threshold (0 to 1, this screenshot's diff threshold) and ignore (up to 32 rectangles the diff never looks at). See Noise, anti-aliasing and masks.

Read them back with the channel's screenshot manifest: every screenshot with its description, tags and URLs. A screenshot must carry every tag in ?tags=. A public project needs no credential; for a private one, send Authorization: Bearer …. The MCP tool screenery_manifest reads the same list.

curl -s "https://app.screenery.dev/v1/projects/acme/dashboard/channels/latest/manifest?tags=major"

Credential. GitHub OIDC when the job has id-token: write, otherwise SCREENERY_TOKEN. See Credentials.

Prints the gallery document for a pull request, a build or a channel: every screenshot, with its status against a base channel (added, removed, changed or unchanged) and its URLs.

npx screenery gallery acme/dashboard --pr 12
FlagWhat it does
--pr <n>The newest finished build of that pull request.
--build <id>One build, by id.
--channel <name>Any channel, such as latest or main.
--against <channel>The channel to compare with. By default, the build's base branch, else latest.
--format json|markdownJSON (the default) or the gallery Markdown.

Pass exactly one of --pr, --build and --channel. A public project needs no credential. For a private one, the CLI uses the same credential as push.

report

Requires CLI 0.4.0. Prints a short JSON report of the same comparison as gallery, and can fail CI on it.

npx screenery report acme/dashboard --pr 12 --fail-on changed,removed

It takes the same --pr, --build, --channel and --against flags. --fail-on takes any of changed, removed and added, and --report-out <file> also writes the report to a file. It exits 2 when --fail-on matched, with the report still on stdout. Stdout holds only the JSON, in GitHub Actions too, so | jq .gate works when the gate fails. It does not approve or promote anything. See Visual testing.

promote

Points a channel at another channel's current version, at one version, or back to an earlier state.

npx screenery promote acme/dashboard --from main --to latest
FlagWhat moves
--to <channel>Required. The channel to move.
--from <channel>Every asset on that channel. The target ends up equal to it.
--version <v_…>One version. Add --asset <name> if two assets share that version id.
--rollback <n>The state the channel had n promotions ago.

Pass exactly one of --from, --version and --rollback. There is no default, because guessing would move the one channel a mistake costs most on.

Screenery checks every promotion at the edge before it answers. If the edge is not serving the new version, the promotion still happened, but the command exits 1 so a release nobody can see is not a green step. If the check was skipped, it exits 0 and prints why. A promotion that changes nothing prints already points at … and exits 0.

promote uses the same credential as push: GitHub OIDC when the job has id-token: write, otherwise a publish token in SCREENERY_TOKEN. CLI 0.3.0 always needs the publish token.

sign

Makes a signed URL for an unlisted asset. The signature expires.

npx screenery sign acme/dashboard/payment-form@latest.png --exp 30d

--exp sets the lifetime (default 7d). --kid sets the key id, if SCREENERY_SIGNING_KID is not set. The signing secret comes from SCREENERY_SIGNING_KEY. You create a signing key on the project's Access screen in the console, and its secret is shown once.

The command first reads the asset's visibility, with the same credential as push (SCREENERY_TOKEN only, in CLI 0.3.0). It refuses a private asset and prints nothing to stdout: a signature does not open a private asset, so the URL would answer 404. Use a read token instead. Signing a public asset works, with a warning that the signature restricts nothing. See Credentials.

verify-publish

Publishes the screenshots and diffs from a check you ran (Playwright, or anything that writes images) as receipts on a pull request's @pr-N channel.

npx screenery verify-publish ./verify-out --project acme/dashboard --verdict fail
FlagWhat it does
--verdict pass|failRequired. Both publish: a failing check is the receipt a reviewer needs.
--pr <n>The pull request. Inferred in a pull_request workflow.
--url <urls>Comma-separated Screenery delivery URLs to check first.
--title <text>A title for the Markdown.
--comment-out <file>Where to write the Markdown to embed in the pull request.
--forceAs on push.

Receipts never move @latest or a branch channel. They are stored under receipts/pass/… or receipts/fail/…, so they cannot replace a product screenshot. A run on the default branch with no pull request is refused, and so is a project that has no @latest yet. See Pull-request comments.

login, whoami, logout

Signs you in as yourself, for the commands that act as a person rather than as CI.

npx screenery login
npx screenery whoami
npx screenery logout

login prints a URL and a short code. Open https://app.screenery.dev/device, sign in with GitHub and confirm the code. The CLI waits until you do, then saves the session in your config directory (%APPDATA%\screenery\credentials.json on Windows, ~/.config/screenery/credentials.json elsewhere). --label <text> sets the name you see when you approve it.

whoami shows who the saved session is and their organization roles. logout revokes the session first, then deletes the file. If the revoke fails, the file is kept and the command exits 1.

CI does not need a session. Publish tokens and read tokens are never written to disk.

doctor

Checks that this machine can run Screenery, and names the fix for each problem.

npx screenery doctor

It checks the Node version (20 or newer), that npm's launcher works, that app.screenery.dev answers, that the delivery origin is valid, and that the session file's directory is writable. It also says whether you are signed in, never the session itself. It writes nothing, and exits 1 if any check fails. If npx screenery fails before any command starts, the problem is npm itself: check npm --version.

members, invite

Requires CLI 0.4.0. Manage who is in an organization, using your signed-in session.

npx screenery members acme
npx screenery invite acme @octocat --role publisher --projects dashboard
npx screenery invite acme --link --role viewer

members lists the members and pending invites. invite invites a GitHub user by name, or makes an invite link with --link, and prints the link once. Roles are owner, admin, publisher and viewer; owner only by name. --projects limits a publisher or viewer to the projects you list. Today, manage members in the console.

install

Adds the Screenery MCP server to an AI agent's config file.

npx screenery install --tool cursor
npx screenery install --all
npx screenery install --tool claude-code --dry-run

--tool takes claude-code, cursor, vscode, codex, windsurf, gemini, opencode or continue. --all writes every one. --dry-run prints the result and writes nothing. Servers already in the file are kept. See Screenery for AI agents.

mcp

Runs the Screenery MCP server over stdio. An agent host starts it; you do not run it by hand.

claude mcp add screenery -- npx -y screenery mcp

See Screenery for AI agents for the tools it offers and the hosted alternative.

clip, eval-publish, demo

Three commands publish things that are not your CI screenshots, onto channels of their own:

CommandWhat it publishes
clip <url>A capture of a live page, onto @clip and a dated channel.
eval-publish <dir>Screenshots and judge reports from an LLM UI eval, onto @eval.
demo <dir>Rendered demo clips, a GIF and an MP4, with ready-made embeds.

See Clips, evals and demos for their flags.

diff, publish

screenery diff <dir> --baseline <dir> compares two directories of screenshots and exits 1 on a change, without contacting Screenery. See Visual testing.

screenery publish --from publish.json renders the gallery, README or pull-request comment Markdown again from a file push --publish-json wrote, without uploading anything. See Pull-request comments.

Environment variables

VariableMeaning
SCREENERY_TOKENA project token. The publish credential when GitHub OIDC is not available.
SCREENERY_USER_TOKENA user session, used instead of the one login saved.
SCREENERY_CREDENTIALS_FILEWhere login saves the session.
SCREENERY_SIGNING_KIDSigning key id, for sign.
SCREENERY_SIGNING_KEYSigning secret, lowercase hex, for sign.
SCREENERY_COMMENT_AUTHORThe login the pull-request comment is posted as, when your workflow's GITHUB_TOKEN belongs to its own GitHub App.
SCREENERY_API_URLThe API. Default https://app.screenery.dev.
SCREENERY_CDN_URLDelivery. Default https://cdn.screenery.dev.

The CLI never prints a token, and never puts one in a URL. Every URL it logs has its signature and token parameters removed.