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.
bv evidence --from evidence.json --push # configured modebv evidence --from evidence.json --push --mode remote # private remote URLIn 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
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Page <title> and the H1 above the gallery. ≤200 chars. |
subtitle | string | no | Single secondary line below the title. ≤300 chars. |
summary | string | no | Short paragraph above the gallery (e.g. ticket ref, what was delivered). ≤2000 chars; preserves line breaks. |
metadata | object | no | Work-management issue for the whole gallery when all items evidence one ticket/story/issue. |
items | array | yes (≥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.
| Field | Type | Required | Notes |
|---|---|---|---|
issue_url | string | no | Absolute HTTP(S) URL for the work item, e.g. a Jira, Linear, GitHub, or Todoist issue URL. |
issue_id | string | no | Work item identifier/key, e.g. DELIVERY-1234, ENG-456, or #789. |
issue_title | string | no | Work item title/summary from the source work-management system. |
Item fields
| Field | Type | Required | Notes |
|---|---|---|---|
src | string | yes | Local 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. |
title | string | no | Heading shown above the asset. ≤200 chars. |
description | string | no | Body text shown beneath the asset. ≤2000 chars. |
sequence | integer | no | Explicit ordering. Sequenced items sort ascending; un-sequenced items keep JSON-array order and follow. Stable sort. |
alt | string | no | Alt text for images. Defaults to the item’s title; never falls back to description. |
metadata | object | no | Work-management issue for this capture when it differs from, or is more specific than, the top-level issue. |
properties | object | no | Arbitrary 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:
bv evidence --schemaThat 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:
| Extension | MIME |
|---|---|
.png | image/png |
.jpg, .jpeg | image/jpeg |
.webp | image/webp |
.gif | image/gif |
.mp4 | video/mp4 |
.webm | video/webm |
.mov | video/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
bvversion and a UTC publication timestamp that the viewer localizes in-browser. - Static JavaScript only.
evidence.jsis 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, andevidence.js; copied assets are the bulk of the bundle.
Caps
| Limit | Value |
|---|---|
| Per-asset file size | 1 GiB |
| Items per evidence site | 1..500 |
Per-item properties count | 50 |
properties key length | 1..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 sites | 30 / tenant / month |
| Paid-tier evidence sites | 300 / 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 gatestemplate=evidencebehind 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 withouttemplate.
See error codes for the full list of
non-zero exits from bv.
See also
bv agent-init— the/butverifyskill callsbv evidenceautomatically once installed.bvCLI reference — every subcommand.- Error codes — what each non-zero exit
from
bvmeans.