recensa

agent
Security Audit
Fail
Health Pass
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 51 GitHub stars
Code Fail
  • child_process — Shell command execution capability in backend/scripts/check.js
  • execSync — Synchronous shell command execution in backend/scripts/check.js
  • fs module — File system access in backend/scripts/check.js
  • child_process — Shell command execution capability in backend/scripts/ops.js
  • exec() — Shell command execution in backend/scripts/ops.js
  • spawnSync — Synchronous process spawning in backend/scripts/ops.js
  • process.env — Environment variable access in backend/scripts/ops.js
  • fs module — File system access in backend/scripts/ops.js
  • network request — Outbound network request in backend/scripts/ops.js
  • exec() — Shell command execution in backend/src/adapters/intel.js
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Self-hosted web viewer for Claude Code session transcripts — read, search, replay, and audit every session you have ever run

README.md

Recensa

Recensa

See what your agents actually did.
A self-hosted viewer for your Claude Code session transcripts — read, search, replay, and audit every session you have ever run. Your transcripts stay on your machine; nothing is uploaded anywhere.

License: MIT Node >= 22 Self-hosted CI status Docker Hub

Quick start · Tour · Why · Themes · Security · Architecture · Contributing

Every Claude Code session you run is written to disk as raw JSONL under
~/.claude/projects/ — the full conversation, every tool call, the model's thinking,
every subagent it spawned. Then it just sits there, unreadable. Want to know how you
fixed that bug last month? What that 40-minute agent run actually changed? Which of
your prompts sent it down a dead end? Good luck grepping.

Recensa indexes those transcripts into a local SQLite database and serves a
three-pane reader at http://localhost:7788. Reading never modifies your
transcripts, and the index is a disposable cache you can rebuild at any time.

Recensa's three-pane reader: a session list on the left, a virtualized conversation stream in the middle, and a session info panel on the right

Quick start

You need Docker (Docker Desktop on Windows/macOS, Docker Engine on Linux) and a
~/.claude directory with some sessions in it.

Try it — run the prebuilt image straight from Docker Hub:

# macOS / Linux
docker run -d --name recensa -p 127.0.0.1:7788:7788 \
  -v "$HOME/.claude:/data/claude:z" \
  -v recensa-data:/app/backend/data \
  -e RECENSA_PROJECTS_DIR=/data/claude/projects \
  s40911120/recensa

On Linux, ~/.claude/projects is created owner-only (700/600) by the host CLI, so the
container needs a matching uid/gid to read it — it detects this on its own from the
mount, no flags required. The trailing :z relabels the mount for SELinux-enforcing
hosts (RHEL/Fedora/CentOS, Podman); it's a no-op everywhere else, including macOS and
Windows Docker Desktop, so it's safe to always include. Override the detected uid/gid
with -e PUID=... -e PGID=... if the mount's owner isn't the account you want.

# Windows — use PowerShell, not Git Bash (MSYS rewrites the /data/... paths)
docker run -d --name recensa -p 127.0.0.1:7788:7788 `
  -v "$env:USERPROFILE\.claude:/data/claude" `
  -v recensa-data:/app/backend/data `
  -e RECENSA_PROJECTS_DIR=/data/claude/projects `
  s40911120/recensa

Or run it from source with Compose:

git clone https://github.com/S40911120/recensa.git && cd recensa
cp .env.example .env             # set CLAUDE_HOME to your .claude path
docker compose up -d --build

Then open http://localhost:7788. The first index runs in a background worker and
can take minutes to tens of minutes depending on how many sessions you have — the
UI is usable while it runs, and progress shows in a floating pill in the corner.

A tour

Search all of it, not the last five. Claude Code's resume picker shows a handful
of recent sessions. Recensa's full-text search (SQLite FTS5, CJK-aware) covers every
session you have ever run — "how did I fix that S3 retry bug?" is one query away.

Full-text search across all sessions, with highlighted snippet matches

Follow the conversation, not the noise. One session can hold thousands of tool
calls. God-view folds tools, thinking, and system noise into landmark rows you can
expand on demand. Async tool results are paired back to their call with a shared
colored rail, so a tool_result landing twelve messages later still reads as one
step.

Tool calls and their asynchronous results linked by a shared colored rail, with the result row echoing the call summary

Or cut the noise down two ways. The Main toggle (top right) folds every tool
call and thinking block away, leaving only your prompts and the model's replies.
Focus mode (the tab at the bottom) hides the app chrome itself — panels, header,
breadcrumb — for full-width, distraction-free reading. Esc exits.

Focus mode: the side panels and header are hidden, leaving the full-width transcript with the Focus On tab at the bottom

See exactly what changed. Every Edit in a session renders as a real diff —
before in red, after in green, per-file, with timestamps. Audit an agent's changes
without leaving the reader.

Per-file edit history rendered as unified diffs with red before and green after blocks

Know what a session cost. Per-session statistics break down tool usage, files
created / edited / deleted, model mix, token flow, and an estimated dollar cost.

Session statistics: tool distribution donut and bars, file change counts, model usage ranking, token totals and estimated cost

Audit multi-agent runs. When a session spawns subagents or teammates, Recensa
rebuilds the roster from the transcripts: every agent with its type, message count,
and model in the info panel, a crosshair that jumps to the exact call site where it
was spawned, and an "Open subagent transcript" deep-link into the agent's own
conversation — and back.

A multi-agent review session: the info panel lists four teammates and two subagents, each with agent type, message count, model, and a jump-to-call-site control; the conversation shows deep-link buttons into each spawned agent's transcript

Navigate compaction like it never happened. When Claude Code compacts a long
session, the pre-compaction text is gone from the working context — but not from the
transcript chain. Recensa marks every compaction point in the stream, lists them in
a Compaction History panel so you can jump straight to any boundary, tracks your
read position within the current compaction segment, and can reconstruct the full
pre-compaction conversation, merged across the fork/resume chain, as a read-only
view.

A twice-compacted session: a compaction-point landmark in the stream, the Compaction History panel with jump links to both boundaries, a Reconstruct-full-conversation button, and the breadcrumb showing the read position within the current compaction segment

The reconstructed view: the full pre-compaction conversation merged along the fork/resume chain, presented as a read-only snapshot

Keep your reading state. Pin, tag, rename, and annotate sessions. Your curation
lives in its own database (flags.db) that index rebuilds never touch. Live
sessions update in real time over WebSocket while Claude Code is still writing them.

The reader in GitHub Light: pinned and tagged sessions in the list, notes and stats in the info panel

Reads fine on a phone. The layout is responsive: on narrow screens the session
list and the info panel become swipe-in drawers around the conversation, so checking
what an agent did from the couch actually works.

Recensa on a phone: the session list drawer on the left, the conversation in the middle, and the info panel drawer on the right

Everything else

The smaller things you notice after a day of use:

  • Advanced search filters — narrow a query by session-id prefix, model, or a
    date window; filter the session list by project or to pinned-only.
  • Copy the resume command — one button in the info panel yields the exact
    claude --resume invocation for the session, ready to paste into a terminal.
  • Rich tool rendering — TodoWrite checklists, AskUserQuestion choices, and Bash
    output render as structured blocks, not JSON dumps; Markdown and syntax
    highlighting throughout.
  • Position that survives navigation — jumping into a subagent and back restores
    your exact scroll position; every session remembers how far you have read.
  • Keyboard-friendlyj/k walk messages, / focuses search, g/Shift+G
    jump to the ends, ? shows the full palette; skip-links cover the reader and
    info panel.
  • Copy anything — per-message and per-code-block copy buttons.
  • Custom titles, reading font size — rename cryptic sessions; tune the reader.
  • Indexing that stays out of the way — first index runs in a worker with a
    progress pill; the UI stays responsive while it fills in.

Why Recensa

Claude Code gives you /context, /resume, and /compact; everything behind them
stays a black box — the raw JSONL, the token breakdown, the tool-call chains, the
point where a run looped or failed. A real run also spawns a team of subagents, and
none of that is visible in Claude Code or Claude Desktop. Recensa rebuilds the full
picture from the transcripts already on your disk:

  • Raw JSONL is unreadable. Recensa renders it as a conversation you can follow.
  • The resume picker only shows the last few. Full-text search covers all of them.
  • Noise buries the signal. God-view folds thousands of tool calls into landmark
    rows and keeps the main line readable.
  • You can't tell what a run cost. Stats break down tools, files, models, tokens.
  • Fork and resume chains fragment history. Recensa reconstructs the full
    pre-compaction conversation across merged transcript files.
  • One bad structural edit can brick a transcript (a permanent API 400). Recensa
    reads without side effects; structural repair is a separate, guarded tool
    (@recensa/claude-session).

Non-goals

  • Not an editor. It never rewrites transcript content. The only write path to your
    transcripts is the explicit, guarded session delete described in
    Security and your data.
  • Not a cloud service. No accounts, no telemetry, no external network calls.
  • Not a Claude Code replacement. It reads the transcripts Claude Code already
    writes to disk.
  • Not a general log viewer. Built for Claude Code session JSONL, not arbitrary logs.

Themes

Fourteen themes — four light, ten dark — as color-scheme homages to classic editor
palettes: Dracula, Tokyo Night, Nord, Gruvbox, One Dark Pro, Monokai, Catppuccin
(Latte and Mocha), Solarized (Light and Dark), Rosé Pine, GitHub (Light and Dark),
and the warm-light Classic. The active theme is injected server-side into the HTML,
so there is no flash of the wrong theme on load.

Tokyo Night Dracula Nord
Tokyo Night Dracula Nord
Catppuccin Mocha Solarized Light GitHub Light
Catppuccin Mocha Solarized Light GitHub Light

Troubleshooting

The reader loads but the session list is empty, or the logs show an empty mount.
CLAUDE_HOME did not resolve to your .claude directory. On Windows, Compose's
${HOME} fallback is usually unset under PowerShell and cmd, so the bind mount comes
up empty. Set CLAUDE_HOME explicitly in .env to an absolute path with forward
slashes (C:/Users/you/.claude), then rebuild with node backend/scripts/ops.js up.
If you used the docker run one-liner from Git Bash instead of PowerShell, MSYS path
rewriting is the likely culprit — rerun it from PowerShell.

docker compose up fails with "port is already allocated", or 7788 never
responds.
Something else on the host already holds 7788. Publish on a free port
by setting RECENSA_HOST_PORT in .env, then rebuild:

RECENSA_HOST_PORT=7799

The container always listens on 7788 internally; only the host-side port changes,
so open http://localhost:7799. Running from source instead of Docker? Set
SESSION_VIEWER_PORT — that is the port the server process itself binds.

Custom hostname (optional). To reach it at http://sessions.local:7788, add a
line to your hosts file (as administrator): 127.0.0.1 sessions.local.

Operations

All operations go through one entry point:

node backend/scripts/ops.js <command>
Command What it does
up Build the image and start the container (docker compose up -d --build)
down Stop and remove the container
restart Restart the running container
logs Follow container logs
status Container state and health
smoke End-to-end smoke test (health / list / paging / host allowlist)
check Type-check, lint, and unit tests, backend and frontend
backfill-chain Recompute derived columns after a schema change (runs in the container)
vacuum Shrink the DB file after large deletes or an FTS migration (runs in the container)

Architecture

Recensa architecture: the host's ~/.claude/projects JSONL, read-only bind-mounted into a single Docker container — ingestion (a live-tail watcher and an incremental index worker) into a JSONL parser, SQLite storage (viewer.db rebuildable cache plus flags.db user data), a Node.js/Express backend (repositories, services, routes behind a Host/Origin allowlist) that delegates to the @recensa/claude-session npm engine, and a React frontend served to the browser over HTTP and WebSocket

Two databases are kept deliberately separate. viewer.db is fully derived from the
JSONL and can be deleted and rebuilt at any time. flags.db holds your pins, tags,
and notes, which cannot be recomputed, so index rebuilds never touch it.

Session-JSONL parsing, verification, and reconstruction are delegated to
@recensa/claude-session, a
zero-dependency npm package that also works standalone as a CLI.

Data integrity is checked mechanically. An independent recompute of msg_count,
tokens, tool calls, and the model list against the raw JSONL matches the index across
sampled sessions, and the msg_count alignment invariant is locked by unit tests.

Configuration

Set these two in .env before the first run (Compose path):

Env var Default Meaning
CLAUDE_HOME (required) Absolute host path to your .claude directory. Its projects/ subdirectory holds the session JSONL. Bind-mounted into the container. On Windows use forward slashes (C:/Users/you/.claude).
RECENSA_HOST_PORT 7788 Host port to publish. Override only if 7788 is already taken. The container always listens on 7788 internally.

Advanced overrides (for running from source or remote self-hosting):

Env var Default Meaning
SESSION_VIEWER_PORT 7788 HTTP/WS port the server listens on.
SESSION_VIEWER_HOST 127.0.0.1 Bind address. The container sets 0.0.0.0 so the published loopback port can reach it.
RECENSA_PROJECTS_DIR ~/.claude/projects Root of the session JSONL tree (shared with the recensa-session CLI). The container sets /data/claude/projects.
RECENSA_ALLOWED_HOSTS (none) Extra comma-separated hostnames added to the Host/Origin allowlist. Only relevant behind your own reverse proxy (see Security).
RECENSA_WATCH_MODE (auto) Watch strategy: events (native fs events) or tick (polling). Auto-selects tick inside Docker, where bind-mount fs events are unreliable.

Security and your data

Recensa is built for single-machine, local use. It ships no authentication.

  • Loopback bind is the boundary. By default the server binds 127.0.0.1 and Docker
    Compose publishes the port to 127.0.0.1 only. On one machine, that loopback bind is
    what actually keeps the service private.
  • The Host/Origin allowlist does two jobs, and neither is a login. It is primarily
    DNS-rebinding defense: it stops a malicious web page from using your browser to reach
    the loopback server. It additionally gates cross-origin writes (next bullet). It is
    still not access control: a non-browser client that can reach the port can forge the
    Host and Origin headers and walk past it.
  • Mutating requests require an allowlisted Origin (CSRF write-gate). Every
    state-changing method (POST/PUT/DELETE/PATCH) must send an Origin header that is
    present and on the allowlist; GET/HEAD keep it optional. A browser always attaches
    Origin to a same-origin mutating fetch and cannot forge it cross-site, so another
    site open in your browser cannot drive writes against your local server. This closes
    the cross-origin write path; it does not replace auth.
  • Do not expose Recensa on 0.0.0.0 or a public interface without your own auth and a
    reverse proxy (TLS) in front of it.
    Adding names via RECENSA_ALLOWED_HOSTS widens
    the rebinding allowlist; it does not add access control.

Your transcripts:

  • Reading never writes. The indexer, parser, and reconstruction paths only read your
    JSONL.
  • One write path exists: deleting a session. DELETE /api/session/:id physically
    removes the session's JSONL file and its subagents directory from disk. This is
    irreversible. It is guarded: pinned sessions, sessions that belong to a fork/resume
    chain, and individual subagents are all refused. Nothing else writes to your
    transcripts.
  • Your curation is safe. Pins, tags, notes, and titles live in a separate flags.db
    that index rebuilds never touch. The viewer.db index is a disposable cache.

No secrets, no telemetry, no external network calls.

Uninstall

Your pins, tags, notes, and titles live in flags.db, inside the Docker named volume
recensa-data. That data is not derived from the transcripts and cannot be
recomputed
, so back it up before removing anything you want to keep:

# with the container still present, copy flags.db out of the volume
docker cp recensa:/app/backend/data/flags.db ./flags.db.backup

Then stop and remove the container, and optionally the image:

node backend/scripts/ops.js down        # stop and remove the container
docker image rm s40911120/recensa       # optional: drop the pulled image

down leaves the recensa-data volume in place, so a later rebuild keeps your
curation. docker compose down -v also deletes that volume — which destroys
flags.db permanently.
The viewer.db index in the same volume is a disposable
cache that rebuilds from your JSONL, so losing it only costs the next reindex. Your
transcripts under ~/.claude are never touched by uninstalling.

Contributing

Contributions are welcome. To get a dev environment running:

docker compose up -d --build          # build and start the container
node backend/scripts/ops.js check     # type-check, lint, and unit tests (must pass)

The container serves the built frontend and runs the server; rebuild with
node backend/scripts/ops.js up after changes. Keep pull requests focused, follow the
existing Conventional Commits style, and make
sure ops.js check passes before opening a PR. See CONTRIBUTING.md
for details.

License

MIT. See LICENSE. Author: Justin Chen.

Reviews (0)

No results found