An audit viewer for Claude Code. It answers a single question, reliably: which files did Claude touch, in which repo, with what diff, and in which session? — without opening an IDE, without AI chat, and without depending on git or hooks.
Status: Phase 2 complete + polish — a desktop app (Tauri 2.x) with the Rust parser as its native backend, already usable day to day on Linux (
.deb+ AppImage). Phases 0 (parser/CLI), 1 (web UI) and 2 (packaging) are complete; after Phase 2 we added 9 parser tests, a resilient watcher, zoom, a custom titlebar, active-session focus, and macOS adaptation. Phases 3 (honesty + git) and 4 (editing) are pending — details in ROADMAP.md.
When Claude Code becomes the one doing most of the changes, your job shifts to steering and reviewing: the IDE stops being where you write and is relegated to "show me what it touched." But keeping an IDE — or a heavy Electron-style Git client — open just to review diffs wastes anywhere from hundreds of MB to several GB of RAM. arrow does exactly that one part — seeing what Claude changed, repo by repo and session by session — in a lightweight, focused app.
And while the tooling space around Claude Code is crowded, nobody covers exactly this: a graphical
UI, no chat, with the repository → touched files → diff/editor hierarchy as a navigable audit
trail. The closest options are a terminal TUI (claude-file-recovery), a web chat client
(claude-code-viewer), or large GUIs oriented toward running agents (opcode, AGPL). Anthropic
closed the "recoverable edit history" feature request (#36542) as not planned — so the gap is
real, even if the margin is narrow.
There is no need to install a session-log hook. Claude Code already persists everything
needed, natively and in a structured form (verified on Claude Code v2.1.x):
| Data | Native source |
|---|---|
| Repos | The cwd field of each record (the real path; we don't decode the directory name, which is ambiguous). |
| Sessions | Each ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. The file name is the sessionId. |
| Touched files | type:"user" records with toolUseResult.filePath (only Edit/Write/MultiEdit). |
| Diff (before/after) | toolUseResult.structuredPatch — the exact hunks {oldStart, oldLines, newStart, newLines, lines}. This is already the per-session diff. |
| "Before" / point-in-time | ~/.claude/file-history/<sessionId>/<hash>@v<n> — snapshots of the prior content. |
git diff is kept as an optional secondary view (the full working tree), and only when the repo
is a git repo: it does not attribute by author or by session, and many repos aren't git at all.
Edit/Write/MultiEdit is captured. Changes made via the session's
Bash commands (sed, prettier, build, mv, rm) do not appear. The correct label is always
"edits via Claude's tools", never "everything Claude did".userModified flag signals drift (the user edited between the read and the write): it's
marked with ⚠.cleanupPeriodDays). Hence the defensive parsing: an invalid line is skipped, never
crashing.cargo build --release
# Summary: all repos -> sessions -> files (+/-)
./target/release/arrow --list
# Full diff of a repo (filters by a substring of the cwd)
./target/release/arrow --repo my-project
# A specific session (prefix of the sessionId)
./target/release/arrow --session 14385fed
# Normalized JSON (the contract the UI consumes)
./target/release/arrow --repo my-project --json
Options: --projects-dir <path> (defaults to ~/.claude/projects), --repo, --session,
--list, --json, and --content --file <path> [--session <id>] (emits {before, after} for a
file, for the UI's diff view).
A Vite + Svelte 5 app that consumes the parser through a local dev-server which runs the arrow
binary (in web/vite.config.ts). The diff view uses CodeMirror 6 + @codemirror/merge.
cargo build --release # the dev-server runs target/release/arrow
cd web && npm install
npm run dev # http://localhost:5173
Layout: a sidebar repo → session → touched files (with +/- and ⚠ userModified), and a central
panel with the before/after diff of the selected file. Total frontend weight: ~93 KB gzip.
The same UI, packaged in Tauri 2.x: the Rust parser is the native backend (no sidecar, no
HTTP server). The Svelte UI is reused as-is; only its transport layer (web/src/lib/api.ts) detects
the environment and uses Tauri invoke() inside the app or fetch in the browser — so npm run dev keeps working for fast iteration.
# system requirements (Linux/Debian/Ubuntu/Pop!_OS), once:
sudo apt install -y libwebkit2gtk-4.1-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev
cargo install tauri-cli --version "^2" # Tauri CLI
# development: native window with frontend hot-reload
cargo tauri dev # (from the repo root)
# installer: .deb + AppImage in src-tauri/target/release/bundle/
cargo tauri build
macOS: the app adapts to the OS on its own (native titlebar with traffic lights + correct font weight). To build the
.app/.dmg— which can only be done on a Mac — see MACOS.md.
src-tauri/): two invoke commands — report() and content(file, session)
— that wrap the parser library (arrow = { path = ".." }, see Architecture). The AppImage runs
standalone, reading ~/.claude/projects directly from Rust..deb).notify watcher over ~/.claude/projects emits a report-changed
event (debounced) and the UI refreshes instantly; a slow polling fallback is kept as a backstop.BURST_WINDOW
in web/src/lib/time.ts); the rest sink into Other repos as they age relative to the active
one. The green dot marks those focused repos with recent activity (the redundant live text
badge was removed). Honest: "active session" = most recent activity on disk, not a running
process (arrow cannot know the latter).decorations:false) with minimize/maximize/close buttons,
drag, and double-click to maximize — guaranteeing cross-distro controls (on GNOME/Pop!_OS the WM
doesn't paint them reliably). VSCode-style UI zoom with Ctrl +/−/0: native to the webview
(setZoom, so it doesn't throw off CodeMirror), persistent across sessions.WEBKIT_DISABLE_DMABUF_RENDERER=1 in its main() (avoids the
white screen caused by DMABUF/NVIDIA); the font-weight (+100) bug is already compensated in
web/src/app.css (font-weight: 350).The parser lives in a library (src/lib.rs): pure functions build_report(projects_dir) and
file_content(projects_dir, file, session) + the serializable structs. Two frontends consume it:
src/main.rs (the CLI, with flags intact) and src-tauri/ (the desktop backend). Zero duplicated
logic; the same source of truth for terminal, web, and native app. The parser ships 9 unit tests
(cargo test) over fixture transcripts in a tempdir, covering the non-obvious parts: defensive
parsing, top-level transcripts only, grouping by git root, +/− counting, filtering of
~/.claude/, and recency ordering.
JSONL → repo/session/file/diff, terminal output and --json.repo → session → files (Antigravity-style) + per-file diff with
CodeMirror 6 + @codemirror/merge. (Read-only; editing is Phase 4.).deb + AppImage). The Rust parser was extracted
into a library (src/lib.rs) and is the native backend (no sidecar); the CLI and the app
share it. Dual-mode frontend (invoke/fetch), a notify watcher → report-changed event
for live refresh, and WebKitGTK mitigation. (The sidebar focus was later refined to
"active session + ~10 min burst" with the green dot as the sole indicator; see ROADMAP.)userModified marking, a
point-in-time timeline reusing file-history.Operational tracking, technical debt, and a backlog of ideas (search, stats, export, shortcuts): see ROADMAP.md. The data contract lives in SPEC.md.
Rust (parser/backend) · CodeMirror 6 (UI, Phase 1+) · Tauri 2.x (packaging, Phase 2+).
On Linux/WebKitGTK you'll need to apply WEBKIT_DISABLE_DMABUF_RENDERER=1 and compensate for the
font-weight (+100) bug — both documented for Phase 2.
Contributions are welcome — bug reports, ideas, and pull requests alike. Please read CONTRIBUTING.md for how to build, test, and submit changes, and our Code of Conduct. To report a security issue, see our Security Policy. When in doubt, keep the product honest: arrow shows edits via Claude's tools, never everything Claude did.
MIT.