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
36200 skills
Install
$ npx -y skills add OutlineDriven/odin-claude-plugin --skill session-viewer --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/session-viewer

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

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
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

Get the whole plugin
Stats
36
Stars
0
Forks
Active
Maintenance
Python
Language
Apache-2.0
License
3d ago
Last commit
10mo ago
Created

Repo: OutlineDriven/odin-claude-plugin

Other skills on odin-claude-plugin.