claude-cast
v0.1.0
Published
Share Claude Code sessions via GitHub Gist
Maintainers
Readme
claude-cast
Turn Claude Code sessions into shareable HTML or Markdown — then optionally upload to GitHub Gist.
Claude Code stores every conversation as a .jsonl file, but those files are messy internal logs full of streaming fragments, progress updates, and system bookkeeping. claude-cast parses them, throws out the noise, merges the streaming duplicates, and produces a clean readable document.
Quick Start
# List your recent sessions
npx claude-cast --list
# Export the latest session as HTML
npx claude-cast latest -o session.html
# Export by session name
npx claude-cast greedy-squishing-spring -o session.html
# Interactive picker (arrow keys to select)
npx claude-cast
# Upload to GitHub Gist
npx claude-cast latest -f md --gistInstallation
# Run directly (no install)
npx claude-cast
# Or install globally
npm install -g claude-cast
claude-cast --listCLI Reference
Usage: claude-cast [options] [session]
Arguments:
session Session UUID, slug, or 'latest'Session Selection
| Command | Description |
|---|---|
| claude-cast | Interactive picker — browse recent sessions |
| claude-cast latest | Most recently modified session |
| claude-cast <slug> | Find by slug name (e.g. greedy-squishing-spring) |
| claude-cast <uuid> | Find by session UUID |
| claude-cast --list | List 20 recent sessions with previews |
Output Options
| Flag | Description | Default |
|---|---|---|
| -f, --format <format> | Output format: html or md | html |
| -o, --output <path> | Write to file instead of stdout | stdout |
| --dry-run | Force output to stdout | — |
Content Options
| Flag | Description | Default |
|---|---|---|
| --include-thinking | Include Claude's extended thinking blocks | excluded |
| --no-tool-results | Hide tool call outputs | included |
Gist Upload
| Flag | Description |
|---|---|
| --gist | Upload to GitHub Gist (secret by default) |
| --gist-public | Make the gist publicly visible |
| --token <token> | GitHub personal access token |
Examples
# HTML file with thinking blocks visible
claude-cast latest --include-thinking -o session.html
# Markdown without tool outputs (clean reading)
claude-cast latest -f md --no-tool-results -o session.md
# Upload specific session to public gist
claude-cast greedy-squishing-spring -f md --gist --gist-public
# Pipe HTML to clipboard (macOS)
claude-cast latest | pbcopyGist Setup
claude-cast resolves GitHub tokens in this order:
--tokenflag —claude-cast latest --gist --token ghp_xxxGITHUB_TOKENenv var —export GITHUB_TOKEN=ghp_xxxGH_TOKENenv var — same as above, alternate nameghCLI — if you have GitHub CLI installed and authenticated
Getting a Token
Option A: GitHub CLI (recommended)
brew install gh
gh auth login
# Done — claude-cast will pick up your auth automaticallyOption B: Personal Access Token
- Go to github.com/settings/tokens
- Generate new token (classic)
- Check only the
gistscope - Copy the
ghp_...token
# Use inline
claude-cast latest -f md --gist --token ghp_xxx
# Or set as env var
export GITHUB_TOKEN=ghp_xxx
claude-cast latest -f md --gistOutput Formats
HTML
- Self-contained single file — all CSS inlined, no external dependencies
- Automatic dark/light theme (follows your OS setting)
- Chat-style layout with colored borders: blue (you), green (Claude), purple (tool calls)
- Collapsible tool calls and thinking blocks
- Best for: sharing via file, opening in browser
Markdown
- GitHub-flavored Markdown with
<details>blocks for tools/thinking - Metadata table at the top (date, model, project, turns)
- Auto-splits into multiple files if content exceeds 900KB (GitHub Gist limit)
- Best for: uploading to Gist (renders natively on GitHub)
Smart Content Detection
claude-cast doesn't just dump raw tool data — it detects patterns in the conversation and renders them appropriately:
| Pattern | Raw Data | Rendered As |
|---|---|---|
| User Q&A answers | "question"="answer" pairs buried in tool result | Visible highlighted Q&A block |
| Agent/Task results | Thousands of lines of agent output | Full formatted markdown (tables, code, headings) |
| Plan files | Write tool JSON with .md content | Rendered markdown document, open by default |
| Plan rejections | "The user doesn't want to proceed..." tool error | Clean "Plan was rejected" notice |
| Interrupted messages | [Request interrupted by user for tool use] | Filtered out entirely |
| Agent metadata | <usage> tags, agentId: lines | Stripped from output |
Architecture
The Problem
A Claude Code session file is a .jsonl log at ~/.claude/projects/<escaped-path>/<uuid>.jsonl. A typical session with 7 conversation exchanges produces 159 JSONL lines:
| Record type | Count | What it is |
|---|---|---|
| progress | 115 | Streaming progress updates |
| assistant | 22 | Claude's responses (fragmented by streaming) |
| user | 12 | Your messages + tool results + system injections |
| file-history-snapshot | 4 | Internal file state tracking |
| system | 4 | System configuration |
| queue-operation | 2 | Internal queue management |
72% is streaming noise. The 22 assistant records are really 7 responses — each written as ~3 streaming snapshots. And several "user" records are actually system-injected messages, not things you typed.
Pipeline
JSONL file
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Locate │ ──▶ │ Filter │ ──▶ │ Dedup │ ──▶ │ Tree │ ──▶ │ Render │
│ │ │ │ │ │ │ │ │ HTML/MD │
│ find the │ │ drop 72% │ │ 22 recs │ │ attach │ │ │
│ file │ │ noise │ │ → 7 turns│ │ tool │ │ │
└──────────┘ └──────────┘ └──────────┘ │ results │ └──────────┘
└──────────┘Project Structure
claude-cast/
├── bin/
│ └── cli.ts # CLI entry point (commander)
└── src/
├── types.ts # All TypeScript interfaces
├── utils.ts # Shared helpers
├── picker.ts # Interactive session selector (inquirer)
├── gist.ts # GitHub Gist upload (gh CLI + REST API)
├── parser/
│ ├── index.ts # parseSession() orchestrator
│ ├── locate.ts # Find sessions by UUID/slug/latest
│ ├── filter.ts # Remove noise records
│ ├── dedup.ts # Merge streaming fragments
│ └── tree.ts # Build conversation turns
└── renderers/
├── markdown.ts # Markdown output with byte-aware splitting
├── html.ts # HTML renderer (uses marked)
└── html-template.ts # Self-contained CSS + HTML shellModule Walkthrough
Types (src/types.ts)
Two layers of data shapes:
Raw layer — what Claude Code writes to disk:
RawRecord— every line in the JSONL. Hastype("user","assistant","progress", ...),uuid,parentUuid(linking records into a chain),sessionId,requestId, and amessageobject.RawMessage— themessageinside a record.contentis either a plain string (simple user messages) or an array ofRawContentItem(structured content with tool calls, thinking blocks, etc).
Normalized layer — what the parser produces:
ContentItem— discriminated union:text | thinking | tool_use | tool_resultConversationTurn— one message bubble. Role + timestamp + array of ContentItems.ParsedSession— the final product:{ meta: SessionMeta, turns: ConversationTurn[] }
Locator (src/parser/locate.ts)
Sessions are scattered across ~/.claude/projects/*/. Each project directory is a path with slashes replaced by dashes (e.g. /Users/faisal/Desktop/projects → -Users-faisal-Desktop-projects).
findSessionByUuid()— matches against filenames (fast, no file reading)findSessionBySlug()— reads the first user record of each file to check itsslugfield (slower, opens every file)findLatestSession()— checks filesystemmtimeof every.jsonllistRecentSessions()— sorts by mtime, reads metadata from top N files
All file reading uses Node's readline with streams — reads only the first few lines, then closes. This matters when you have dozens of multi-MB session files.
Filter (src/parser/filter.ts)
Three rules:
- Drop noise types —
progress,system,file-history-snapshot,queue-operation, etc. - Drop sidechain records —
isSidechain: true(branching explorations) - Drop system user messages — messages starting with
<system-reminder>,<command-name>,<local-command-caveat>, etc.
Dedup (src/parser/dedup.ts)
The most technically interesting module. When Claude responds, Claude Code writes multiple JSONL records for the same response — they share a requestId but each contains a partial content snapshot at that moment of streaming:
Record 1 (requestId=req_abc): content=[{type:"text", text:"\n\n"}]
Record 2 (requestId=req_abc): content=[{type:"thinking", thinking:"Let me..."}]
Record 3 (requestId=req_abc): content=[{type:"text", text:"Here's the answer..."}]deduplicateAssistants():
- Groups assistant records by
requestId - Sorts each group by timestamp
- Collects all content items in order
- Deduplicates
tool_useby ID (streaming can repeat the same tool call) - Coalesces adjacent text blocks split by streaming
Result: one merged response per actual Claude reply.
Tree Builder (src/parser/tree.ts)
Handles the tricky part: tool result attachment.
In the raw data, when Claude calls a tool (like Read or Bash), the result comes back as a "user" record — but it's not something you typed. The tree builder:
- Indexes all
tool_useitems by their ID - Finds
tool_resultrecords in user messages - Splices each result right after its matching tool call in the assistant turn
- Drops user records that contain only tool results (they'd be empty bubbles)
- Merges all turns and sorts by timestamp
Renderers
Both take a ParsedSession and produce a string.
Markdown (src/renderers/markdown.ts): Uses <details> blocks for tool calls and thinking. Tracks byte count and splits at turn boundaries if a file would exceed 900KB (GitHub Gist limit).
HTML (src/renderers/html.ts + html-template.ts): Self-contained file with all CSS inlined. Uses the CSS light-dark() function for automatic dark/light theming — no JavaScript. Assistant markdown is converted to HTML via the marked library.
Both renderers use content-aware detection — they inspect tool names and result patterns to decide how to render each item (see Smart Content Detection above).
Gist Upload (src/gist.ts)
Dual-path with automatic fallback:
ghCLI — if installed and authenticated, writes to temp files and runsgh gist create- REST API —
POST https://api.github.com/gistswith token from flag/env/gh-auth
Data Flow Example
Running claude-cast greedy-squishing-spring -o output.html:
1. CLI parses args
→ session="greedy-squishing-spring", format="html", output="output.html"
2. findSessionBySlug("greedy-squishing-spring")
→ Scans ~/.claude/projects/*/
→ Reads first user record of each .jsonl
→ Match found: 3ba41ea0-b987-4731-9dec-e8e1412eb264.jsonl
3. parseSession(filePath)
→ Read 159 lines → RawRecord[]
→ Filter: 159 → 34 records (dropped noise + system messages)
→ Dedup: 22 assistant records → 7 merged responses
→ Tree: 12 conversation turns (5 user + 7 assistant)
→ Meta: {slug, model: "claude-opus-4-6", turnCount: 12, ...}
4. renderHtml(session, options)
→ Header with metadata pills
→ Each turn → avatar + content card
→ Assistant text → marked.parse() → HTML
→ Tool calls → collapsible <details>
→ Wrap in htmlShell() with full inline CSS
5. Write to output.html → 42KB self-contained HTML fileLicense
MIT
