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.
| Operation | Before (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 payload | 4 of 5 return UNCHANGED (~15 tok each) |
| Clean build (PASS, nothing to review) | ~250+ tok | 0 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 type | Prefix | Scope | Best for |
|---|---|---|---|
| Project access token | pxg_ | Single project | CI pipelines — least privilege, no cross-project access |
| Personal access token (PAT) | pxgp_ | All projects the user can access | Workstation 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:
| Verdict | Exit code | Meaning | Action |
|---|---|---|---|
PASS | 0 | No pending diffs, no pending new, no rejections | Continue. Build is clean. |
REVIEW | 1 | Pending diffs (with or without new snapshots) | Inspect diffs, fix or approve. |
REVIEW_NEW | 2 | Pending new snapshots only, zero diffs | Approve to establish baselines. |
FAIL | 3 | Any run rejected, build status failed, or size-mismatch run | Read last_rejection comment. Fix the issue. |
WAIT | — | Diffing 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 ofPASS,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 forNEWruns.severity:trivial,minor,moderate, ormajor. Omitted forNEWruns.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
NEWruns or runs withoutdiff_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:
h7k2in build A refers to a different run thanh7k2in 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 field | Location | Description |
|---|---|---|
verdict | Top-level | "PASS", "REVIEW", "REVIEW_NEW", "FAIL", or "WAIT" |
digest | Top-level | 8-char hex state hash for conditional polling |
handle | Per run | 4-char handle for compact references |
view_url | Per run | URL 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:
-
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_approvesettings, and nature-mismatch checks. A group approval is a convenience shorthand — it never bypasses guardrails that would block individual run approval. -
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
| Severity | Meaning | Action |
|---|---|---|
trivial | Scattered sub-pixel changes, ratio < 0.1%. Anti-aliasing or font rendering noise. | Usually safe to approve. |
minor | Clustered small change, ratio < 1%. A small layout shift or color tweak. | Inspect if the change was not intended. |
moderate | Noticeable change, ratio < 5%. Real UI difference. | Always inspect before deciding. |
major | Large 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:
| Bucket | Meaning |
|---|---|
expected_changes | Named in intent AND actually changed. Auto-approvable. |
unexpected_changes | NOT named in intent BUT changed. Inspect these. |
expected_unchanged | Named 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:
| Value | Behavior |
|---|---|
"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 themin_approvalscount. - Agent approvals do not satisfy the
required_rolerequirement. - Agent approvals do not satisfy the
require_distinct_from_authorrequirement.
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.
| Result | CLI exit | MCP status | Required action |
|---|---|---|---|
| Terminal approved | 0 | approved | Continue with the workflow. |
| Terminal rejected | 3 | rejected | Return the review comments to the developer workflow. |
| Processing | 2 | waiting | Wait, then inspect the current state before retrying. |
| Undecided runs | 1 | pending_decisions | Inspect and decide the listed actionable runs. |
| Human policy remains | 1 | human_action_required | Wait for quorum, role, or CODEOWNERS approval. |
| Agent finishing disabled | 1 | agent_finish_not_allowed | Leave completion to a human. |
| Auth/config/server failure | 4 | MCP error | Fix configuration or connectivity; do not invent a result. |
The project selects the operating mode; the agent does not:
| Mode | Configuration | Completion behavior |
|---|---|---|
| Manual | agent_auto_approve: none | Agent feedback is advisory; a human decides and finishes. |
| Hybrid | intent_only, agent_may_finish: false, or unmet human quorum/CODEOWNERS | Agent triages and comments; Pixmoat waits for human action. |
| Fully automatic | agent_auto_approve: all, agent_may_finish: true, and all human-policy requirements satisfied | Agent 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 }
}
}
| Field | Meaning |
|---|---|
new_violations | Violations not present in the baseline — these are regressions introduced by this build |
resolved_violations | Violations that were in the baseline but are now absent — improvements |
total_violations | Total violation count across all snapshots |
by_impact | Breakdown by severity: critical, serious, moderate, minor |
by_rule | Breakdown 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:
| Visual | Performance | Accessibility | Action |
|---|---|---|---|
| Clean | Clean | Clean | All clear |
| Diffs | Clean | Clean | Review visual diffs only |
| Clean | Clean | Violations | Fix accessibility violations or approve as baseline |
| Diffs | Degraded | Violations | Review 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
| Mode | Effect |
|---|---|
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
| Rule | What it means | Agent action |
|---|---|---|
color-contrast | Foreground/background contrast ratio is below the WCAG threshold | Check if your CSS change affected text or background colors. Fix the contrast ratio. |
label | A form input is missing an accessible label | Add an aria-label, aria-labelledby, or associated <label> element |
link-in-text-block | A link is only distinguishable from surrounding text by color | Add an underline or other non-color visual indicator |
aria-required-attr | An element with an ARIA role is missing required attributes | Add 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 }
]
}
}
| Field | Description |
|---|---|
result | new (no baseline), clean (no new findings), warnings (new non-fail_on findings), violations (new fail_on findings) |
new_errors | Count of new pageerror and error findings not in the baseline |
failed_requests | Count of new requestfailed findings not in the baseline |
top | Up 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:
| Visual | Performance | Accessibility | Console | Action |
|---|---|---|---|---|
| Clean | Clean | Clean | Clean | All clear |
| Diffs | Clean | Clean | Violations | Review visual diffs and fix console errors |
| Clean | Clean | Clean | Violations | Fix console errors or approve as baseline |
| Diffs | Degraded | Violations | Violations | Review 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
| Mode | Effect |
|---|---|
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
- Iterate locally against
previous: capture, check--against previous, fix, repeat. - Run once against
baseline: when satisfied, runpixmoat check --local(against cached baselines) to confirm parity with the server’s view. - 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)
®ion=largest|<index>|all|full (default: largest)
&padding=24 (default: 24 image px)
&max_width=900 (default: 900)
run_refaccepts a handle (e.g.h7k2) or a UUID.kind=triptychcomposes baseline, current, and diff panes horizontally with 4px separator bars.region=largestcrops to the largest changed region expanded bypadding, clamped to image bounds.region=allcrops to the union bounding box of all regions.region=fullreturns the full page (no cropping).- Results are content-addressed and cached in the blob store.
- When no
diff_analysisexists (pre-ADR-026 runs),region=largestdegrades toregion=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
}
}
| Field | Description |
|---|---|
regions | Bounding boxes of changed areas (up to 20). |
regions[].nature | Per-region nature classification: class, confidence, optional shift/color params. Use it as triage context before following the visual review guide. |
regions[].elements | Attributed DOM elements overlapping this region (up to 3, most specific first). Empty array when no element map was captured. |
region_count | Number of distinct changed regions. |
largest_region_area_ratio | Fraction of image area covered by the largest region. |
spread | clustered (<=5 regions, <10% area), scattered (many small changes), or full_page (>60% area). |
mean_color_delta | Mean YIQ color distance of changed pixels (0–255 scale). Low = subtle; high = content change. |
severity | Rule-based severity: trivial, minor, moderate, major. |
nature | Run-level nature classification (dominant class by 80% rule). |
summary | Rule-templated sentence describing the change (e.g. “Content shifted down 8px in 2 regions”). |
aa_pixel_ratio | Fraction 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
| Tool | Purpose |
|---|---|
check_build | Check build for regressions — returns compact digest with verdict, handles, and region summaries |
list_builds | List recent builds and their statuses |
get_screenshot | Cropped triptych image (baseline|current|diff) around the largest changed region |
approve_runs | Approve one or more runs by handle, UUID, or group ID |
reject_run | Reject a run with a required comment |
finish_review | Explicitly finish staged decisions after all actionable runs are decided |
get_history | View recent changes for a snapshot name |
get_feedback | Read last human rejection comments for a snapshot |
list_projects | Discover 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:
| Condition | Orchestrator action |
|---|---|
verdict = "PASS" or all runs are unchanged | Pass. 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 passed | Configuration error. The capture step did not run, the project slug is wrong, or the token doesn’t have access. |
Using web_url for deep links
Both agent-summary and agent-builds responses include a web_url field when PIXMOAT_PUBLIC_URL is configured on the server:
- Build-level:
web_urllinks to the build review page. - Per-run: each entry in
pending_review[]has aweb_urlthat 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:
- CI pipeline runs: capture job uploads screenshots,
visual-reviewjob runspixmoat check --waitand fails (exit 1 or 2) while diffs are pending. - The orchestrator detects the red pipeline but does not treat the
visual-reviewjob failure as a build failure — it knows the review is in progress. - A human approves (or the developer AI fixes and re-captures).
- The orchestrator retries the
visual-reviewCI job (via GitLab API).pixmoat checknow exits 0. - 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