agentvcr

How agentvcr works

agentvcr turns a coding agent's session into a replay you can watch, send as a link or get on a pull request. Most of the work is in three places: reading the transcripts of eight agents, each with its quirks, masking secrets without masking everything, and keeping one HTML file fast on sessions with thousands of steps.

01Eight agents, one shape

Every agent already writes its session to disk, as JSON lines, a JSON document or a database. agentvcr reads them into one shared shape (prompts, answers and tool calls with their results) and the player only knows that shape. The parsers are plain modules without Node APIs, so the same code runs in the CLI and on the website, where a dropped file is parsed in the browser.

AgentWhereThe quirk
Claude Code~/.claude/projectsOne line per content block, and every line repeats its message's token usage. Counted naively, a 300-message session reports its tokens about three times over, so usage is counted once per message id.
Codex~/.codex/sessionsThe same content arrives twice, as response_item and as event_msg. Only the first is read. Shell output can be wrapped in JSON with an exit code inside.
Cursor~/.cursor/projects/…/agent-transcriptsTool calls without their results and no per-step times. The replay counts steps instead of minutes, and the start time comes from a timestamp tag in the first prompt.
Gemini CLI~/.gemini/tmp/…/chatsAppend-only JSONL with edits to the past: $rewindTo drops messages, $patch fills in tool results later, $set can replace the whole list. They are applied in order, the way the CLI loads a chat.
Qwen Code~/.qwen/projects/…/chatsRecords form a tree. Rewinding leaves the abandoned turns in the file, so the conversation is the chain from the last record back through parentUuid, the way Qwen Code walks it itself. Records that arrive in fragments under one id are merged first.
Cline~/.cline/data/sessionsTwo formats. Tasks from before Cline's SDK are a list of UI messages with each tool call as JSON inside the message text, and commands that wait for approval end in a marker that has to come off. SDK sessions are plain content blocks, but the working directory sits in a manifest next to them. Roo Code and Kilo Code tasks use the older format.
Aider.aider.chat.history.mdNot JSON at all: the Markdown aider shows you, appended run after run. Typed lines start with ####, aider's own messages with >, and edits are search/replace blocks after a file name (or a udiff that names the file in +++). An edit counts as applied only once aider says so; a block that failed to match is marked.
OpenCodea databaseRead from opencode export, in both the current format (typed messages with tool state) and the older one with message parts.

Detecting the format from the first 64 KB has one trap worth naming: Claude Code lines contain message ids like msg_01…, which is also what OpenCode ids look like. Matching on ids misfiled every Claude session, so OpenCode is recognised by how its document opens instead, and a test pins that down.

02Masking without masking everything

Replays are made to be shared, so secrets are masked before anything is written: 25 patterns for provider keys and tokens (Anthropic, OpenAI, GitHub, AWS, Stripe, Hugging Face and others), JWTs, bearer tokens, private keys, credentials in URLs, webhook URLs, email addresses, and the home folder and username.

The hard part is the generic rule for KEY=value pairs whose name sounds secret. Run naively over real sessions, it masked max_tokens: 5000, token: string and apikey: $SUPABASE_KEY. Now the value has to mix letters and digits and must not start with $, and each of those look-alikes is a test that must stay unmasked. On the largest sessions it was tested on, what remains masked is keys, tokens and email addresses.

Patterns cannot know a password someone made up. So the CLI also takes the exact values of the secret-looking variables in your environment and in a .env in the folder it runs in (names with KEY, TOKEN, SECRET, PASSWORD and so on, values of at least eight characters, no paths) and masks every occurrence as [env:NAME] before the patterns run. An exact value cannot over-mask, and it catches the classic leak of an agent running cat .env. The values are only compared, never written. The idea came from a comment on the launch post.

Patterns are grouped (keys, tokens, URLs, variables, emails, home), which lets the website switch each group off to show what it catches. It is still pattern matching, so the CLI reports how many things it masked and asks you to skim a replay before sharing it.

03Sharing without a server

--share uploads the already masked replay data as a secret gist on your GitHub account and prints a link to a static viewer on GitHub Pages. The viewer fetches gist.githubusercontent.com/<owner>/<id>/raw/agentvcr.json: the raw CDN allows cross-origin reads and, unlike the gist API, has no 60-requests-per-hour limit per visitor. Nothing of agentvcr's runs in between, and deleting the gist takes the replay down. The full working directory never leaves your machine; only its last folder name is kept as the project name.

04One file that stays fast

A replay is a single HTML file: the player, its CSS and JS, the session as JSON, and the pixel font for the counter, subset to ASCII and embedded at 3.4 KB. Transcript text can contain </script>, so every < in the embedded JSON is escaped; a test feeds it exactly that.

Playback reveals events along a compressed timeline, so an hour of work plays in under a minute while the counter shows real working time (breaks over five minutes count as away). Skipping to the end of a long session first took seconds. Each long output was measured right after it was shown, which forced a layout per step:

4,929 ms → 11 ms

for a session with 5,000 tool calls. Outputs are now measured when they come near the viewport, and total layout time for that session dropped from 4.1 s to 0.2 s.

05Clips on a controlled clock

-o clip.mp4 and -o clip.gif record a replay with the Chrome and ffmpeg already on your machine. Recording in real time drops frames whenever a screenshot is slow, so the page's clock is replaced: performance.now and requestAnimationFrame only advance when the recorder says so, one frame at a time, with a screenshot after each. The clip is equally smooth on any machine. GIFs go through ffmpeg's palette pass, which keeps the blue screen free of banding at a fraction of the size.

06How it is checked

agentvcr is open source under the MIT licence: github.com/tojacob03/agentvcr. Try it with npx agentvcr-cli, or watch the example replay.