claude-code-chat-browser reads JSONL session files written by Claude Code under ~/.claude/projects/ and serves them through a JSON HTTP API to a single-page web UI, with a parallel CLI export tool. The app is read-only toward ~/.claude/ — it never writes session data back to that tree.
┌─────────────────────────────┐
│ ~/.claude/projects/ │
│ <project>/*.jsonl │ (read-only data source)
└──────────────┬────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ session_path │ │ jsonl_parser │ │ exclusion_rules │
│ list_projects │ │ parse_session │ │ load + match │
│ list_sessions │ │ quick_session_info │ └────────┬─────────┘
│ safe_join │ │ _parse_tool_result │ │
└────────┬────────┘ └──────────┬──────────┘ │
│ │ │
└────────────┬───────────┴────────────────────────┘
│
▼
┌────────────────────────────┐
│ api/ │
│ projects · sessions │
│ search · export_api │
│ error_codes │
└─────────────┬──────────────┘
│ Flask blueprints
▼
┌────────────────────────────┐
│ app.py — create_app() │
└─────────────┬──────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ static/ │ │ scripts/export.py │
│ index.html + js │ │ (CLI, uses utils) │
└──────────────────┘ └──────────────────┘
| Layer | Responsibility | Key modules |
|---|---|---|
| Data discovery | Resolve ~/.claude/projects/, list projects and sessions, prevent path traversal |
utils/session_path.py |
| Parsing | JSONL → session dict (messages, metadata, tool rendering) | utils/jsonl_parser.py |
| Filtering | Exclude sensitive sessions via rules file | utils/exclusion_rules.py |
| Statistics | Aggregates for API and exporters | utils/session_stats.py |
| Export — Markdown | Session → YAML-frontmatter Markdown | utils/md_exporter.py |
| Export — JSON | Session → JSON string for download | utils/json_exporter.py |
| Export — state | Incremental export checkpoints on disk | utils/export_state_store.py, api/export_api.py |
| HTTP | Routes, validation, error envelope | api/*.py, api/error_codes.py |
| App factory | Blueprint registration, rules loading, SPA static route | app.py |
| Frontend | Hash-routed UI, markdown render, shared state | static/index.html, static/js/ |
| CLI | Same export semantics as bulk API, no HTTP | scripts/export.py |
- Browser loads
GET /→static/index.html. - SPA calls
GET /api/projects→list_projects()+quick_session_info()for titled counts. - User opens a project →
GET /api/projects/<name>/sessions→ fullparse_session()per file, exclusion filter, summary rows. - User opens a session →
GET /api/sessions/<project>/<id>→ full session JSON for the message panel. - Optional:
GET /api/sessions/.../statsfor sidebar metrics without loading all messages. - Search:
GET /api/search?q=...scans all projects (brute force). - Export:
POST /api/exportorGET /api/export/session/...→ Markdown/zip via exporters; state file updated on successful bulk export.
In utils/jsonl_parser.py, tool results are classified through _parse_tool_result, a predicate-ordered dispatch table (not a simple if tool_name == ... chain). Order is load-bearing: the first matching predicate wins. Tests in tests/test_jsonl_parser.py guard ordering regressions.
When adding a new tool renderer:
- Add predicate + builder pair in the dispatch table in the correct order (specific before generic).
- Add or extend a JSONL fixture under
tests/fixtures/if needed. - Run
pytest tests/test_jsonl_parser.py -v.
Bulk export (POST /api/export) is stateful. State lives in ~/.claude-code-chat-browser/export-state.json (see EXPORT_STATE_FILE in utils/export_state_store.py).
since mode |
Behavior |
|---|---|
all |
Export all eligible sessions; update per-session mtimes in state |
last |
Export sessions active on the latest UTC calendar day in history |
incremental |
Export only sessions newer than last recorded mtime per id |
Writes are atomic (temp file + os.replace) under a lock from _state_lock().
If zero sessions match, the API returns 422 with EXPORT_NOTHING_TO_EXPORT and echoes since — not an empty zip.
GET /api/export/state reads the same file without mutating it.
At startup, create_app() loads rules from --exclude-rules or the default path into app.config["EXCLUSION_RULES"]. is_session_excluded() is applied on list, detail, search, and export paths so filtered sessions never appear in the UI or downloads.
The UI is a hash-routed SPA with ES modules under static/js/:
app.js— routing and bootprojects.js,sessions.js,search.js,export.js— route handlersshared/markdown.js— markdown + DOMPurify sanitization (do not render raw LLM HTML)shared/state.js,shared/utils.js,shared/theme.js— shared UI state and helpers
No bundler step — modern browsers load modules directly. Frontend unit tests use vitest + jsdom (npm test).
.github/workflows/ci.yml runs on push/PR:
prod-install-smoke— productionrequirements.txtboots the apppytest— full Python suite with coverage gate onapi/+utils/integration-tests— API integration subset + coverage artifactjs-tests—npm ci+ vitest
- Not multi-user — no authn/authz; single local operator.
- Not a writeback tool — never modifies
~/.claude/. - Not a search engine —
/api/searchis O(sessions × messages); fine for personal history, not for large multi-tenant indexes. - Not a versioned public API — no semver or OpenAPI contract yet; see
docs/api-reference.mdas the human contract.