# How to snapshot-test a Claude Code workflow

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.

Markdown twin of https://www.reelier.com/guides/snapshot-test-claude-code-workflows — a step-by-step Reelier how-to. Every command below is a real Reelier command and every output block is verbatim, never a mockup.

## 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.

## 01 · 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.

```bash
reelier scan
```

## 02 · 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.)

```bash
reelier from-session registry-check.jsonl
```

```
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.
```

## 03 · 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.

```bash
reelier run registry-check.skill.md
```

```
✓ Step 1 — Call http.get [passed] 334ms

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

## 04 · 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.

```bash
reelier diff registry-check
```

```
  = Step 1 "Call http.get": passed, unchanged.

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

## The 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.
```

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.

## Related

- [Snapshot testing for AI agents — the pattern this guide runs, defined](https://www.reelier.com/learn/agent-snapshot-testing)
- [Reelier for Claude Code — where scan reads your history, and how capture works](https://www.reelier.com/for/claude-code)
- [Reelier for GitHub Actions — run this snapshot as a CI gate on every push](https://www.reelier.com/for/github-actions)
- [Guide: diff two agent runs and fail CI on drift](https://www.reelier.com/guides/diff-two-agent-runs)
- [Agent drift — the failure mode the diff is there to catch](https://www.reelier.com/learn/tool-call-drift)

## Sources

- [seldonframe/reelier — the open-source (AGPL-3.0) CLI every command here runs](https://github.com/seldonframe/reelier)
- [Jest: Snapshot Testing — the render-compare-fail discipline this borrows](https://jestjs.io/docs/snapshot-testing)
- [agentsnap — independent snapshot tests for agents (record traces, diff baselines, fail CI)](https://github.com/MukundaKatta/agentsnap)

Other guides: [How to record MCP tool calls with a wrap proxy](https://www.reelier.com/guides/record-mcp-tool-calls) · [How to replay an agent workflow at 0 LLM tokens](https://www.reelier.com/guides/replay-agent-workflow-zero-tokens) · [How to gate CI on agent drift](https://www.reelier.com/guides/ci-gate-for-agent-drift) · [Replay an agent skill on every PR with the Reelier GitHub Action](https://www.reelier.com/guides/github-action-agent-replay) · [Test your MCP server for regressions by replaying a golden run](https://www.reelier.com/guides/test-mcp-servers) · [Run a deterministic cron agent by scheduling a recorded replay](https://www.reelier.com/guides/deterministic-cron-agents) · [Build an agent audit trail from Reelier run receipts](https://www.reelier.com/guides/agent-audit-trail-receipts) · [Configure BYOK escalation levels: L0, L1, and L2](https://www.reelier.com/guides/byok-escalation-levels) · [Compile a skill from an existing session with reelier from-session](https://www.reelier.com/guides/from-session-compile) · [Diff two agent runs with reelier diff (exit 1 on drift)](https://www.reelier.com/guides/diff-two-agent-runs) · [Convert an LLM agent skill into a deterministic, assertion-checked skill](https://www.reelier.com/guides/convert-agent-skill-deterministic)
