Snapvisor Docs

Visual testing without CI: let your agent do it

Let Claude Code, Cursor or any MCP client screenshot your running app, push the images to Snapvisor, and report the visual diff back to you, with no CI pipeline and no Git repository link. Free plan.

Your coding agent can run Snapvisor itself. It takes screenshots of the app it just changed, uploads them as a build, waits for the diff, and tells you what moved. You do not need a CI pipeline, and the project does not need a connected GitHub or GitLab repository.

This page is the recipe. The MCP server guide covers connecting and the tool list.

What you need

  • An agent with a shell and a browser tool it can screenshot with (Playwright, Chrome DevTools, a headless browser script). The upload step is a plain HTTP request from the agent's shell, which MCP cannot carry. A chat app without a shell (claude.ai, ChatGPT) can read and review builds but cannot create one.
  • The Snapvisor MCP server connected. A personal access token is the simplest for a coding agent, see Authentication. The token must be scoped to the account that will own the project, and you must be an administrator of that account, because createProject needs both.
  • A running copy of your app that the agent can open.

The loop

The tool names below are the operations the server exposes. In an MCP client you call each one as external_<name> (for example external_getMe) inside the execute_typescript tool, after search_tools shows its signature. See How the tools are exposed.

getMe → createProject (once) → screenshot the app → createBuildForProject →
upload each file with curl → updateBuildForProject (final: true) →
poll getBuild → listBuildDiffs → tell the user

1. Connect and find your account

Call getMe. It returns the accounts the token can reach; the account slug is the owner every other tool takes. On the Free plan getMe reports mcpAccessIncluded: false for the account. That field describes the paid review and team tools. The build loop on this page still works, so the agent should keep going and not stop on it.

2. Create the project, once

Call createProject with the account slug and a project name. No repository link is needed. On later runs listProjects finds it, so the agent should look before it creates.

3. Screenshot the running app

The agent captures the pages or states it changed with its own browser tooling and saves them as PNG files. Use stable, descriptive names such as home.png or settings-billing.png: the name is what a later build is compared by. Capture at a fixed viewport so two runs of the same page match.

4. Create the build

Hash each file (sha256sum home.png) and call createBuildForProject with:

  • commit: the output of git rev-parse HEAD.
  • branch: the output of git branch --show-current.
  • prNumber: optional. Without a connected repository this is only a label shown on the build; no pull request is looked up and no status check is posted.
  • screenshots: one { key, contentType } per file, where key is the file's sha256 and contentType is image/png.

The response lists, for every file the server does not already have, a postUrl and a set of fields. A file the server already has is not listed, so there is nothing to upload for it.

5. Upload with curl

This step is not a tool call. For each listed file, send a multipart/form-data POST to its postUrl, with every entry of fields first and the file last, as file:

curl -sS --fail "$POST_URL" \
  --form-string "$FIELD_1_NAME=$FIELD_1_VALUE" \
  --form-string "$FIELD_2_NAME=$FIELD_2_VALUE" \
  -F "file=@home.png;type=image/png"

Repeat one --form-string name=value for every entry of fields, in the order returned. Use --form-string rather than -F so a value starting with @ or < is sent literally instead of being read as a file. Each URL is single use, expires after 30 minutes, and accepts files up to 50 MB.

6. Finalize

Call updateBuildForProject with the build number and a screenshots array of { key, name }, one per file. final defaults to true, which starts diff processing. finalizeProjectBuilds is only for parallel builds that were sent in several shards.

7. Poll for the result

Call getBuild while status is pending or progress. It settles on no-changes, changes-detected, or an error state (error, aborted, expired).

8. Read the diff and report

On changes-detected, call listBuildDiffs. Tell the user which screenshots changed, whether each change looks intended given the edit just made, and give the build link from the response. On no-changes, say so in one line.

The agent should not approve changes on its own. Approving is a human decision: the user does it in the app, or asks the agent to call createReview on a paid plan.

How the baseline is chosen without a repository

With no connected repository, Snapvisor has no commit history to walk, so a branch name alone does not select a baseline. The rules, in order:

  1. If you pass referenceCommit, only a build on that commit can be the baseline, and it must be eligible: finished, passed, not a partial (subset) build, not rejected, and approved. A reference or orphan build counts as approved; any other build needs an approved review. Approval is not checked when the reference branch is auto-approved, which by default is the project's default branch. If no eligible build exists for that commit, rule 3 is skipped and Snapvisor does not substitute another build, so the new build is compared against nothing and every screenshot shows as added, unless a build in parentCommits matches (rule 2).
  2. Otherwise, parentCommits is searched. List the build's own commit first and then its ancestors, closest first. Snapvisor drops the first entry, so the build's own commit is never matched. The closest remaining commit that has a finished, passed, non-rejected build wins, approved or not.
  3. Otherwise, when no referenceCommit was sent, the build is compared against the latest approved build on referenceBranch (the project's default base branch when referenceBranch is not set). If none is approved, it falls back to the latest completed build on that branch. The only build excluded is the new build itself, so a build on the default branch is compared against the previous build on that same branch. A build on any other branch is compared against the default branch, not against its own earlier builds. Set referenceBranch to that branch's own name to chain them.
  4. Otherwise, a previous approved build on the same commit and branch is used, so running twice without committing does not leave both builds without a baseline.

The first build of a project has nothing to compare to and needs no approval. It is recorded as a reference build on the default branch or an orphan build elsewhere, and both count as approved baselines, so the next build can compare against it. Approving changes on later builds needs a paid plan, and the loop does not depend on it.

To pin a specific baseline, call findBaselineForProject first. Its required input is commits, a list of ancestor commit SHAs, closest first. It also takes an optional name (default default) and mode (ci or monitoring, default ci). The first commit with an eligible build wins. The response is { baseline }, where baseline is the build, or null when none of the commits has an eligible build. Pass the SHA at baseline.head.sha as referenceCommit and baseline.head.branch as referenceBranch.

What the Free plan includes

The build loop above runs on the Free plan: getMe, createProject, listProjects, getProject, createBuildForProject, updateBuildForProject, finalizeProjectBuilds, findBaselineForProject, getBuild, listBuilds and listBuildDiffs. Screenshots count against the plan's monthly allowance like any other build.

Everything else (review, approve, comments, team, domains, automation, settings, billing) needs a paid plan; on Free those tools reply with an upgrade message. See Pricing.

Nudge your agent to run it

Nothing here is enforced. An agent only runs a visual check if something reminds it. Both snippets below are a reminder, not a gate, and the agent can ignore either.

A repo rule

Paste this into CLAUDE.md or AGENTS.md, or into .cursor/rules/snapvisor.mdc (with alwaysApply: true in its front matter):

## Visual check (Snapvisor)

After you change UI code, run a Snapvisor visual check before you say the work is done:
screenshot the affected pages of the running app, create a build with the Snapvisor MCP
tools (createBuildForProject), upload the files with curl, finalize with
updateBuildForProject, then read getBuild and listBuildDiffs and tell me what changed.
Skip it for changes that cannot affect rendering. Do not approve changes yourself.
Skill: snapvisor-visual-check. Recipe: https://snapvisor.io/docs/agents/visual-testing-without-ci

A Claude Code hook

This adds a reminder to the agent's context after it edits a UI file. It is a reminder, not a gate: it does not block the agent and it does not run Snapvisor. The hook exits 0 and only returns additionalContext, which Claude Code passes to the agent next to the tool result. Add it to .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "node -e \"let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const p=(JSON.parse(s).tool_input||{}).file_path||'';if(/[.](tsx|jsx|vue|svelte|css|scss|html)$/.test(p))console.log(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'A UI file changed. Before you finish, consider a Snapvisor visual check (skill: snapvisor-visual-check).'}}))})\""
          }
        ]
      }
    ]
  }
}

Never make this hook exit with code 2 or return decision: "block": that turns the reminder into a gate. A Stop hook is a poor fit here: stderr from a hook that exits 0 is not shown, and additionalContext on Stop makes the agent continue its turn.

Install the skill

The Snapvisor repository ships a Claude Code skill that walks the agent through this loop. Copy .claude/skills/snapvisor-visual-check/SKILL.md into your project's .claude/skills/snapvisor-visual-check/, or into ~/.claude/skills/snapvisor-visual-check/ to use it in every project.

On this page