Skip to content

Your app, recorded from main.

A clip is a script in your repository. On every push, CI starts your app, records the clip again and renders the video. When the app only looks different, you get a new video. When a control the clip needs is gone, the build fails and names it.

version.cam, where the recordings go after CI, is in a private beta: the first ten accounts that ask have Pro free until it ends. Sign in with GitHub, or see what it adds first.

Each clip is a TypeScript file in the repository. The video beside it is what CI recorded from that file on the last push to main, and the lit line is the one running.

  1. import { clip } from "versioncam";
  2. /**
  3. * Filling in a form and waiting for the result.
  4. *
  5. * The app takes 800ms to create a shipment and shows a spinner while it does.
  6. * Neither appears in the clip as a wait: `settle()` advances the page's clock
  7. * without capturing, so the eight hundred milliseconds never reach the video.
  8. * That is the whole point of authoring time rather than measuring it.
  9. */
  10. export default clip(
  11. "01-create-a-shipment",
  12. { title: "Create a shipment", seed: 11 },
  13. async (s) => {
  14. // The spinner, held for a beat so it reads as work rather than as a
  15. // flicker, then the wait itself edited out.
  16. },
  17. );
v0.5.1main@0eeee9d00:00:00:00

The script recorded the video. The lit line is the one running; select a line to see its moment.

Frames
425
Captured
79
FPS
60
Recorded
2026-11-01 21:15 UTC

A new video

  • Move the button.
  • Change its colour, the layout, the theme.
  • Rewrite the copy around it.
  • Add a field the clip does not touch.

The clip finds its controls by test id or role, never by position. A change in how the app looks never fails a check: CI records the clip again, and the new look is the new video.

The build fails, by name

  • Rename the control's test id, or remove it.
  • Move the flow to another page.
  • A spinner that never resolves.
  • An app that will not start.

The failing clip names what it looked for and the second it gave up, and keeps a picture of the page at that moment. It renders nothing: a video of the wreckage would help nobody.

Every push to main runs npx versioncam check against the example app. On the left, the last run. On the right, the same check run once for this page, after one button’s test id was renamed and nothing else.

Livemain@0eeee9dexit 0
$ npx versioncam check
app: started with "npm run dev", HTTP 200 after 0.9 s; it stops when this command does
PASS  01-create-a-shipment
PASS  02-find-a-shipment
PASS  03-cancel-a-shipment

3/3 clips still match the app.
Captured oncecreate-shipment renamedexit 1
$ npx versioncam check
app: started with "npm run dev", HTTP 200 after 0.4 s; it stops when this command does
FAIL  01-create-a-shipment
PASS  02-find-a-shipment
PASS  03-cancel-a-shipment

2/3 clips still match the app.

01-create-a-shipment
  Nothing matched getByTestId('create-shipment') within 15000ms at t=3.71s (frame 222). The clip expected it to be on screen by this point.
The example app at the moment the clip gave up: the form is filled in, and the Create shipment button is on screen under its new test id.
Authored time
The page’s clock moves one frame at a time, and only when the recorder moves it. A slow query or a spinner is waited for off camera and never reaches the video. How it works
Real input
A real mouse follows a planned, seeded path, so hover and press states fire as they would for a person. The cursor you see is drawn afterwards, at output resolution. The clip DSL
Same bytes
Two recordings of a clip on one machine are the same file. versioncam stability proves your app does its part before a clip is written for it. Determinism
One sentence
In Claude Code, /versioncam “…” writes the clip, records it, and has a fresh reviewer judge the frames. There is no API key: the model is the session you already have. Authoring

The recorder and the GitHub Action are free and run in your CI, which already has your app and your secrets. version.cam is what happens to a recording after CI has made it.

What it adds

One address that only plays what passed
clips.version.cam/<owner>/<repo>/<clip> plays the newest version from your default branch whose check passed. A broken clip keeps playing the last one that did, and the dashboard says so.
Plays where the README is
An embed and oEmbed, so a pasted address unfurls in Notion and Slack; a GIF for the README; a screenshot of any beat, on request.
The pull request sees the clips
A check on the commit, and one comment, edited as the branch moves, with a picture of each clip that looks different or failed.
A badge that cannot lie
demo verified, the commit and how long ago, for the recorder's own repositoryThe commit it names is the one whose CI run published the recordings. This one is the recorder's own, live.
Told when it breaks
On Pro, by email, in Slack or as a GitHub issue: when a clip breaks on your default branch, again two days later, then weekly, until it passes or someone acknowledges it.
Private repositories, on Pro
A private repository's clips play only for people GitHub lets read it, through short-lived signed addresses. Recordings live in Cloudflare's EU jurisdiction.

The plans, as planned

Free $0
Public repositories. Three projects, ten clips a project, thirty days of history, clips to sixty seconds, 1 GB. The pull request check and the badge.
Pro $35 a project, a month planned
Private repositories too, by GitHub's own permissions. Unlimited history and a hundred clips a project. Told by email, Slack or an issue when a clip breaks. $350 a year.

Per project, never per seat. Planned means the final prices follow the first bill, and an account from the beta pays 20% less for its first twelve months. Ten projects and more are priced in the docs. The recorder and the Action are free whatever you choose.

Sign in with GitHub

The beta is private: nothing is sold yet, and no card is asked for. Free works for public repositories as soon as you sign in. The first ten accounts that ask have Pro free until the beta ends: sign in, then write tohello@version.cam naming your account. How version.cam works says the rest.

Three commands and one sentence. The first run of the skill writes the config from what it finds in the repository, proves the app records the same way twice, and writes the first clip. On the example app that took six to twelve minutes and one or two review rounds.

Terminal
npm i -D versioncam
npx versioncam install
npx versioncam init
Then, in Claude Code
/versioncam "Show a user filtering the list down to one shipment"

versioncam 0.5.3 on npm · FSL-1.1-ALv2 · Imprint · The source is public at github.com/versioncam/versioncam, and so is this site’s.