Skip to content
← butverify.dev

bv evidence

bv evidence is the CLI subcommand that turns a small evidence.json manifest plus local screenshot/video files into a static gallery. Agents run it after finishing a piece of work to surface clickable proof — captioned, ordered, and either served locally or published as an identity-gated remote microsite — instead of dumping a wall of screenshots into chat.

Terminal window
bv evidence --from evidence.json --push # configured mode
bv evidence --from evidence.json --push --mode remote # private remote URL

In remote mode, bv prints JSON with the published URL and its expiry:

{ "url": "https://<site>.butverify.dev", "expires_at": "2026-05-04T17:31:02Z" }

The render is fully client-side: the control plane never sees your raw screenshots. The published bundle is static HTML, CSS, a small local JavaScript controller, and copied assets, so the gallery first-paints without network round trips after the page itself loads.

Input contract — evidence.json

A minimal manifest:

{
"title": "Login page redesign",
"subtitle": "Ticket DELIVERY-1234 · 2026-04-27",
"summary": "Updated the login form to match the new identity. All states pass automated tests; here is the human-visible proof.",
"metadata": {
"issue_url": "https://jira.example.com/browse/DELIVERY-1234",
"issue_id": "DELIVERY-1234",
"issue_title": "Login page redesign"
},
"items": [
{
"src": "./screenshots/01-empty.png",
"title": "Empty state",
"description": "Page loads with no validation errors visible.",
"sequence": 1
},
{
"src": "./screenshots/02-error.png",
"title": "Inline validation",
"description": "Empty-email submit shows the helper inline; field gets aria-describedby.",
"metadata": {
"issue_url": "https://jira.example.com/browse/DELIVERY-1235",
"issue_id": "DELIVERY-1235",
"issue_title": "Inline validation bug"
},
"properties": {
"commit": "abc123",
"build_number": 42,
"checks": { "unit": "passed", "lint": "passed" }
},
"sequence": 2
},
{
"src": "./videos/03-success.webm",
"title": "Successful sign-in",
"description": "End-to-end flow from form fill to dashboard redirect (3s clip).",
"sequence": 3
}
]
}

Top-level fields

FieldTypeRequiredNotes
titlestringyesPage <title> and the H1 above the gallery. ≤200 chars.
subtitlestringnoSingle secondary line below the title. ≤300 chars.
summarystringnoShort paragraph above the gallery (e.g. ticket ref, what was delivered). ≤2000 chars; preserves line breaks.
metadataobjectnoWork-management issue for the whole gallery when all items evidence one ticket/story/issue.
itemsarrayyes (≥1)One gallery entry per element.

Metadata fields

metadata is optional at both the top level and on each item. Use the top-level object when the gallery proves one work-management item. Use item-level metadata when a specific screenshot/video maps to a different or more specific item.

FieldTypeRequiredNotes
issue_urlstringnoAbsolute HTTP(S) URL for the work item, e.g. a Jira, Linear, GitHub, or Todoist issue URL.
issue_idstringnoWork item identifier/key, e.g. DELIVERY-1234, ENG-456, or #789.
issue_titlestringnoWork item title/summary from the source work-management system.

Item fields

FieldTypeRequiredNotes
srcstringyesLocal relative path to the asset. Resolved against the directory of the --from file (or CWD when --from -). MUST stay inside that directory — lexical and symlink.
titlestringnoHeading shown above the asset. ≤200 chars.
descriptionstringnoBody text shown beneath the asset. ≤2000 chars.
sequenceintegernoExplicit ordering. Sequenced items sort ascending; un-sequenced items keep JSON-array order and follow. Stable sort.
altstringnoAlt text for images. Defaults to the item’s title; never falls back to description.
metadataobjectnoWork-management issue for this capture when it differs from, or is more specific than, the top-level issue.
propertiesobjectnoArbitrary per-item metadata. Up to 50 non-empty keys, each ≤100 chars. String values are ≤1000 chars.

String and number properties render as label/value rows. Object values render as formatted JSON in the collapsed item details panel.

The JSON is parsed in strict mode: unknown top-level or item fields fail at parse time with a path pointing at the offending JSON node. The canonical schema is bundled into the binary and printable on demand:

Terminal window
bv evidence --schema

That writes a Draft 2020-12 JSON Schema to stdout and exits 0 — useful for editor integration and for asserting your manifest is valid before running the renderer.

Flags

--from <path|-> (required unless --schema)

Reads the manifest from a JSON file, or from stdin when the value is -. Stdin must be piped — running bv evidence --from - from an interactive TTY exits with a usage error rather than hanging. Stdin payloads are capped at 4 MiB (the manifest is text — assets stay out-of-line on disk).

--out <dir>

Render-only mode. Produces a self-contained static bundle at <dir> (index.html, styles.css, evidence.js, assets/) that opens in a browser without network. The renderer writes to a sibling temp directory and atomically renames into place on success — your --out path is never partially written and never rm -rf’d.

--push

Renders into a CLI-owned temp directory and runs the standard push pipeline. In local mode, bv serves the rendered gallery on 127.0.0.1 until interrupted. In remote mode, it returns the published private URL:

{ "url": "https://<site>.butverify.dev", "expires_at": "2026-05-04T17:31:02Z" }

The temp directory is cleaned up whether the push succeeds or fails. --push and --out are mutually exclusive in spirit — pick whichever end you want.

--mode local|remote

Overrides the configured publish mode for this invocation. Fresh installs default to local; bv login switches the default to remote.

--schema

Prints the JSON Schema for evidence.json to stdout and exits 0. The example payload in this page validates against it.

--ttl-seconds N

Paid-plan TTL override for the published site. Free-plan accounts get the default platform TTL.

--upload-id <id>

Idempotent retry token for --push. If a push is interrupted between upload and finalize, re-running with the same --upload-id reuses the in-flight upload instead of starting over.

Containment & containment root

Every asset src is resolved inside a single containment root:

  • --from <path> — the directory of the manifest file.
  • --from - (stdin) — the current working directory.

The renderer enforces this in three steps: it canonicalises the root with filepath.EvalSymlinks, joins each src against it, canonicalises the result, and rejects anything whose relative path starts with .. or resolves absolute. Both lexical (../../etc/passwd) and symlink-based traversal fail during render preflight before any bytes are copied.

Recommended: prefer --from <path> for narrow containment. Running bv evidence --from - from a broad CWD (e.g. a repo root) widens the containment root and gives prompt-injected paths more surface to play with. --from <path> keeps the root scoped to a dedicated evidence working directory.

Supported MIME types

The accepted asset types are a closed allowlist:

ExtensionMIME
.pngimage/png
.jpg, .jpegimage/jpeg
.webpimage/webp
.gifimage/gif
.mp4video/mp4
.webmvideo/webm
.movvideo/quicktime

Each asset passes a two-gate check: the file extension AND the result of http.DetectContentType over the first 512 bytes must both be in the allowlist and must agree. Polyglot files and renamed extensions fail one or both gates and abort the render.

SVG is excluded by design. SVG is an executable XML format (<script> tags, event handlers, external href); serving an attacker-supplied SVG verbatim from the published site would create an XSS surface inside its origin. Agents producing screenshot evidence should output PNG/JPEG/WebP.

Layouts

Rendered evidence pages include a layout switcher. Viewers can swap between a vertical stacked view and a horizontal carousel without republishing the site.

The stacked view renders each item as title, asset, and description in JSON-resolved order inside a scrollable content panel with a collapsible outline. Item issue metadata and properties are hidden by default behind each capture’s collapsed More Details panel. The carousel view uses horizontal snap-scroll with previous/next buttons, paging buttons, and left/right keyboard navigation; previous is disabled on the first capture and next is disabled on the last capture. Carousel media is fit inside the viewport without cropping or stretching, while long captions scroll inside a fixed-height caption area.

Top-level issue metadata appears in the pinned metadata bar and expandable metadata sidebar. The metadata bar also shows the item count, layout controls, bv version, and publication timestamp; the timestamp is rendered in the viewer browser’s local timezone. The light/dark mode toggle follows the browser preference until a viewer toggles it, then stores the last choice in localStorage for future butverify pages on the same browser origin. Both views share the same HTML and JSON contract. Clicking an image opens a lightbox with zoom controls and a fullscreen toggle.

Bundle properties

  • Publication-stamped. Rendered pages embed the bv version and a UTC publication timestamp that the viewer localizes in-browser.
  • Static JavaScript only. evidence.js is bundled locally and drives layout toggles, metadata/outline panels, carousel navigation, and image lightbox controls. It also persists the light/dark theme preference in localStorage. It does not fetch remote code or data.
  • Small. A typical input renders to under 100 KiB across index.html, styles.css, and evidence.js; copied assets are the bulk of the bundle.

Caps

LimitValue
Per-asset file size1 GiB
Items per evidence site1..500
Per-item properties count50
properties key length1..100 chars
properties string value length≤1000 chars
Stdin manifest size (--from -)4 MiB
Bundle upload (free tier)100 MB
Bundle upload (paid tier)1 GB
Free-tier evidence sites30 / tenant / month
Paid-tier evidence sites300 / tenant / month

Error envelopes worth knowing

  • HTTP 402 — fairness cap exceeded for the month (free tier 30, paid 300 evidence sites/tenant/month). The CLI surfaces a clear envelope; no site is created and the temp directory is cleaned.
  • HTTP 413 — bundle exceeds the per-tier upload cap (100 MB free / 1 GB paid). Trim videos or split into multiple evidence sites.
  • EV-E-8 (HTTP 400, distinctive)evidence template not yet enabled on this control plane; retry after the server-side rollout completes. The control plane gates template=evidence behind a rollout flag; if you hit this, the CLI is ahead of the server and will work once the Worker rolls out. The CLI does not silently retry without template.

See error codes for the full list of non-zero exits from bv.

See also