Skip to content
Development
Skill

/session-viewer

Use when the user asks to view, export, or inspect a session transcript in a browser. Not for sharing a session: use session-share.

From plugin
odin-claude-plugin
40200 skills

Claude Code

$ npx -y skills add OutlineDriven/odin-claude-plugin --skill session-viewer --agent claude-code

SKILL.md

session-viewer.SKILL.md
name: session-viewer
description: 'Use when the user asks to view, export, or inspect a session transcript in a browser. Not for sharing a session: use session-share.'

Session viewer

Contract

| Field | Bound contract | |---|---| | Trigger | User asks to view, export, inspect, or share a Codex, Claude Code, OpenClaw, or Pi session transcript in a browser. | | Authority | Reversible local: writes only the single HTML viewer file and one disposable generator script in the system temp directory; rollback is undo. No remote mutation. Never modifies the session file. | | Side effect | A single-file searchable HTML viewer embedding the (optionally raw) session JSONL is produced; it is opened in a browser only when the user asked to view it or passed `--open`. | | Done | HTML file is generated; session is correctly detected and normalized; tool output is searchable; private/credential content is not exposed. The file opens in a browser only when the user passed `--open` or explicitly asked to view it. |

Not for

  • Beaming or publishing a session to a remote endpoint, use session-share.

Inputs

  • Required: the path to an existing, non-empty, at most 256 MiB session `.jsonl` file. A directory, a plain text log, or a URL is out of scope; stop and ask for the path instead of guessing.
  • Optional flags: `--format claude|codex|pi|openclaw` to force detection, `--out PATH` for the output file, `--raw` to embed the original lines (only on explicit user opt-in), `--open` to launch the browser.
  • The host needs Python 3 with its standard library. No other runtime, package, service, or skill is involved.

Procedure

1. Validate the input path at the trust boundary before any mutation: the file must exist, be non-empty, and be at most 256 MiB; otherwise stop with no writes. **Done when:** the path is validated or the stop is reported. 2. Fix the privacy mode before writing: default mode embeds a normalized, credential-scrubbed projection. Embed raw lines only when the user explicitly opts in with `--raw`; raw mode changes fidelity, never the scrub or the local-only boundary. **Done when:** the privacy mode is fixed. 3. Write the generator script below exactly as given to a scratch file in the system temp directory (for example `/tmp/session_viewer_gen.py`). It uses only the Python 3 standard library. Do not edit it. **Done when:** the scratch script is written. 4. Run `python3 <scratch> <session.jsonl>` with any optional flags. The script detects the format from structural signatures with a path-hint tiebreak, parses the JSONL line by line, and normalizes each line into unified records (index, timestamp, kind: user, assistant, system, summary, thinking, tool-call, tool-result, other; role, tool name, text). It scrubs credential-shaped strings (sk- tokens, ghp_ tokens, AKIA keys, xox tokens, bearer headers, key/token/password assignments). It renders one self-contained HTML viewer with an embedded JSON payload, substring search across message text, tool names, roles, and tool output, role filter chips, collapsible raw lines in raw mode, a metadata header, no external assets, and no network access. Every record is rendered through textContent so session content cannot inject markup. **Done when:** the HTML viewer is generated and the script report is captured. 5. Read the script report: format and how it was chosen, line, record, and skipped counts, masked-string count, raw mode, output path, and size. To prove the done predicate, open the file when the user asked to view it (or run with `--open`) and confirm it renders and that searching returns tool output. **Done when:** the report is read and the done predicate is proven or disproven. 6. Delete the scratch script. The HTML file is the only remaining artifact; deleting it is the complete rollback. **Done when:** the scratch script is deleted and only the HTML file remains.

#!/usr/bin/env python3
"""Generate a single-file searchable HTML viewer from an agent session JSONL.

Supported session formats: claude, codex, pi, openclaw.
Usage:
  python3 session_viewer_gen.py SESSION.jsonl [--format auto|claude|codex|pi|openclaw]
      [--out PATH] [--raw] [--open]
Exit codes: 0 success, 2 input or parse failure, 3 ambiguous format.
Standard library only; no network access.
"""
import argparse
import html
import json
import re
import sys
import webbrowser
from datetime import datetime, timezone
from pathlib import Path

FORMATS = ("claude", "codex", "pi", "openclaw")
MAX_INPUT_BYTES = 256 * 1024 * 1024
MASK = "[redacted]"

SCRUBBERS = (
    (re.compile(r"\bsk-[A-Za-z0-9_-]{16,}"), MASK),
    (re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}"), MASK),
    (re.compile(r"\bAKIA[0-9A-Z]{16}\b"), MASK),
    (re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}"), MASK),
    (re.compile(r"(?i)authorization[\s\"':=]+bearer\s+\S{16,}"), MASK),
    (re.compile(r"(?i)\b(?:api[_-]?key|secret|token|password)[\"'\s:=]{1,4}\S{16,}"), MASK),
)

masked_hits = [0]

def scrub(text):
    if not isinstance(text, str):
        return text
    for pattern, replacement in SCRUBBERS:
        text, count = pattern.subn(replacement, text)
        masked_hits[0] += count
    return text

def record(index, kind, role, text, tool="", ts="", raw=None):
    return {
        "i": index,
        "kind": kind,
        "role": role,
        "tool": tool,
        "ts": ts,
        "text": scrub(text) if isinstance(text, str) else "",
        "raw": raw,
    }

def text_of(content):
    if isinstance(content, str):
        return content
    parts = []
    if isinstance(content, list):
        for block in content:
            parts.append(text_of(block))
    elif isinstance(content, dict):
        for key in ("text", "thinking", "input_text", "output_text", "summary_text", "query", "content", "output"):
            if key in content:
                parts.append(text_of(content[key]))
    elif content is not None:
        parts.append(str(content))
    return chr(10).join(part fo
Read more
@outlinedriven
Ships withodin-claude-plugin

Formerly the ODIN Claude Plugin. The repository URL is unchanged. Outline-Driven Development, nicknamed ODIN, is a highly opinionated code-agent skill library: principles-first engineering, surgical editing, and workflow automation, published as installable

Stars on GitHub

Momentum

stars in total
40
Star history of OutlineDriven/odin-claude-plugin on GitHub, a chart by star-history.com

Other skills on odin-claude-plugin.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about session-viewer.

Which coding agents can use session-viewer?
session-viewer installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.