Zoetrope turns an agent transcript into a graph, and its replay flags only fit files
Watch a Claude Code session as a live flow graph, in your terminal or your browser.
At a glance
- What is it?
- Zoetrope is a Rust terminal UI that reads the transcripts Claude Code and Codex already write, then draws the main agent, its subagents and their tool calls as a live or replayable graph. It is read-only with no network, the same engine compiles to WebAssembly for a browser build, and every documented replay option arrives with a .jsonl path attached to it.
- Who is it for?
- Zoetrope earns its place for anyone who has lost track of which subagent ran which command, and its read-only, no-network design means pointing it at a live session carries no write risk. Before building habits around the replay flags, check which start form your workflow actually uses, since speed and follow are documented only against a file argument and the promise of identical controls across every start form is not spelled out.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Every replay option in the usage block arrives with a file path attached
The usage block pins each option to a literal `.jsonl` path. Speed control appears only as `zoe <file.jsonl> --speed N`, carrying a documented default of 8.0. Follow mode appears only as `zoe <file.jsonl> --follow`, introduced as opening a recording at its live edge. The one option that drops the path is `--provider codex`, and that line ends in a literal ellipsis with no list anywhere of which provider names the flag accepts. By id you get a single line, replay by id or by a unique prefix, and by directory you get `zoe <dir>` and nothing else. Running with no argument at all finds the newest session in the current project and follows it. The prose immediately after the block then claims the controls are the same whichever way you start, which leaves the id form and the directory form without any stated way to set speed or to jump forward. The gap is small but it lands on the two forms a person reaches for when working through an older run rather than watching a current one.
zoe # follow the current project's live session
zoe <dir> # follow another project's session
zoe <file.jsonl> # replay a recording from the start (any file of a session)
zoe <id> # replay a session by id, or a unique prefix of one
zoe <file.jsonl> --follow # open a recording at its live edge
zoe <file.jsonl> --speed N # playback speed (default 8.0)
zoe --provider codex ... # force the format instead of detecting it from the file
zoe inspect <file|id> # print the session tree and exit (no TUI)The same engine backs the browser page, compiled to WebAssembly through ratzilla, where you either open a session from disk or drop a transcript onto the page.
A file argument already tails the transcript, so --follow is a seek rather than a mode
Two descriptions of the file form sit next to each other and they do not line up neatly. The prose says that given a file, zoetrope reads the whole transcript and then keeps watching for new lines. The usage block says a file means replaying a recording from the start, and that `--follow` is the flag that opens a recording at its live edge. Read together, a file already does both things, and `--follow` is really a seek to the newest record rather than a switch into a different mode. That reading is consistent with the rest of the interface, where the live edge is a position on one timeline and `End` or `g` jumps to it, and where seeking backwards rewinds the graph to the state it held at that moment rather than loading a different session. What the documentation leaves open is the harder case. It says nothing about what happens after you seek backwards during a live follow, and it says nothing about how the live edge is decided for a transcript that has stopped growing. Whether the view parks at the last record or keeps waiting for more is not something a reader can settle from the page.
The scrubber is indexed by events while playback is paced by timestamps
The timeline is described as indexed by event rather than by wall-clock, so that a busy minute gets room instead of collapsing into a sliver. Replay, separately, is paced by the session's own timestamps. That is two clocks inside one feature, and the distinction does real work. Positions on the scrubber are a property of the transcript's structure, which is what makes dragging through a long run stable and repeatable. How fast the playhead advances is a property of how long the agent actually took between records, which is what makes a replay read like the session itself rather than like a summary of it. The only control over real time is a binary one: `s` toggles gap compression between faithful pacing and skipping idle stretches, so there is no dial between the two. Seeking backwards is where the two clocks meet, and the described effect is unusually concrete. Agents un-finish, tool counts fall, and the graph shrinks back to what it held at that moment. Prompt eras are the other unit of movement, stepped with `[` and `]`.
cargo build --release never produces the WASM build behind the hosted page
The from-source recipe yields the terminal binary and nothing else. `Cargo.toml` excludes `web/wasm` from the workspace, and the comment explaining why is unusually concrete about the cost. The browser frontend, `zoetrope-web`, can only be compiled for wasm32 rather than merely targeted at it, because rataflow gates its ratzilla `From` impls on `all(feature = "ratzilla", target_arch = "wasm32")`. As a workspace member the crate would be host-checked by `cargo check --workspace` and by rust-analyzer, which would sit on two permanent phantom errors in the editor. Excluding it resolves that frontend as its own workspace with its own lockfile and target directory, and trunk builds it through `web/scripts/build-wasm.sh`. So the claim that the hosted app is the same binary compiled to WASM holds, but nothing in the short build sequence below reaches it. Anyone wanting to reproduce what runs at zoetrope.furkankly.dev from source has to find that script separately.
git clone https://github.com/furkankly/zoetrope
cd zoetrope
cargo build --release
./target/release/zoeOne more exclusion is worth knowing about. The same file argues for an allow-list on packaged files rather than a deny-list, on the grounds that the inverse leaks: anything added to the repository later, such as recordings, web fonts or an OG image, silently lands in the published tarball until someone audits it. The comment then ends mid-sentence, right after naming GIFs and tapes, so what it says about those is unstated.
j and k are bound twice in the keymap with no stated priority
The short key line offers five things: `space` for play and pause, `[` and `]` to step between prompt eras, `End` or `g` to jump to the live edge, dragging the bar to seek, and `?` for everything else. The full table behind `?` then binds `j` and `k` twice. They appear under `h j k l` for panning the graph, and they appear again for scrolling the detail panel, and neither row states a condition on when it applies. The panel opens on click and carries its own scrolling keys including `PgUp` and `PgDn`, so the same two presses have to mean one thing while a panel is open and another when it is not. The table never says which wins. The rest of the bindings are unambiguous: arrows and `Tab` and `Shift-Tab` move between agents, `+` and `-` and `0` zoom in, out and reset, `c` centers on the selected agent, `r` relayouts the graph, and `o` and `f` switch the camera between Overview and Follow. A minimap appears once the graph outgrows the screen, which is when the pan bindings start earning their place.
Formats are told apart by content, and unreadable records are skipped rather than fatal
A session file is identified by what is inside it rather than by where it sits, which is why `zoe <file>` works for a Claude Code transcript and a Codex one alike, and why `--provider` exists as an override for when the guess is wrong. Claude Code sessions live under `~/.claude/projects/` and Codex sessions, from both the CLI and the desktop app, live under `~/.codex/sessions/`. When you pass an id, the search spans both trees, and a unique prefix is accepted in place of the full id. The stated failure policy is deliberately forgiving: when an agent writes a record the tool has not seen, the unfamiliar record is skipped and is never fatal. For a viewer pointed at a live transcript that a freshly updated agent build is appending to, that is the right default, since format churn should not kill the display. The same policy has a cost the documentation does not price. A file in an unexpected shape produces a partial graph rather than an error, and no count of skipped records is surfaced, nor is there any stated behaviour for a prefix that matches more than one session.
The input is a transcript, and a transcript is your prompts and tool output
Read-only, with no network at all, is the project's own summary of what happens to your sessions, and the WebAssembly build is described as staying local in the browser too. That claim is worth weighing against what a graph of a session actually renders. Each agent card carries status, current tool, tool count and output tokens. Opening any agent gives its provenance: the prompt that spawned it, the reasoning around it, its model, and every tool call with timings. A session overlay adds mode, permissions, queued operations, file edits and the last prompt. Tool calls surface as chips under their agent, showing a repeat count or ticking through the duration of a single call, resolving to a check mark or a cross when it settles. So the input is a fairly complete record of the session, not a status line. The two integration paths take that input differently: the browser route takes a file dropped onto a page, and the terminal route installs a plugin that runs a setup action.
herdr integration install claude # and/or codex, so Herdr learns session ids
herdr plugin install furkankly/zoetrope/herdr-plugin
herdr plugin action invoke setup-keys --plugin furkankly.zoetropeThree install routes with three different toolchain requirements
Homebrew covers macOS and Linux with one tap. Cargo needs a Rust toolchain first, and the manifest sets `rust-version = "1.88"`, described as the floor inherited from ratatui 0.30 that nothing in the crate's own source needs to exceed, with an `msrv` job in CI verifying it. Prebuilt archives skip the toolchain altogether and cover macOS on both Apple Silicon and Intel, Linux as musl for arm64 and x86_64, and Windows on x86_64, so unpacking one and putting `zoe` on the path is the only route with no prerequisites. Every route lands on the same command name, `zoe`. On the release side, v0.1.0 shipped 2026-08-18 and v0.2.0 shipped 2026-09-10, and the last push on the default branch is 2026-09-15, so the second release landed less than a month after the first. The manifest reads 0.2.0, matching the newest tag exactly. At the root you also find `release-plz.toml` alongside `cliff.toml` and `committed.toml`, which is the shape of a pipeline where the version bump and the changelog are generated from commit messages rather than typed by hand, plus a `benches/` directory and a `typos.toml`. Nothing in that arrangement amounts to a stability promise at a version below 1.0.
Editorial conclusion
Zoetrope earns its place for anyone who has lost track of which subagent ran which command, and its read-only, no-network design means pointing it at a live session carries no write risk. Before building habits around the replay flags, check which start form your workflow actually uses, since speed and follow are documented only against a file argument and the promise of identical controls across every start form is not spelled out. Pin a version while it is pre-1.0, and remember that what you feed it is a full record of your prompts, file paths and tool output rather than a status line, so confirm the browser build really keeps a dropped transcript local before you use that route.
Frequently asked questions
Where does Zoetrope look for Claude Code and Codex sessions?
Claude Code sessions live in `~/.claude/projects/` and Codex sessions, from both the CLI and the desktop app, live in `~/.codex/sessions/`. Zoetrope tells the two formats apart by content rather than by path, and it searches both trees when you pass a session id.
Can Zoetrope print anything a script can parse?
The only documented headless path is `zoe inspect <file|id>`, which prints the session tree and exits, working anywhere without a TTY. No JSON or other machine-readable output mode is described.
Does Zoetrope need the network, and what does the Herdr integration install?
Zoetrope is described as read-only with no network at all, and the browser build as staying local there too. The terminal side is separate: it installs the plugin from `herdr-plugin/` and invokes a `setup-keys` action, after Herdr itself learns session ids through its own integration install.
What does it take to build the Zoetrope browser app from source?
`Cargo.toml` excludes `web/wasm` from the workspace so the wasm-only frontend is not host-checked, which gives it its own lockfile and target directory. `web/scripts/build-wasm.sh` builds it with trunk; `cargo build --release` produces only the terminal binary.
Which Rust version does Zoetrope require for a cargo install?
The manifest sets `rust-version = "1.88"`, described as the floor inherited from ratatui 0.30 and checked by an `msrv` job in CI. The Homebrew and prebuilt binary routes need no Rust toolchain at all.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/furkankly-zoetrope)