Skip to content

Transcript & Context Engineering

The transcript is Claude's memory - a JSONL file containing the entire conversation history. It's mutable: edit it, and you edit what Claude remembers.

"Claude only knows what's in the transcript. Modify it, and Claude's memory changes."

Quick Start

from fasthooks import HookApp
from fasthooks.depends import Transcript

app = HookApp()

@app.on_prompt()
def inject_context(event, transcript: Transcript):
    """Add context before Claude responds."""
    from fasthooks.transcript import UserMessage

    # Create a "memory" that user mentioned type hints
    reminder = UserMessage.create(
        "Remember: always use type hints in this project",
        context=transcript.entries[-1]  # Copy metadata from last entry
    )
    transcript.insert(0, reminder)  # Insert at start
    transcript.save()

The Transcript Model

Loading a Transcript

from fasthooks.transcript import Transcript

# From path
t = Transcript("/path/to/transcript.jsonl")

# In hooks - auto-injected via DI
@app.pre_tool("Bash")
def check(event, transcript: Transcript):
    print(f"Entries: {len(transcript.entries)}")

Entry Types

Type Description
UserMessage User input or tool results
AssistantMessage Claude's responses
SystemEntry System events (compaction, hooks)
FileHistorySnapshot File backup for undo
from fasthooks.transcript import UserMessage, AssistantMessage

for entry in transcript.entries:
    if isinstance(entry, UserMessage):
        print(f"User: {entry.text[:50]}...")
    elif isinstance(entry, AssistantMessage):
        print(f"Claude: {entry.text[:50]}...")
        if entry.has_tool_use:
            for tu in entry.tool_uses:
                print(f"  Tool: {tu.name}")

Content Blocks

Assistant messages contain content blocks:

for entry in transcript.assistant_messages:
    # Text content
    print(entry.text)

    # Thinking (extended thinking mode)
    if entry.thinking:
        print(f"Thinking: {entry.thinking[:100]}...")

    # Tool uses
    for tu in entry.tool_uses:
        print(f"Tool: {tu.name}, Input: {tu.input}")

        # Get the result
        if tu.result:
            print(f"Result: {tu.result.content[:100]}...")
            if tu.result.is_error:
                print("  (error)")

Querying

Pre-built Views

# Messages by type
transcript.user_messages      # List[UserMessage]
transcript.assistant_messages # List[AssistantMessage]

# Tool interactions
transcript.tool_uses    # All ToolUseBlocks
transcript.tool_results # All ToolResultBlocks
transcript.errors       # Tool results where is_error=True

# Groupings
transcript.turns        # List[Turn] - grouped by requestId

Fluent Query API

Inspired by Django ORM and Tidyverse:

# Type shortcuts
transcript.query().users().all()
transcript.query().assistants().with_tools().all()

# Filtering
transcript.query().filter(type="assistant").all()
transcript.query().filter(text__contains="error").all()
transcript.query().where(lambda e: e.has_tool_use).all()

# Lookups
transcript.query().filter(timestamp__gt=datetime(2024, 1, 1)).all()
transcript.query().filter(type__in=["user", "assistant"]).all()

# Ordering
transcript.query().order_by("-timestamp").limit(10).all()

# Terminals
transcript.query().assistants().count()     # int
transcript.query().with_errors().exists()   # bool
transcript.query().filter(uuid="abc").one() # single entry or ValueError

Time-based Queries

from datetime import datetime

# Entries since timestamp
transcript.query().since(datetime(2024, 1, 1)).all()
transcript.query().since("2024-01-01T00:00:00").all()

# Entries until timestamp
transcript.query().until(datetime.now()).all()

Creating Entries

Factory Methods

from fasthooks.transcript import UserMessage, AssistantMessage

# Create user message
msg = UserMessage.create(
    "Remember to use Python 3.11+",
    parent=transcript.entries[-1],  # Sets parent_uuid
    context=transcript.entries[0],  # Copies session_id, cwd, etc.
)

# Create assistant message
response = AssistantMessage.create(
    "Understood, I'll use Python 3.11+ features.",
    parent=msg,
    model="synthetic",  # Default
)

Created entries are marked with is_synthetic=True.

Injecting Tool Results

For faking tool executions:

from fasthooks.transcript import inject_tool_result

# Claude "remembers" running this command
assistant, user = inject_tool_result(
    transcript,
    tool_name="Read",
    tool_input={"file_path": "/project/config.json"},
    result='{"debug": true, "log_level": "INFO"}',
)

# With error
inject_tool_result(
    transcript,
    "Bash",
    {"command": "rm -rf /"},
    "Permission denied",
    is_error=True,
)

# At specific position
inject_tool_result(transcript, "Bash", {...}, "output", position="start")
inject_tool_result(transcript, "Bash", {...}, "output", position=5)

CRUD Operations

Insert

# At position (rewires parent_uuid chain)
transcript.insert(0, entry)      # At start
transcript.insert(5, entry)      # At index 5

# At end
transcript.append(entry)

Remove

# Remove single entry, relink children
transcript.remove(entry, relink=True)  # Default

# Remove entry and all descendants
removed = transcript.remove_tree(entry)
print(f"Removed {len(removed)} entries")

Replace

# Swap entry, preserve position in chain
transcript.replace(old_entry, new_entry)

Save

# Atomic write (temp file + rename)
transcript.save()

# Batch operations with auto-commit/rollback
with transcript.batch():
    transcript.remove(entry1)
    transcript.insert(0, new_entry)
    # Auto-saves on success, rollback on exception

Statistics

stats = transcript.stats

# Token usage
print(f"Input: {stats.input_tokens}")
print(f"Output: {stats.output_tokens}")
print(f"Cache read: {stats.cache_read_tokens}")

# Tool calls
print(f"Tools: {stats.tool_calls}")  # {"Bash": 5, "Read": 3}
print(f"Errors: {stats.error_count}")

# Session info
print(f"Messages: {stats.message_count}")
print(f"Turns: {stats.turn_count}")
print(f"Duration: {stats.duration_seconds}s")

Exporting

To String

# Markdown - nice for reading
md = transcript.to_markdown()
md = transcript.to_markdown(
    include_thinking=True,      # Show thinking blocks (collapsed)
    include_tool_input=True,    # Show tool input JSON
    max_content_length=500,     # Truncate long content
)

# HTML - for sharing
html = transcript.to_html(title="Debug Session")

# JSON - for processing
json_str = transcript.to_json(indent=2)

# JSONL - original format
jsonl = transcript.to_jsonl()

To File

transcript.to_file("session.md")
transcript.to_file("session.html", format="html")
transcript.to_file("session.json", format="json")

Use Cases

Inject Project Context

@app.on_session_start()
def add_context(event, transcript: Transcript):
    """Inject project guidelines at session start."""
    reminder = UserMessage.create(
        "Project rules: Use Black formatting, type hints required, pytest for tests",
        context=transcript.entries[0] if transcript.entries else None,
    )
    transcript.insert(0, reminder)
    transcript.save()

Redact Sensitive Data

@app.on_stop()
def redact_secrets(event, transcript: Transcript):
    """Remove API keys from transcript."""
    import re
    pattern = re.compile(r'sk-[a-zA-Z0-9]{32,}')

    with transcript.batch():
        for result in transcript.tool_results:
            if pattern.search(result.content):
                result.content = pattern.sub('[REDACTED]', result.content)

Summarize Large Outputs

@app.post_tool("Read")
def summarize_large_files(event, transcript: Transcript):
    """Replace large file contents with summary."""
    if len(event.content or "") > 5000:
        for result in transcript.tool_results:
            if result.tool_use_id == event.tool_use_id:
                lines = result.content.count('\n')
                result.content = f"[File: {event.file_path}, {lines} lines]"
        transcript.save()

Analyze Session

@app.on_stop()
def analyze(event, transcript: Transcript):
    """Log session analytics."""
    stats = transcript.stats

    # Check for issues
    if stats.error_count > 5:
        print(f"Warning: {stats.error_count} errors in session")

    # Export for review
    if stats.output_tokens > 50000:
        transcript.to_file(f"/tmp/large_session_{stats.slug}.md")

Fake Tool Results for Context

@app.on_prompt()
def inject_fake_config(event, transcript: Transcript):
    """Make Claude 'remember' reading a config file."""
    inject_tool_result(
        transcript,
        "Read",
        {"file_path": "/project/.claude-config"},
        "prefer_typescript=true\nmax_file_size=1000",
        position="start",
    )
    transcript.save()

Advanced: Archived Entries

Entries before the last context compaction are in transcript.archived:

# Current context window only (default)
transcript.entries

# Pre-compaction entries
transcript.archived

# Both
transcript.all_entries

# Query with archived
transcript.query(include_archived=True).count()

When Do Changes Take Effect?

Hook When Changes Apply
on_session_start First response
on_prompt Current response
pre_tool Next turn (current continues)
post_tool Next turn
on_stop Next user turn

To affect the current response, modify in on_prompt before Claude starts.