TO

tool-renderer

Enables the implementation of custom rendering logic for Claude Code tools.

Install

mkdir -p .claude/skills/tool-renderer && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4935" && unzip -o skill.zip -d .claude/skills/tool-renderer && rm skill.zip

Installs to .claude/skills/tool-renderer

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Implement specialized rendering for Claude Code tools. Use when adding a new tool type (WebSearch, WebFetch, etc.) to the transcript viewer, or when asked to implement tool rendering.
183 chars✓ has a “when” trigger
Intermediate

Key capabilities

  • Define Pydantic input models for tool interactions
  • Register tool factories for transcript parsing
  • Implement custom HTML formatters for tool outputs
  • Parse and render toolUseResult structures

How it works

Integrates with the transcript viewer by linking input/output models to HTML template formatters, mapping structured tool data to visual components.

Inputs & outputs

You give it
Tool definition JSON and desired visual layout
You get back
Pydantic models and HTML rendering functions

When to use tool-renderer

  • Add renderer for new tool
  • Define tool output model
  • Create custom HTML tool output

About this skill

Implementing a Tool Renderer

This guide walks through adding rendering support for a new Claude Code tool, using WebSearch as an example.

Before You Start

Examine existing test data to understand the tool's actual JSON structure:

# Find test files containing the tool
rg -l "ToolName" test/test_data/

# Look at actual JSONL entries
rg '"name":\s*"ToolName"' test/test_data/ -A 2 -B 2

Key fields to identify:

  • Input parameters: What's in tool_use.input?
  • toolUseResult structure: What metadata does the structured result contain?
  • tool_result.content: What does the raw text output look like?

The toolUseResult field on transcript entries often contains richer structured data than tool_result.content. Always prefer parsing from toolUseResult when available.

Overview

Tool rendering involves several components working together:

  1. Models (models.py) - Type definitions for tool inputs and outputs
  2. Factory (factories/tool_factory.py) - Parsing raw JSON into typed models
  3. HTML Formatters (html/tool_formatters.py) - HTML rendering functions
  4. Renderers - Integration with HTML and Markdown renderers

Step 1: Define Models

Tool Input Model

Add a Pydantic model for the tool's input parameters in models.py:

class WebSearchInput(BaseModel):
    """Input parameters for the WebSearch tool."""
    query: str

Tool Output Model

Add a dataclass for the parsed output. Output models are dataclasses (not Pydantic) since they're created by our parsers, not from JSON:

@dataclass
class WebSearchLink:
    """Single search result link."""
    title: str
    url: str

@dataclass
class WebSearchOutput:
    """Parsed WebSearch tool output."""
    query: str
    links: list[WebSearchLink]
    preamble: Optional[str] = None  # Text before the Links
    summary: Optional[str] = None   # Markdown analysis after the Links

Note: Some tools have structured output with multiple sections. WebSearch is parsed as preamble/links/summary - text before Links, the Links JSON array, and markdown analysis after. This allows flexible rendering while preserving all content.

Update Type Unions

Add the new types to the ToolInput and ToolOutput unions:

ToolInput = Union[
    # ... existing types ...
    WebSearchInput,
    ToolUseContent,  # Generic fallback - keep last
]

ToolOutput = Union[
    # ... existing types ...
    WebSearchOutput,
    ToolResultContent,  # Generic fallback - keep last
]

Step 2: Implement Factory Functions

In factories/tool_factory.py:

Register Input Model

Add the input model to TOOL_INPUT_MODELS:

TOOL_INPUT_MODELS: dict[str, type[BaseModel]] = {
    # ... existing entries ...
    "WebSearch": WebSearchInput,
}

Implement Output Parser

Important: Always check if the tool has structured toolUseResult data available. This is the preferred approach because:

  • It's more reliable than regex parsing of text content
  • It often contains metadata (timing, byte counts, status codes) not in the text
  • The structure is well-defined and type-safe

Example toolUseResult structures in test data:

// WebSearch
{"query": "...", "results": [...], "durationSeconds": 15.7}

// WebFetch
{"url": "...", "result": "...", "code": 200, "codeText": "OK", "bytes": 12345, "durationMs": 1500}

Create a parser function that extracts from toolUseResult:

def _parse_websearch_from_structured(
    tool_use_result: ToolUseResult,
) -> Optional[WebSearchOutput]:
    """Parse WebSearch from structured toolUseResult data.

    The toolUseResult for WebSearch has the format:
    {
        "query": "search query",
        "results": [
            {"tool_use_id": "...", "content": [{"title": "...", "url": "..."}]},
            "Analysis text..."
        ]
    }
    """
    if not isinstance(tool_use_result, dict):
        return None
    query = tool_use_result.get("query")
    results = tool_use_result.get("results")
    # ... extract links from results[0].content, summary from results[1] ...
    return WebSearchOutput(query=query, links=links, preamble=None, summary=summary)


def parse_websearch_output(
    tool_result: ToolResultContent,
    file_path: Optional[str],
    tool_use_result: Optional[ToolUseResult] = None,  # Extended signature
) -> Optional[WebSearchOutput]:
    """Parse WebSearch tool result from structured toolUseResult."""
    del tool_result, file_path  # Unused
    if tool_use_result is None:
        return None
    return _parse_websearch_from_structured(tool_use_result)

Register Output Parser

Add to TOOL_OUTPUT_PARSERS and register in PARSERS_WITH_TOOL_USE_RESULT if using the extended signature:

TOOL_OUTPUT_PARSERS: dict[str, ToolOutputParser] = {
    # ... existing entries ...
    "WebSearch": parse_websearch_output,
}

# REQUIRED for parsers that use toolUseResult - without this, the structured
# data won't be passed to your parser!
PARSERS_WITH_TOOL_USE_RESULT: set[str] = {"WebSearch", "WebFetch"}

Note: If your parser has the 3-argument signature (tool_result, file_path, tool_use_result), you MUST add it to PARSERS_WITH_TOOL_USE_RESULT. Otherwise create_tool_output() won't pass the structured data.

Step 3: Implement HTML Formatters

In html/tool_formatters.py:

Input Formatter

Design consideration: The title already shows key info (tool name + primary parameter). Only show content in the body if it adds value or is too long for the title.

def format_websearch_input(search_input: WebSearchInput) -> str:
    """Format WebSearch tool use content."""
    # If query is short enough to fit in title, return empty
    if len(search_input.query) <= 100:
        return ""  # Full query shown in title
    escaped_query = escape_html(search_input.query)
    return f'<div class="websearch-query">{escaped_query}</div>'

This avoids redundancy when the title already shows everything important.

Output Formatter

For tools with structured content like WebSearch, combine all parts into markdown then render:

def _websearch_as_markdown(output: WebSearchOutput) -> str:
    """Convert WebSearch output to markdown: preamble + links list + summary."""
    parts = []
    if output.preamble:
        parts.extend([output.preamble, ""])
    for link in output.links:
        parts.append(f"- [{link.title}]({link.url})")
    if output.summary:
        parts.extend(["", output.summary])
    return "\n".join(parts)


def format_websearch_output(output: WebSearchOutput) -> str:
    """Format WebSearch as single collapsible markdown block."""
    markdown_content = _websearch_as_markdown(output)
    return render_markdown_collapsible(markdown_content, "websearch-results")

Update Exports

Add functions to __all__:

__all__ = [
    # ... existing exports ...
    "format_websearch_input",
    "format_websearch_output",
]

Step 4: Wire Up HTML Renderer

In html/renderer.py:

Import Formatters

from .tool_formatters import (
    # ... existing imports ...
    format_websearch_input,
    format_websearch_output,
)

Add Format Methods

def format_WebSearchInput(self, input: WebSearchInput, _: TemplateMessage) -> str:
    return format_websearch_input(input)

def format_WebSearchOutput(self, output: WebSearchOutput, _: TemplateMessage) -> str:
    return format_websearch_output(output)

Add Title Method (Optional)

For a custom title in the message header:

def title_WebSearchInput(self, input: WebSearchInput, message: TemplateMessage) -> str:
    return self._tool_title(message, "🔎", f'"{input.query}"')

Watch out: the template's wrench-suppression is emoji-range-gated

Tool-use messages get a default 🛠️ prefix prepended by templates/transcript.html unless the title already starts with an emoji that html/utils.py::starts_with_emoji recognises. That function whitelists specific Unicode ranges:

  • 0x2300-0x23FF Misc Technical (⏰ ⏳ ⏱️ ⏲️ ⏸ ⏹ ⏺ ⏏ …)
  • 0x2600-0x26FF Misc Symbols
  • 0x2700-0x27BF Dingbats
  • 0x1F300-0x1F5FF Misc Symbols and Pictographs
  • 0x1F600-0x1F64F Emoticons
  • 0x1F680-0x1F6FF Transport and Map Symbols
  • 0x1F900-0x1F9FF Supplemental Symbols

If the icon you pass to _tool_title falls outside these ranges, the template will helpfully add a 🛠️ in front of it, producing a redundant double-icon title like 🛠️ <your-icon> <ToolName>. Verify by rendering a fixture and grepping for 🛠️ co-occurring with your icon, or by checking ord(your_icon) against the ranges above.

If your icon is a real emoji that lives in a Unicode range not listed there, add the range to starts_with_emoji rather than picking a different icon.

Step 5: Implement Markdown Renderer

In markdown/renderer.py:

Import Models

from ..models import (
    # ... existing imports ...
    WebSearchInput,
    WebSearchOutput,
)

Add Format Methods

def format_WebSearchInput(self, input: WebSearchInput, _: TemplateMessage) -> str:
    """Format -> empty (query shown in title)."""
    return ""

def format_WebSearchOutput(self, output: WebSearchOutput, _: TemplateMessage) -> str:
    """Format -> markdown list of links."""
    parts = [f"Query: *{output.query}*", ""]
    for link in output.links:
        parts.append(f"- [{link.title}]({link.url})")
    return "\n".join(parts)

def title_WebSearchInput(self, input: WebSearchInput, _: TemplateMessage) -> str:
    """Title -> '🔎 WebSearch `query`'."""
    return f'🔎 WebSearch `{input.query}`'

Step 6: Add Tests

Create a dedicated test file test/test_{toolname}_rendering.py. Tests are required - they catch regressions and document expected behavior.

Test Structure

"""Test cases for {ToolName} tool rendering."""

from claude_code_log.factories.tool_factory import 

---

*Content truncated.*

When not to use it

  • Core business logic implementation
  • Non-Claude Code environments

Prerequisites

pydantic

Limitations

  • Requires manual parsing of existing test data for accurate modeling
  • Limited to the existing renderer/factory architecture

How it compares

It simplifies the process of creating visual representations for tool results that would otherwise show up as raw JSON blobs.

Compared to similar skills

tool-renderer side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
tool-renderer (this skill)13moReviewIntermediate
python-project-structure86moNo flagsBeginner
graph66moNo flagsAdvanced
library15moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry