stableFor developers

pixmoat AI Agent Integration Guide

This guide is for AI coding agents (Claude Code, Codex, Cursor, custom harnesses) that make UI changes and need to verify them against visual regression baselines.

Cost of a Review Loop

pixmoat is designed to minimise token cost. The digest format and cropped triptych inspection mean a full review loop costs 1–2k tokens, not 15–20k.

OperationBefore (JSON + full page)After (digest + cropped triptych)
Read build summary (40 snapshots, 12 diffs)~5,000–10,000 tok~320–700 tok
One visual inspection (3 full-page images)~3,700 tok~300 tok (single cropped triptych)
Poll loop (5 polls while diffing)5× full payload4 of 5 return UNCHANGED (~15 tok each)
Clean build (PASS, nothing to review)~250+ tok0 tok (--quiet prints nothing)

Prerequisites

Set three environment variables before running any command:

export PIXMOAT_URL=https://pixmoat.example.com   # your pixmoat server
export PIXMOAT_PROJECT=my-app                       # project slug (required by most commands)
# PIXMOAT_TOKEN is injected by your CI/MCP host secret store.

Inject these values through your CI secret store, MCP host environment, or shell environment. Do not pass a token on the command line: command history and process listings can expose it. Never commit a token to .pixmoat.yaml or source control, and never include one in logs or build artifacts.

Personal Access Tokens

Two token types are supported:

Token typePrefixScopeBest for
Project access tokenpxg_Single projectCI pipelines — least privilege, no cross-project access
Personal access token (PAT)pxgp_All projects the user can accessWorkstation agents — multi-project reach, discovery endpoints

Both token types work on all agent endpoints (agent-summary, decisions, history, feedback, builds, images, view). PATs additionally unlock the discovery endpoints (/api/agent/orgs, /api/agent/projects) which project tokens cannot access.

Decisions made via PAT are attributed to the PAT owner in the audit trail (decided_by). Create PATs in the UI under Settings → Access Tokens.

Recommendation: Use project tokens for CI (one token per project). Use PATs for interactive workstation agents that operate across multiple projects.

Capability discovery

Before scheduling autonomous review work, check the installed client rather than attempting a different finish route:

pixmoat capabilities --json

Its versioned local contract includes cli_version, schema_version, and mcp.tools: ["finish_review"]. An MCP harness can instead inspect tools/list; the MCP initialization metadata advertises the same capability and server version. If finish_review is missing, update the local CLI/MCP before dispatching the work. If it is present but returns human_action_required or agent_finish_not_allowed, the client is current and project policy is blocking completion. This local check does not establish that the remote Pixmoat server has the endpoint; an unsupported server response is a compatibility error.


1. The Loop

Every UI change follows this cycle:

1. Edit code (make UI changes)
2. Run Playwright capture (npx playwright test)
3. Check results:  pixmoat check --build <id> --wait --quiet
4. Decide:
   - Exit 0 → all clean, continue to next task
   - Exit 1 → diffs detected, read the digest, inspect and act
   - Exit 2 → new snapshots, approve to establish baselines
   - Exit 3 → build failed, rejected, or timed out — investigate

Do not skip step 3. Every UI change must be verified.

Verdicts

The VERDICT line on the digest (and the verdict field in JSON) tells you what to do in one word:

VerdictExit codeMeaningAction
PASS0No pending diffs, no pending new, no rejectionsContinue. Build is clean.
REVIEW1Pending diffs (with or without new snapshots)Inspect diffs, fix or approve.
REVIEW_NEW2Pending new snapshots only, zero diffsApprove to establish baselines.
FAIL3Any run rejected, build status failed, or size-mismatch runRead last_rejection comment. Fix the issue.
WAITDiffing still in progress (only with wait=false)Re-poll or use --wait.

2. Reading the Output

Digest format (default)

pixmoat check and the MCP check_build tool return a compact text digest by default. The digest is a machine-parseable, line-oriented format with a fixed column order.

Example — a build with 12 diffs, 2 new, 1 flaky:

pixmoat 7f3a2b1c  proj=my-app  branch=feat/checkout  commit=a1b2c3d
VERDICT REVIEW  12 diff · 2 new · 44 unchanged · 1 flaky · 0 removed  digest=9c1fa3b2
web https://pixmoat.example.com/p/my-app/b/7f3a2b1c

DIFF
h7k2  checkout/cart-desktop   1280x720 cx  0.42%  moderate  1rgn 120,340 220x48
h7k3  checkout/cart-mobile     390x844 cx  0.39%  moderate  1rgn  88,301 180x44
h9m1  header/nav-desktop      1280x720 cx  0.02%  trivial   scattered 7rgn
p4tt  legal/footer-desktop    1280x720 cx  1.80%  major     1rgn   0,880 1280x200
      ! rejected 2026-07-22 by ana@acme.com: "footer spacing regressed, revert"

NEW
n3p4  checkout/coupon-badge   1280x720 cx

FLAKY (skipped — not your regression)
f2q9  home/hero-desktop       1280x720 cx  quarantined

NEXT  inspect: pixmoat view <build> h7k2 | approve: pixmoat approve --build <build> --run h7k2,h7k3

A clean build (PASS) is exactly 3 lines:

pixmoat 7f3a2b1c  proj=my-app  branch=main  commit=a1b2c3d
VERDICT PASS  0 diff · 0 new · 24 unchanged · 0 flaky · 0 removed  digest=abc12345
web https://pixmoat.example.com/p/my-app/b/7f3a2b1c

Digest grammar (normative)

The digest is a wire format. The column order and section names are stable and parseable.

Line 1 — header:

pixmoat <build_short_id>  proj=<slug>  branch=<branch>  commit=<sha7>
  • build_short_id: first 8 characters of the build UUID.
  • sha7: first 7 characters of the commit SHA.

Line 2 — verdict:

VERDICT <verdict>  <N> diff · <N> new · <N> unchanged · <N> flaky · <N> removed  digest=<hex8>
  • verdict: one of PASS, REVIEW, REVIEW_NEW, FAIL, WAIT.
  • digest: 8-character hex state hash for conditional polling.

Line 3 — web URL:

web <url>

Omitted when PIXMOAT_PUBLIC_URL is not configured on the server.

Sections: DIFF, NEW, FLAKY — each section is preceded by a blank line and a header. Sections with zero runs are omitted entirely.

Run line (DIFF and NEW sections):

<handle>  <name>  <viewport> <browser2>  <ratio%>  <severity>  <regions>
  • handle: 4-char run handle (see Handles below). Falls back to first 8 chars of UUID if no handle.
  • name: snapshot key name (may contain spaces).
  • viewport: e.g. 1280x720.
  • browser2: two-letter browser code — cx (chromium), ff (firefox), wk (webkit).
  • ratio%: mismatch ratio as percentage (e.g. 0.42%). Omitted for NEW runs.
  • severity: trivial, minor, moderate, or major. Omitted for NEW runs.
  • regions: one of:
    • Nrgn x,y WxH [x,y WxH] — clustered spread, up to 2 regions with coordinates.
    • scattered Nrgn — scattered spread, count only.
    • full-page — full page spread.
    • Omitted for NEW runs or runs without diff_analysis.

Rejection continuation line:

      ! rejected <date> by <email>: "<comment>"

Appears only when last_rejection is present for the preceding run. Comments with newlines are escaped to a single line.

FLAKY run line:

<handle>  <name>  <viewport> <browser2>  quarantined

NEXT line:

NEXT  inspect: pixmoat view <build> <handle> | approve: pixmoat approve --build <build> --run <handles>

Appears only when there are actionable (DIFF or NEW) runs.

Handles

Runs are identified by short handles — 4-character alphanumeric codes (e.g. h7k2) unique within a build. Handles are accepted anywhere a run ID is accepted: in approve, reject, view, and get_screenshot.

  • Handles use the Crockford base-32 alphabet (excludes I, L, O, U to avoid visual ambiguity).
  • They are deterministic: re-running the same suite yields the same handles.
  • They are build-scoped: h7k2 in build A refers to a different run than h7k2 in build B. Always pass handles alongside a build ID.
  • UUIDs continue to work everywhere.

JSON format

Pass --json (CLI) or format: "json" (MCP) for the full JSON response. JSON is unchanged from earlier versions — new fields are additive:

New fieldLocationDescription
verdictTop-level"PASS", "REVIEW", "REVIEW_NEW", "FAIL", or "WAIT"
digestTop-level8-char hex state hash for conditional polling
handlePer run4-char handle for compact references
view_urlPer runURL to the view endpoint for cropped/composed images

Use ?fields=run_id,name,mismatch_ratio,analysis to trim JSON per-run entries. run_id and name are always included regardless of fields=.

Conditional polling (?since=)

The digest includes a digest=<hex> state hash. On repeat polls, pass ?since=<digest> and the server returns a minimal UNCHANGED response when nothing has changed:

pixmoat 7f3a2b1c UNCHANGED digest=9c1fa3b2

The CLI and MCP tools handle this automatically within a single invocation.


3. Deciding

After the check returns a non-PASS verdict, apply these rules in order:

If the FLAKY section lists the run

Do not treat as a real regression. Skip it. Flaky/quarantined screenshots change non-deterministically (animations, timing, fonts) and are not caused by your code change.

If a ! rejection continuation line is present

A human previously rejected this screenshot. Read the comment — it tells you what was wrong. Fix the issue described in the comment before re-submitting.

If the diff was intended

Approve it:

pixmoat approve --build <id> --run h7k2 --comment "Intended: <reason>"

Or approve multiple handles at once:

pixmoat approve --build <id> --run h7k2,h7k3,n3p4 --comment "<reason>"

If the diff was NOT intended

This is a regression. Do not approve. Fix your code to eliminate the unintended visual change, re-run Playwright capture, and check again.

If the verdict is REVIEW_NEW

New screenshots with no baseline. Approve to establish baselines:

pixmoat approve --build <id> --run n3p4 --comment "New baseline"

Change groups

When a build has multiple diffs sharing the same root cause (e.g. 14 screenshots all shifted down 8px because a header grew), they are collapsed into change groups. Groups appear in the digest and agent-summary response.

Two rules govern group behaviour:

  1. Approving a group applies per-run guardrails, never widens permissions. When you approve a group, each run is evaluated individually against intent declarations, agent_auto_approve settings, and nature-mismatch checks. A group approval is a convenience shorthand — it never bypasses guardrails that would block individual run approval.

  2. Singleton groups (singleton: true) deserve individual attention. A group with only one run has no “shared root cause” signal. Treat it like an individual diff — inspect it before deciding.

Approve a group:

pixmoat approve --build <id> --group g1 --comment "Intended: header height change"

Or via MCP:

approve_runs(build_id: "...", group_ids: ["g1"], comment: "Intended: header height change")

Inspecting diffs visually

Use get_screenshot (MCP) or pixmoat view (CLI) to see the actual visual change before deciding. The default returns a cropped triptych: baseline | current | diff side-by-side.

pixmoat view <build> h7k2

The triptych panes are separated by coloured bars:

  • Blue bar (#3B82F6) — left of the baseline pane
  • Green bar (#22C55E) — left of the current pane
  • Magenta bar (#D946EF) — left of the diff pane

When the digest shows region_count > 1, pass region=all to see all changed areas in one image instead of just the largest:

# MCP: get_screenshot with region: "all"
# CLI: pixmoat view <build> h7k2 --region all

For the full page (pre-cropping behaviour): use region=full.

Severity guide

SeverityMeaningAction
trivialScattered sub-pixel changes, ratio < 0.1%. Anti-aliasing or font rendering noise.Usually safe to approve.
minorClustered small change, ratio < 1%. A small layout shift or color tweak.Inspect if the change was not intended.
moderateNoticeable change, ratio < 5%. Real UI difference.Always inspect before deciding.
majorLarge change, ratio >= 5% or full-page diff.Always inspect. Likely a significant regression or intentional redesign.

Intent Declaration

When you know which screenshots will change before you check, declare intent on POST /builds/{id}/finish:

{
  "intent": {
    "expected_changes": ["checkout/cart-desktop", "checkout/cart-mobile"],
    "reason": "Repositioned checkout CTA from sidebar to main content"
  }
}

The response classifies runs into three buckets:

BucketMeaning
expected_changesNamed in intent AND actually changed. Auto-approvable.
unexpected_changesNOT named in intent BUT changed. Inspect these.
expected_unchangedNamed in intent BUT did NOT change. Your intent was wrong.

Use --expected on the CLI to approve only the expected changes:

pixmoat approve --build <id> \
  --expected "checkout/cart-desktop,checkout/cart-mobile" \
  --comment "Repositioned checkout CTA"

agent_auto_approve Setting

Each project has an agent_auto_approve setting that controls what agents can approve:

ValueBehavior
"all"Agent can approve any run. Fully autonomous mode.
"intent_only" (default)Agent can only approve runs declared in intent. Other runs require human approval.
"none"Agent approvals are recorded as advisory only. A human must still complete the review.

This setting is configured in the project settings UI by project admins.

Required Approvals & Agent Exclusion

When a project has a review policy configured (see Merge Request Integration — Required Approvals), the finish-review gate enforces additional conditions beyond “all runs decided.”

Agent approvals do not count toward the human quorum. Specifically:

  • Agent approvals (decision_source = 'agent' or 'auto') are excluded from the min_approvals count.
  • Agent approvals do not satisfy the required_role requirement.
  • Agent approvals do not satisfy the require_distinct_from_author requirement.

This means an agent can still pre-triage and approve runs to narrow the queue, but a human must provide the required sign-off before finish-review succeeds.

agent_may_finish flag

The review policy includes an agent_may_finish flag (default: true). When set to false, agents are forbidden from calling finish-review on sessions where the policy is active — even if all policy conditions are already satisfied by human approvers.

# .pixmoat.yaml
review_policy:
  min_approvals: 2
  agent_may_finish: false    # agents can approve runs but cannot finish the session

When an agent attempts to finish a session with agent_may_finish: false, it receives the successful domain result agent_finish_not_allowed (CLI exit 1 / MCP status: "agent_finish_not_allowed"). No review state changes.

Policy and the REVIEW verdict

When a policy is unmet, pixmoat check returns REVIEW (exit 1) even if all runs are decided. A finish attempt returns the structured domain result human_action_required, including only safe requirement details:

{
  "status": "human_action_required",
  "requirements": [
    { "kind": "min_approvals", "required": 2, "current": 1 },
    { "kind": "required_role", "role": "project_admin" }
  ]
}

The agent should report this to the user and wait for the human quorum to be met before retrying finish.

CODEOWNERS routing for agents

When CODEOWNERS routing is active, runs include requested reviewer information. If the project enables require_codeowner_approval, each run with resolved owners must have at least one approval from an owner. Since agent approvals are excluded from this check, a human code owner must approve their owned runs before finish succeeds.

Explicit finish-review

After inspecting changed screenshots and deciding every actionable visual, accessibility, console, performance, and removal result, call finish exactly once. Each approval comment should state why the change is intended; each rejection comment should describe the regression and the affected UI.

pixmoat finish --build <id>
pixmoat finish --build <id> --json

--json is a global flag, so pixmoat --json finish --build <id> is equivalent. It writes the authoritative server receipt or blocked result unchanged to stdout; diagnostics go to stderr. The finish command derives the project from the authorized build, so it needs PIXMOAT_URL and PIXMOAT_TOKEN but does not require PIXMOAT_PROJECT.

MCP callers use finish_review(build_id: "..."). It accepts no force flag, outcome, or decision input. Rejected is a terminal workflow signal: preserve the receipt and comments for the developer workflow instead of treating it as a transport failure.

ResultCLI exitMCP statusRequired action
Terminal approved0approvedContinue with the workflow.
Terminal rejected3rejectedReturn the review comments to the developer workflow.
Processing2waitingWait, then inspect the current state before retrying.
Undecided runs1pending_decisionsInspect and decide the listed actionable runs.
Human policy remains1human_action_requiredWait for quorum, role, or CODEOWNERS approval.
Agent finishing disabled1agent_finish_not_allowedLeave completion to a human.
Auth/config/server failure4MCP errorFix configuration or connectivity; do not invent a result.

The project selects the operating mode; the agent does not:

ModeConfigurationCompletion behavior
Manualagent_auto_approve: noneAgent feedback is advisory; a human decides and finishes.
Hybridintent_only, agent_may_finish: false, or unmet human quorum/CODEOWNERSAgent triages and comments; Pixmoat waits for human action.
Fully automaticagent_auto_approve: all, agent_may_finish: true, and all human-policy requirements satisfiedAgent decides all actionable runs, then explicitly finishes once.

4. Accessibility Results

When accessibility checking is enabled for a project, builds include WCAG violation data alongside visual diffs. The accessibility dimension is independent from visual and performance — a build can have visual diffs without accessibility violations, or vice versa.

Reading accessibility data

The agent-summary and check responses include accessibility counts when the feature is enabled:

{
  "accessibility_counts": {
    "new": 0,
    "clean": 34,
    "warnings": 2,
    "violations": 12,
    "total_violations": 31,
    "new_violations": 3,
    "resolved_violations": 1,
    "by_impact": { "critical": 0, "serious": 14, "moderate": 12, "minor": 5 },
    "by_rule": { "color-contrast": 18, "label": 6, "link-in-text-block": 4 }
  }
}
FieldMeaning
new_violationsViolations not present in the baseline — these are regressions introduced by this build
resolved_violationsViolations that were in the baseline but are now absent — improvements
total_violationsTotal violation count across all snapshots
by_impactBreakdown by severity: critical, serious, moderate, minor
by_ruleBreakdown by axe-core rule ID (e.g. color-contrast, label)

When accessibility_counts is null, the feature was not enabled for that build. Do not interpret null as “clean” — it means “not evaluated.”

Accessibility results vs visual diffs

The three dimensions are independent:

VisualPerformanceAccessibilityAction
CleanCleanCleanAll clear
DiffsCleanCleanReview visual diffs only
CleanCleanViolationsFix accessibility violations or approve as baseline
DiffsDegradedViolationsReview all three independently

A build’s overall outcome is the worst of all three dimensions.

Approving accessibility violations

Accessibility violations use the same approve/reject semantics as visual diffs:

  • Approving a violation run means the current violation set is acceptable. The server writes it as the new accessibility baseline for that snapshot on that branch.
  • Rejecting means the violations must be fixed before merge.
# Approve accessibility violations (same command as visual approvals)
pixmoat approve --build <id> --run <handle> --comment "Known contrast issue on decorative text"

Gating modes

ModeEffect
advisory (default)Violations are shown but do not block “Finish review”
blocking“Finish review” is blocked until all violation runs have decisions

In advisory mode, the build can be finished with unresolved accessibility violations. In blocking mode, the build behaves like visual diffs with blocking mode — all violations must be approved or fixed.

Common accessibility violation patterns

RuleWhat it meansAgent action
color-contrastForeground/background contrast ratio is below the WCAG thresholdCheck if your CSS change affected text or background colors. Fix the contrast ratio.
labelA form input is missing an accessible labelAdd an aria-label, aria-labelledby, or associated <label> element
link-in-text-blockA link is only distinguishable from surrounding text by colorAdd an underline or other non-color visual indicator
aria-required-attrAn element with an ARIA role is missing required attributesAdd the missing ARIA attributes

Accessibility score

The project-level accessibility score is: 100 - (critical*10 + serious*5 + moderate*2 + minor*1), clamped to 0-100. It is a trend indicator, not a compliance certification.

5. Console Results

When console capture is enabled for a project, builds include browser console errors, uncaught page errors, and failed network request data alongside visual diffs. The console dimension is independent from visual, performance, and accessibility — a build can have visual diffs without console violations, or vice versa.

Reading console data

The agent-summary response includes a console object per run when console data exists:

{
  "console": {
    "result": "violations",
    "new_errors": 2,
    "failed_requests": 1,
    "top": [
      { "level": "pageerror", "message": "TypeError: Cannot read properties of undefined (reading 'map')", "location": "checkout.js:142:18", "new": true },
      { "level": "requestfailed", "message": "GET https://api.example.com/cart - net::ERR_FAILED", "location": null, "new": true },
      { "level": "error", "message": "Warning: Each child in a list should have a unique \"key\" prop.", "location": "react-dom.js:1:2", "new": false }
    ]
  }
}
FieldDescription
resultnew (no baseline), clean (no new findings), warnings (new non-fail_on findings), violations (new fail_on findings)
new_errorsCount of new pageerror and error findings not in the baseline
failed_requestsCount of new requestfailed findings not in the baseline
topUp to 5 entries, most severe first: pageerror > requestfailed > error > warning

When console is null or absent, the feature was not enabled for that snapshot. Do not interpret absence as “clean” — it means “not evaluated.”

Console in digest format

The digest appends a [console: ...] marker on run lines when new console findings exist:

DIFF
h7k2  checkout/cart-desktop   1280x720 cx  0.42%  moderate  1rgn 120,340 220x48  [console: 2 new errors, 1 failed request]
h7k3  checkout/cart-mobile     390x844 cx  0.39%  moderate  1rgn  88,301 180x44

The marker only appears on runs with new findings (findings whose fingerprints are absent from the baseline).

Console results vs other dimensions

The four dimensions are independent:

VisualPerformanceAccessibilityConsoleAction
CleanCleanCleanCleanAll clear
DiffsCleanCleanViolationsReview visual diffs and fix console errors
CleanCleanCleanViolationsFix console errors or approve as baseline
DiffsDegradedViolationsViolationsReview all four independently

A build’s overall outcome is the worst of all four dimensions.

Approving console findings

Console findings use the same approve/reject semantics as visual diffs:

  • Approving a console run means the current finding set is acceptable. The server writes it as the new console baseline for that snapshot on that branch.
  • Rejecting means the console errors must be fixed before merge.
# Approve console findings (same command as visual approvals)
pixmoat approve --build <id> --run <handle> --comment "Known console error in third-party script"

Gating modes

ModeEffect
advisory (default)Console findings are shown but do not block “Finish review”
blocking“Finish review” is blocked until all console violation runs have decisions

6. Fast Inner Loop (Local Mode)

The local loop lets you iterate on CSS and layout changes without a server round-trip or CI minutes. A check costs no network and completes in sub-second time, so you can iterate on a visual change ten times before opening an MR.

Decision rule

  1. Iterate locally against previous: capture, check --against previous, fix, repeat.
  2. Run once against baseline: when satisfied, run pixmoat check --local (against cached baselines) to confirm parity with the server’s view.
  3. Push and let CI be the record: the CI build is the authoritative result. Local results carry no approval authority.

Sequence

# 1. Sync baselines (once per branch)
pixmoat baselines sync

# 2. Capture locally (no network, no build)
PIXMOAT_MODE=local npx playwright test

# 3. Check against previous capture (iteration delta)
pixmoat check --local --against previous --quiet

# 4. When satisfied, check against baselines
pixmoat check --local --quiet

# 5. Push for the authoritative CI run
git push

What local mode cannot do

  • Cannot approve or reject. Local mode is read-only — it cannot write baselines, create builds, or make decisions.
  • Cannot substitute for CI. A clean local run is a reason to push, never a reason to skip pushing.
  • Never report a local result as a passing build. The local verdict is advisory — only CI produces an authoritative PASS.

See Using pixmoat for the product review workflow after an agent has inspected a build.


7. Reference

Command reference

pixmoat check

Check build status and list pending visual changes.

pixmoat check --build <id> --wait --quiet     # default: digest, quiet on PASS
pixmoat check --build <id> --wait             # digest output always
pixmoat check --build <id> --wait --json      # JSON output
pixmoat check --build <id> --min-mismatch 0.01  # only show >= 1% mismatch

Flags:

  • --quiet — prints nothing on PASS (exit 0), prints digest on any other verdict.
  • --json — full JSON response instead of digest.
  • --wait — wait for diffing to complete (default timeout 120s).
  • --timeout N — custom timeout in seconds (max 300).

pixmoat approve

Approve one or more runs. Accepts handles or UUIDs. All approvals are tagged with source: "agent".

pixmoat approve --build <id> --run h7k2 --comment "Intended change"
pixmoat approve --build <id> --run h7k2,h7k3,n3p4 --comment "CTA redesign"
pixmoat approve --build <id> --name "checkout/*" --comment "CTA redesign"
pixmoat approve --build <id> --expected "name1,name2" --comment "Reason"
pixmoat approve --build <id> --scope diff --comment "Verified visually"
pixmoat approve --build <id> --group g1 --comment "Intended: header height change"

pixmoat reject

Reject a run. Accepts handles or UUIDs. Comment is required.

pixmoat reject --build <id> --run h7k2 --comment "Header alignment broken on mobile"

pixmoat finish

Request the one explicit, policy-enforced completion transaction after every actionable run has a decision. It does not decide runs and has no force or outcome option.

pixmoat finish --build <id>
pixmoat finish --build <id> --json

pixmoat view

Show the view URL for a run’s cropped/composed image.

pixmoat view <build> h7k2                     # default: triptych, largest region
pixmoat view <build> h7k2 --region all        # all changed regions
pixmoat view <build> h7k2 --region full       # full page
pixmoat view <build> h7k2 --kind baseline     # baseline pane only

pixmoat history

Show recent change history for a snapshot.

pixmoat history --name "checkout/cart-desktop" --branch main --limit 5
pixmoat history --name "checkout/cart-desktop" --branch main --json

pixmoat feedback

Show recent rejection comments for a snapshot.

pixmoat feedback --name "header/nav-mobile" --branch main
pixmoat feedback --name "header/nav-mobile" --branch main --json

pixmoat builds

List recent builds for the project.

pixmoat builds                           # all branches, last 10
pixmoat builds --branch feat/redesign    # filter by branch
pixmoat builds --limit 20 --json         # more results, JSON output

View endpoint

GET /api/builds/{id}/runs/{run_ref}/view
    ?kind=triptych|baseline|current|diff      (default: triptych)
    &region=largest|<index>|all|full          (default: largest)
    &padding=24                                (default: 24 image px)
    &max_width=900                             (default: 900)
  • run_ref accepts a handle (e.g. h7k2) or a UUID.
  • kind=triptych composes baseline, current, and diff panes horizontally with 4px separator bars.
  • region=largest crops to the largest changed region expanded by padding, clamped to image bounds.
  • region=all crops to the union bounding box of all regions.
  • region=full returns the full page (no cropping).
  • Results are content-addressed and cached in the blob store.
  • When no diff_analysis exists (pre-ADR-026 runs), region=largest degrades to region=full.
  • When no baseline exists (result=new), the triptych shows only the current pane.
  • Auth: same dual-auth as other agent endpoints (project token, PAT, session).

analysis block reference

Each run in pending_review (JSON format) includes an analysis block with backend-computed diff analysis:

{
  "analysis": {
    "regions": [
      {
        "x": 120, "y": 340, "w": 220, "h": 48, "changed_pixels": 8214,
        "nature": { "class": "shift", "confidence": "high", "shift": { "dx": 0, "dy": 8 } },
        "elements": ["button[data-testid=checkout-submit]", "div.cart-total"]
      }
    ],
    "region_count": 2,
    "largest_region_area_ratio": 0.0115,
    "spread": "clustered",
    "mean_color_delta": 41.2,
    "severity": "moderate",
    "nature": { "class": "shift", "confidence": "high", "shift": { "dx": 0, "dy": 8 } },
    "summary": "Content shifted down 8px in 2 regions (220x48 at 120,340).",
    "aa_pixel_ratio": 0.02
  }
}
FieldDescription
regionsBounding boxes of changed areas (up to 20).
regions[].naturePer-region nature classification: class, confidence, optional shift/color params. Use it as triage context before following the visual review guide.
regions[].elementsAttributed DOM elements overlapping this region (up to 3, most specific first). Empty array when no element map was captured.
region_countNumber of distinct changed regions.
largest_region_area_ratioFraction of image area covered by the largest region.
spreadclustered (<=5 regions, <10% area), scattered (many small changes), or full_page (>60% area).
mean_color_deltaMean YIQ color distance of changed pixels (0–255 scale). Low = subtle; high = content change.
severityRule-based severity: trivial, minor, moderate, major.
natureRun-level nature classification (dominant class by 80% rule).
summaryRule-templated sentence describing the change (e.g. “Content shifted down 8px in 2 regions”).
aa_pixel_ratioFraction of anti-aliased pixels over total image pixels. High values (> 0.5) with scattered spread indicate noise.

Old runs without analysis return "analysis": null — fall back to mismatch_ratio interpretation and image inspection. Pre-classification runs may have nature: null — these are handled gracefully everywhere.

MCP setup

pixmoat ships an MCP server as a CLI subcommand: pixmoat mcp. It uses stdio transport and exposes 9 tools that wrap the agent API. Agent harnesses that support MCP (Claude Code, Cursor, Codex) will discover the tools automatically.

Add this to .mcp.json in the consumer repository:

{
  "mcpServers": {
    "pixmoat": {
      "command": "pixmoat",
      "args": ["mcp"],
      "env": {
        "PIXMOAT_URL": "https://pixmoat.example.com",
        "PIXMOAT_TOKEN": "${PIXMOAT_TOKEN}",
        "PIXMOAT_PROJECT": "my-app"
      }
    }
  }
}

Set PIXMOAT_TOKEN through the MCP host’s secret/environment injection, not in .mcp.json. Do not commit tokens to .pixmoat.yaml, source control, logs, or build artifacts.

MCP tools

ToolPurpose
check_buildCheck build for regressions — returns compact digest with verdict, handles, and region summaries
list_buildsList recent builds and their statuses
get_screenshotCropped triptych image (baseline|current|diff) around the largest changed region
approve_runsApprove one or more runs by handle, UUID, or group ID
reject_runReject a run with a required comment
finish_reviewExplicitly finish staged decisions after all actionable runs are decided
get_historyView recent changes for a snapshot name
get_feedbackRead last human rejection comments for a snapshot
list_projectsDiscover orgs and projects visible to your token (PAT only)

get_screenshot returns a single MCP image content block — the agent sees the composed screenshot directly without needing to fetch URLs with Bearer headers.

Skill installation

Copy the pixmoat skill into your consumer repository so agents know to verify UI changes:

mkdir -p .claude/skills
cp -r skills/pixmoat .claude/skills/pixmoat

The skill provides the trigger (“after any UI change, capture and check”) and the decision tree. See skills/pixmoat/SKILL.md for the full content.

Orchestrator integration (hive)

This section documents the poll-loop contract for CI/AI orchestrators that treat pixmoat as a “human review on an external platform” gate. hive’s provider: pixmoat monitor phase (hive ADR-206) is the reference consumer, but the pattern applies to any orchestrator.

Resolve the build

After CI finishes and screenshots are uploaded, the orchestrator knows the branch and HEAD commit SHA. Resolve the build in one request:

GET /api/projects/{slug}/agent-builds?branch=feat/x&commit=9c2f41a...&limit=1
Authorization: Bearer <project access token>

The ?commit= parameter filters by exact full SHA. Combined with limit=1 and the default created_at DESC ordering, this returns the single newest build for that commit.

Poll agent-summary

Once you have the build_id, poll the agent-summary endpoint. Use ?since=<digest> after the first poll to avoid re-sending the full payload when nothing has changed:

GET /api/builds/{build_id}/agent-summary?since=9c1fa3b2
Authorization: Bearer <project access token>

Status mapping

Map the summary response to orchestrator outcomes:

ConditionOrchestrator action
verdict = "PASS" or all runs are unchangedPass. The build is clean. Proceed.
verdict = "FAIL" or any run has decision = "rejected"Fail. Read last_rejection.comment from rejected runs and feed the comments back to the developer AI.
verdict = "REVIEW" / "REVIEW_NEW"Keep polling. The review is not yet complete.
verdict = "WAIT"Keep polling. Diffing is still in progress.
No build found after CI has passedConfiguration error. The capture step did not run, the project slug is wrong, or the token doesn’t have access.

Both agent-summary and agent-builds responses include a web_url field when PIXMOAT_PUBLIC_URL is configured on the server:

  • Build-level: web_url links to the build review page.
  • Per-run: each entry in pending_review[] has a web_url that deep-links to the specific screenshot diff.

Use these URLs in dashboard UIs, Slack notifications, or PR comments. When PIXMOAT_PUBLIC_URL is not set, web_url is null — do not attempt to construct URLs manually.

The merge-blocking loop

On GitLab Free (no MR approval rules), the orchestrator works with a dedicated hard-failing CI job (pixmoat check --wait) to block merges while a review is open. The loop:

  1. CI pipeline runs: capture job uploads screenshots, visual-review job runs pixmoat check --wait and fails (exit 1 or 2) while diffs are pending.
  2. The orchestrator detects the red pipeline but does not treat the visual-review job failure as a build failure — it knows the review is in progress.
  3. A human approves (or the developer AI fixes and re-captures).
  4. The orchestrator retries the visual-review CI job (via GitLab API). pixmoat check now exits 0.
  5. The pipeline turns green and the MR is mergeable.

See Blocking merges on GitLab Free for the CI configuration.


Upgrading

get_screenshot default change

The MCP get_screenshot tool now returns a cropped triptych (baseline | current | diff, cropped to the largest changed region) by default. Previously it returned three separate full-page images.

This is an intentional change that reduces image inspection cost from ~3,700 tokens to ~300 tokens. If you need the previous full-page behaviour, pass region: "full":

get_screenshot(build_id: "...", run_ref: "h7k2", region: "full")

Or request individual panes with kind: "baseline", kind: "current", or kind: "diff".


Consumer Repository Setup

If your project uses pixmoat, add this to your CLAUDE.md or AGENTS.md:

## Visual Regression

After making UI changes, verify with pixmoat:
1. Run `npx playwright test` to capture screenshots
2. Run `pixmoat check --build <id> --wait --quiet` to check for regressions
3. See the full agent guide: <your-pixmoat-url>/docs/ops/ai-agent-guide.md