Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ All commands support these options:
- `--open` - open the generated `index.html` in your default browser (default if no `-o` specified)
- `--gist` - upload the generated HTML files to a GitHub Gist and output a preview URL
- `--json` - include the original session file in the output directory
- `--output-mode` - control how much detail is shown: `full` (default), `compact`, or `conversation`

The generated output includes:
- `index.html` - an index page with a timeline of prompts and commits
Expand Down Expand Up @@ -157,6 +158,33 @@ JSON: ./my-transcript/session_ABC.json (245.3 KB)

This is useful for archiving the source data alongside the HTML output.

### Filtering output

Use `--output-mode` to control how much detail is included in the generated HTML:

```bash
# Show everything (default)
claude-code-transcripts json session.json --output-mode full

# Hide tool results but keep tool call headers (useful for hiding sensitive output)
claude-code-transcripts json session.json --output-mode compact

# Show only user prompts and assistant text (no tools, no thinking)
claude-code-transcripts json session.json --output-mode conversation
```

The three modes are:

| Mode | User text | Assistant text | Tool calls | Tool results | Thinking |
|------|-----------|---------------|------------|-------------|----------|
| `full` | Yes | Yes | Yes | Yes | Yes |
| `compact` | Yes | Yes | Yes | No | Yes |
| `conversation` | Yes | Yes | No | No | No |

`compact` mode is useful when tool results might contain sensitive information (environment variables, file contents, etc.) but you still want to see what tools were invoked. `conversation` mode gives a clean "what did we discuss?" view.

This option works with all commands (`local`, `web`, `json`, `all`).

### Converting from JSON/JSONL files

Convert a specific session file directly:
Expand Down
126 changes: 112 additions & 14 deletions src/claude_code_transcripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import subprocess
import tempfile
import webbrowser
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

Expand Down Expand Up @@ -78,6 +79,41 @@ def extract_text_from_content(content):
# Module-level variable for GitHub repo (set by generate_html)
_github_repo = None


@dataclass
class OutputFilter:
"""Controls which content types are included in the rendered output.

Individual flags can be set directly, or use from_mode() for presets.
"""

hide_tool_calls: bool = False
hide_tool_results: bool = False
hide_thinking: bool = False

def should_hide(self, block_type: str) -> bool:
"""Check whether a content block type should be hidden."""
if block_type == "thinking":
return self.hide_thinking
if block_type == "tool_use":
return self.hide_tool_calls
if block_type == "tool_result":
return self.hide_tool_results
return False

@classmethod
def from_mode(cls, mode: str) -> "OutputFilter":
if mode == "compact":
return cls(hide_tool_results=True)
elif mode == "conversation":
return cls(hide_tool_calls=True, hide_tool_results=True, hide_thinking=True)
else: # "full"
return cls()


# Module-level variable for output filtering (set by generate_html)
_output_filter = OutputFilter()

# API constants
API_BASE_URL = "https://api.anthropic.com/v1"
ANTHROPIC_VERSION = "2023-06-01"
Expand Down Expand Up @@ -304,7 +340,11 @@ def find_all_sessions(folder, include_agents=False):


def generate_batch_html(
source_folder, output_dir, include_agents=False, progress_callback=None
source_folder,
output_dir,
include_agents=False,
progress_callback=None,
output_filter=None,
):
"""Generate HTML archive for all sessions in a Claude projects folder.

Expand All @@ -319,6 +359,7 @@ def generate_batch_html(
include_agents: Whether to include agent-* session files
progress_callback: Optional callback(project_name, session_name, current, total)
called after each session is processed
output_filter: Optional OutputFilter controlling content visibility

Returns statistics dict with total_projects, total_sessions, failed_sessions, output_dir.
"""
Expand Down Expand Up @@ -347,7 +388,9 @@ def generate_batch_html(

# Generate transcript HTML with error handling
try:
generate_html(session["path"], session_dir)
generate_html(
session["path"], session_dir, output_filter=output_filter
)
successful_sessions += 1
except Exception as e:
failed_sessions.append(
Expand Down Expand Up @@ -747,6 +790,8 @@ def render_content_block(block):
if not isinstance(block, dict):
return f"<p>{html.escape(str(block))}</p>"
block_type = block.get("type", "")
if _output_filter.should_hide(block_type):
return ""
if block_type == "image":
source = block.get("source", {})
media_type = source.get("media_type", "image/png")
Expand Down Expand Up @@ -885,7 +930,7 @@ def analyze_conversation(messages):
continue
block_type = block.get("type", "")

if block_type == "tool_use":
if block_type == "tool_use" and not _output_filter.should_hide("tool_use"):
tool_name = block.get("name", "Unknown")
tool_counts[tool_name] = tool_counts.get(tool_name, 0) + 1
elif block_type == "tool_result":
Expand Down Expand Up @@ -954,12 +999,14 @@ def render_message(log_type, message_json, timestamp):
except json.JSONDecodeError:
return ""
if log_type == "user":
content_html = render_user_message_content(message_data)
# Check if this is a tool result message
if is_tool_result_message(message_data):
if _output_filter.should_hide("tool_result"):
return ""
role_class, role_label = "tool-reply", "Tool reply"
else:
role_class, role_label = "user", "User"
content_html = render_user_message_content(message_data)
elif log_type == "assistant":
content_html = render_assistant_message(message_data)
role_class, role_label = "assistant", "Assistant"
Expand Down Expand Up @@ -1295,7 +1342,7 @@ def generate_index_pagination_html(total_pages):
return _macros.index_pagination(total_pages)


def generate_html(json_path, output_dir, github_repo=None):
def generate_html(json_path, output_dir, github_repo=None, output_filter=None):
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)

Expand All @@ -1314,9 +1361,11 @@ def generate_html(json_path, output_dir, github_repo=None):
"Warning: Could not auto-detect GitHub repo. Commit links will be disabled."
)

# Set module-level variable for render functions
# Set module-level variables for render functions
global _github_repo
_github_repo = github_repo
global _output_filter
_output_filter = output_filter or OutputFilter()

conversations = []
current_conv = None
Expand Down Expand Up @@ -1516,7 +1565,15 @@ def cli():
default=10,
help="Maximum number of sessions to show (default: 10)",
)
def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit):
@click.option(
"--output-mode",
type=click.Choice(["full", "compact", "conversation"]),
default="full",
help="Output detail level: full (default), compact (hide tool results), conversation (text only).",
)
def local_cmd(
output, output_auto, repo, gist, include_json, open_browser, limit, output_mode
):
"""Select and convert a local Claude Code session to HTML."""
projects_folder = Path.home() / ".claude" / "projects"

Expand Down Expand Up @@ -1567,7 +1624,12 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
output = Path(tempfile.gettempdir()) / f"claude-session-{session_file.stem}"

output = Path(output)
generate_html(session_file, output, github_repo=repo)
generate_html(
session_file,
output,
github_repo=repo,
output_filter=OutputFilter.from_mode(output_mode),
)

# Show output directory
click.echo(f"Output: {output.resolve()}")
Expand Down Expand Up @@ -1668,7 +1730,15 @@ def fetch_url_to_tempfile(url):
is_flag=True,
help="Open the generated index.html in your default browser (default if no -o specified).",
)
def json_cmd(json_file, output, output_auto, repo, gist, include_json, open_browser):
@click.option(
"--output-mode",
type=click.Choice(["full", "compact", "conversation"]),
default="full",
help="Output detail level: full (default), compact (hide tool results), conversation (text only).",
)
def json_cmd(
json_file, output, output_auto, repo, gist, include_json, open_browser, output_mode
):
"""Convert a Claude Code session JSON/JSONL file or URL to HTML."""
# Handle URL input
if is_url(json_file):
Expand Down Expand Up @@ -1698,7 +1768,12 @@ def json_cmd(json_file, output, output_auto, repo, gist, include_json, open_brow
)

output = Path(output)
generate_html(json_file_path, output, github_repo=repo)
generate_html(
json_file_path,
output,
github_repo=repo,
output_filter=OutputFilter.from_mode(output_mode),
)

# Show output directory
click.echo(f"Output: {output.resolve()}")
Expand Down Expand Up @@ -1775,7 +1850,9 @@ def format_session_for_display(session_data):
return f"{repo_display:30} {date_display:19} {title}"


def generate_html_from_session_data(session_data, output_dir, github_repo=None):
def generate_html_from_session_data(
session_data, output_dir, github_repo=None, output_filter=None
):
"""Generate HTML from session data dict (instead of file path)."""
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True, parents=True)
Expand All @@ -1788,9 +1865,11 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
if github_repo:
click.echo(f"Auto-detected GitHub repo: {github_repo}")

# Set module-level variable for render functions
# Set module-level variables for render functions
global _github_repo
_github_repo = github_repo
global _output_filter
_output_filter = output_filter or OutputFilter()

conversations = []
current_conv = None
Expand Down Expand Up @@ -1983,6 +2062,12 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
is_flag=True,
help="Open the generated index.html in your default browser (default if no -o specified).",
)
@click.option(
"--output-mode",
type=click.Choice(["full", "compact", "conversation"]),
default="full",
help="Output detail level: full (default), compact (hide tool results), conversation (text only).",
)
def web_cmd(
session_id,
output,
Expand All @@ -1993,6 +2078,7 @@ def web_cmd(
gist,
include_json,
open_browser,
output_mode,
):
"""Select and convert a web session from the Claude API to HTML.

Expand Down Expand Up @@ -2068,7 +2154,12 @@ def web_cmd(

output = Path(output)
click.echo(f"Generating HTML in {output}/...")
generate_html_from_session_data(session_data, output, github_repo=repo)
generate_html_from_session_data(
session_data,
output,
github_repo=repo,
output_filter=OutputFilter.from_mode(output_mode),
)

# Show output directory
click.echo(f"Output: {output.resolve()}")
Expand Down Expand Up @@ -2132,7 +2223,13 @@ def web_cmd(
is_flag=True,
help="Suppress all output except errors.",
)
def all_cmd(source, output, include_agents, dry_run, open_browser, quiet):
@click.option(
"--output-mode",
type=click.Choice(["full", "compact", "conversation"]),
default="full",
help="Output detail level: full (default), compact (hide tool results), conversation (text only).",
)
def all_cmd(source, output, include_agents, dry_run, open_browser, quiet, output_mode):
"""Convert all local Claude Code sessions to a browsable HTML archive.

Creates a directory structure with:
Expand Down Expand Up @@ -2198,6 +2295,7 @@ def on_progress(project_name, session_name, current, total):
output,
include_agents=include_agents,
progress_callback=on_progress,
output_filter=OutputFilter.from_mode(output_mode),
)

# Report any failures
Expand Down
4 changes: 2 additions & 2 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,10 @@ def test_handles_failed_session_gracefully(self, output_dir):
# Patch generate_html to fail on one specific session
original_generate_html = __import__("claude_code_transcripts").generate_html

def mock_generate_html(json_path, output_dir, github_repo=None):
def mock_generate_html(json_path, output_dir, github_repo=None, **kwargs):
if "session1" in str(json_path):
raise RuntimeError("Simulated failure")
return original_generate_html(json_path, output_dir, github_repo)
return original_generate_html(json_path, output_dir, github_repo, **kwargs)

with patch(
"claude_code_transcripts.generate_html", side_effect=mock_generate_html
Expand Down
Loading