Guides · how-to

How to snapshot-test a Claude Code workflow

▸ What you'll do

The mental model is one line: the first run writes the snapshot, and every run after diffs against it. A snapshot test for a Claude Code workflow records the tool calls your agent made on a known-good run, freezes them as a baseline, then re-runs that baseline and fails loudly the moment the calls diverge — the same discipline UI snapshot tests brought to rendered components, applied to an agent's tool-call trace.

This guide walks the whole loop with the reelier CLI: scan your Claude Code history for a run worth freezing, compile it to a SKILL.md, replay it deterministically, and diff two runs so drift becomes an exit code. Every command below is real and every output is verbatim from an actual run.

Before you start

  • Reelier installed — npm i -g reelier (or use npx -y reelier@latest in place of reelier in every command below).
  • One completed Claude Code session that drove real MCP or HTTP tool calls — a data pull, a status check, a report. Reelier replays tool calls (mcp__server__tool, http.get/http.post), not Bash/Read/Edit/Write — so pick a run whose value was in the API calls, not the file edits.
  • Nothing else. No account, no API key: deterministic replay never calls a model.

Find the run worth freezing

Do the task once with your agent the way you already do — Claude Code writes every session to a transcript under ~/.claude/projects as it goes. reelier scan reads those transcripts, keeps only the deterministically-replayable tool-call sequences, and ranks the read-only ones first (they are the safe snapshots — a run full of writes re-fires those writes on every replay).

The picker prints one line per candidate: how many calls are replayable, how many are read-only versus side-effectful, and which MCP servers each touched. Pick the read-only run that captures the workflow you want to guard. (Scan finds few read-only sessions for most people — only about 1.1% of scanned sessions were read-only end-to-end — so this step is also where you learn which of your workflows are snapshot-ready at all.)

How capture works on each agent — including why Cursor is wrap-proxy only — is covered on the platform page; this guide stays on the how-to.

terminal
reelier scan

Compile the session into a SKILL.md

Point from-session at the transcript the scan surfaced (or let scan write the skills for you). It reads the recorded tool calls and emits a SKILL.md — a plain markdown file, checked into your repo, that IS the snapshot baseline.

The compiler writes minimal assertions: only a step that recorded a clean result gets an assert, and it asserts a shape, not a bound value. In the run below, one http.get recorded a 200, so the step gets exactly one assertion — status == 200 — and zero brittle value-pins. Steps that recorded no result stay assertion-less and will replay unchecked, never falsely passed.

Reelier also tells you the truth about replay-worthiness before you schedule anything: here, ✓ all 1 steps are read-only — safe to replay repeatedly. (Paths in the output are shortened to the filename.)

terminal
reelier from-session registry-check.jsonl
Reelier · output
Scanned registry-check.jsonl: 1 replayable call(s) found.


Wrote registry-check.skill.md
  steps:   1
  asserts: 1
  binds:   0

✓ all 1 steps are read-only — safe to replay repeatedly

Open questions (1):
  - Step 1: no narration — describe what this step is for.
verbatim output

Replay it — the first run writes the snapshot

reelier run re-executes the recorded tool calls against the live systems and checks each assertion. No model is in the loop — at the default --max-level 0 an LLM client is never even constructed — so the replay costs 0 tokens and the result is the deterministic core of the workflow, nothing re-reasoned.

Every run appends a receipt to .reelier/runs/<skill>.jsonl. The first run establishes the baseline the next step diffs against; each run after is a dated line in that append-only record.

terminal
reelier run registry-check.skill.md
Reelier · output
✓ Step 1 — Call http.get [passed] 334ms

PASSED: 1/1 steps ok, 0 failed, 334ms total
verbatim output

Fail on drift — diff two runs

Run the skill a second time, then reelier diff <name> compares the last two runs in that record, step by step. It reports SAME and exits 0 when every step's outcome and escalation level match the baseline; it reports DRIFTED and exits 1 when a step's outcome flips, a step is added or removed, or a step needed a different level to pass. That exit code is the whole point: wire this command into CI and a drift blocks the merge.

Data that legitimately changes run-to-run is not drift. The assertion checks a shape (status == 200), so a fresh response body on each honest run updates the receipt without failing the snapshot — the verdict below stayed SAME across two live calls to the npm registry seconds apart.

When the diff fails because the upstream changed on purpose, you update the baseline the same way you would a UI snapshot: edit the failing assertion in the SKILL.md (it is markdown in your repo), or re-record the run and recompile.

terminal
reelier diff registry-check
Reelier · output
  = Step 1 "Call http.get": passed, unchanged.

✓ SAME — 1 step(s), every outcome and escalation level identical to the baseline.
verbatim output

The receipt

Reelier · replay receipt
$ reelier run registry-check.skill.md
✓ Step 1 — Call http.get [passed] 334ms

PASSED: 1/1 steps ok, 0 failed, 334ms total

$ reelier run registry-check.skill.md
✓ Step 1 — Call http.get [passed] 186ms

PASSED: 1/1 steps ok, 0 failed, 186ms total

$ reelier diff registry-check
  = Step 1 "Call http.get": passed, unchanged.

✓ SAME — 1 step(s), every outcome and escalation level identical to the baseline.
verbatim output

The whole snapshot loop, verbatim: the compiled registry-check skill replayed twice against the live npm registry, then diffed. Produced on 2026-07-22 with reelier v0.14.0 in a scratch directory; the skill was compiled by reelier from-session from a one-call Claude Code-style transcript (a single http.get, shaped per reelier/test/session.test.ts). No model was constructed at any point; reelier diff exited 0 (a drift would exit 1).

FAQ

Is this different from running evals on the workflow?

Yes. A snapshot test checks exactness against one recorded known-good run: binary verdict, no model, no rubric. Evals score output quality across a dataset and the judge is often a model. They compose — snapshot the deterministic tool-call core, spend eval effort on the judgment steps a snapshot can't cover.

My workflow writes data. Can I still snapshot it?

Replay is read-only by default — recorded write steps do not re-fire unless you pass --allow-writes (which unlocks idempotent writes only; a destructive step still needs --yes). For a snapshot you run on a schedule, prefer a read-only run, or gate the writes deliberately. scan flags which of your runs are read-only so you know what is safe to freeze.

The diff failed after an intentional change. Now what?

Update the baseline. When the contract change is small, edit the failing assertion in the SKILL.md — it is markdown in your repo. When the workflow itself changed, run the job once with your agent and recompile with from-session: Reelier builds steps only from a run that actually happened, never from instruction text.

Reading as an agent? This page has a markdown twin: /guides/snapshot-test-claude-code-workflows/md