Skip to content

Render the Artifact tool (deploy HTML/MD page → claude.ai URL) (#257)#260

Merged
cboos merged 3 commits into
mainfrom
dev/artifact-tool
Jul 3, 2026
Merged

Render the Artifact tool (deploy HTML/MD page → claude.ai URL) (#257)#260
cboos merged 3 commits into
mainfrom
dev/artifact-tool

Conversation

@cboos

@cboos cboos commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #257.

Adds rendering support for the Claude Code Artifact tool (2.1.172+), which deploys a self-contained HTML or Markdown file as a (default-private) claude.ai web page.

What it renders

  • Title: 🖼️ Artifact <file_path> — same full-path recipe as Read/Write; the icon is inside the wrench-suppression whitelist so no double 🛠️.
  • Input body: description plus a favicon · label · redeploys <url> · force meta line (only fields that are present).
  • Result: Published <title> → <url> with a clickable link; error/denial results (e.g. a permission denial) fall back to their text.
  • Markdown renderer: same trio, emitting a [url](url) link with a bold page title.

Wire format

Field-for-field from the observed wire format on Claude Code 2.1.198 (real captured publishes, sanitized samples in dev-docs/messages/tools/Artifact-*):

  • tool_use.input: file_path + favicon (required), description / label / url (redeploy target) / force optional. ArtifactInput uses extra="allow" so schema drift across CC versions doesn't drop messages to the generic params table.
  • toolUseResult: {url, path, title}title is the page <title> for .html sources but the file basename for .md ones.
  • text content: Published <path> at <url> on success; plain text + is_error (no toolUseResult) on denial. The parser prefers toolUseResult, falls back to the text line, then to raw text.

A successful publish also appends a uuid-less {type: "frame-link", path, frameUrl, timestamp} bookkeeping entry — fully redundant with the tool_result, so the second commit adds it to SILENT_SKIP_TYPES (together with mode, the same session-state family as permission-mode).

XSS

The page content itself never reaches the transcript (the tool takes a file path), but every string field — description, label, title, path, URL — is transcript-derived:

  • all fields render escaped in both renderers (the Escape raw HTML in assistant/tool content to fix XSS  #245 contract);
  • URLs become links only for http(s) schemes — a hostile javascript: URL stays inert escaped text in HTML and inline code in Markdown (new _markdown_safe_url helper, parens percent-encoded in link targets).

Tests

31 tests in test/test_artifact_rendering.py (models, parser fallbacks, both renderers, scheme guards) including end-to-end HTML + Markdown runs over test/test_data/artifact_tool.jsonl — 5 variants: HTML publish, Markdown publish, redeploy+force, classifier-denied error, and a hostile entry with <script>/onerror/javascript: payloads. Full just ci green on this tip (rebased onto #258's main).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added end-to-end support for the Artifact tool (tool input/output parsing and HTML/Markdown rendering).
    • Artifact rendering now shows “Published” links, paths, and titles when available.
  • Bug Fixes
    • Reduced transcript noise by silently skipping additional internal message types.
    • Hardened rendering for XSS and URL safety by gating clickable links to http(s) only.
  • Documentation
    • Updated developer docs with Artifact tool examples and the new URL-safety helper for plugins.
  • Tests
    • Added comprehensive Artifact rendering/parsing/end-to-end and plugin API coverage.

cboos and others added 2 commits July 3, 2026 01:01
Typed input/output models + HTML and Markdown renderers for the
Artifact tool (Claude Code 2.1.172+): title '🖼️ Artifact <file_path>',
body with description/favicon/label/redeploy metadata, result as
'Published <title> → <url>'. Shapes match the wire format observed on
Claude Code 2.1.198 (toolUseResult {url, path, title}; title is the
page <title> for .html sources, the file basename for .md ones).

The page content never reaches the transcript (the tool takes a file
path), but every string field is transcript-derived: all fields render
escaped (#245 contract) and URLs only become links for http(s) schemes
— a hostile javascript: URL stays inert text in both renderers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A successful Artifact publish also appends a uuid-less
{type: 'frame-link', path, frameUrl, timestamp} entry mapping the
source file to the deployed claude.ai page — fully redundant with the
Artifact tool_result, so skip it without the 'unrecognized message
type' warning. 'mode' ({mode: 'normal'|...}) joins the same
session-state-snapshot family as 'permission-mode'.

The silent-skip warn-once test now uses a made-up type as its unknown
example since 'mode' is recognized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8b4aa4d-4eea-437a-a68f-442f6a4b3c29

📥 Commits

Reviewing files that changed from the base of the PR and between e5db628 and c47d39c.

📒 Files selected for processing (7)
  • claude_code_log/html/tool_formatters.py
  • claude_code_log/markdown/renderer.py
  • claude_code_log/models.py
  • claude_code_log/plugins.py
  • claude_code_log/utils.py
  • dev-docs/plugins.md
  • test/test_plugin_xss_api.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • claude_code_log/models.py
  • claude_code_log/html/tool_formatters.py
  • claude_code_log/markdown/renderer.py

📝 Walkthrough

Walkthrough

Adds Artifact tool support across parsing, models, HTML and Markdown rendering, URL-safety helpers, silent-skip handling, sample transcripts, docs, and tests.

Changes

Artifact Tool Support

Layer / File(s) Summary
Silent-skip transcript types
claude_code_log/converter.py, test/test_silent_skip.py
SILENT_SKIP_TYPES now includes mode and frame-link; the related warning test uses not-yet-known instead.
Artifact models and parsing
claude_code_log/models.py, claude_code_log/factories/tool_factory.py
Adds ArtifactInput and ArtifactOutput, registers Artifact input parsing, and parses Artifact outputs from structured results or published-text fallbacks.
HTML rendering support
claude_code_log/html/tool_formatters.py, claude_code_log/html/renderer.py, claude_code_log/html/__init__.py
Adds Artifact HTML formatter functions, exports them, and wires HtmlRenderer dispatch/title handling for Artifact input and output.
Markdown rendering support
claude_code_log/markdown/renderer.py
Adds Artifact Markdown-safe URL handling plus input, output, and title rendering methods.
URL safety helper and plugin export
claude_code_log/utils.py, claude_code_log/plugins.py, dev-docs/plugins.md, test/test_plugin_xss_api.py
Adds is_safe_web_url, re-exports it through plugins, documents it, and tests its public contract.
Artifact fixtures, docs, and rendering tests
dev-docs/messages.md, dev-docs/messages/tools/Artifact-tool_*, test/test_data/artifact_tool.jsonl, test/test_artifact_rendering.py
Adds Artifact sample messages, a JSONL fixture, and end-to-end rendering tests covering parsing, escaping, link safety, and transcript rendering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: daaain

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Artifact tool rendering support for claude.ai URL output.
Linked Issues check ✅ Passed The PR implements Artifact tool parsing, modeling, and HTML/Markdown rendering as requested in #257.
Out of Scope Changes check ✅ Passed The changes stay focused on Artifact support and related safety, tests, and docs with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/artifact-tool

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
claude_code_log/models.py (1)

1485-1502: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring conflicts with the actual optionality of favicon.

The docstring states "Only file_path and favicon are required in the current schema" (lines 1489-1490), but favicon: str = "" has a default value, making it optional per Pydantic semantics — only file_path is actually required. This could mislead future maintainers into assuming favicon must always be supplied.

📝 Proposed doc fix
-    Deploys a self-contained HTML or Markdown file as a (default-private)
-    claude.ai web page. Only ``file_path`` and ``favicon`` are required in
-    the current schema; keep the rest optional and tolerate unknown fields
-    so schema tweaks across Claude Code versions don't drop the message to
-    the generic params table.
+    Deploys a self-contained HTML or Markdown file as a (default-private)
+    claude.ai web page. Only ``file_path`` is required in the current
+    schema; keep the rest optional and tolerate unknown fields so schema
+    tweaks across Claude Code versions don't drop the message to the
+    generic params table.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/models.py` around lines 1485 - 1502, The ArtifactInput
docstring is inconsistent with the model definition because favicon is not
required in the schema. Update the ArtifactInput class docstring to reflect the
actual required fields, keeping file_path as required and describing favicon as
optional/defaulted, so the documentation matches the Pydantic field defaults and
optional parameters like description, label, url, and force remain clearly
optional.
claude_code_log/html/tool_formatters.py (1)

865-875: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate scheme-guard logic vs. Markdown renderer.

_artifact_link and claude_code_log/markdown/renderer.py's _markdown_safe_url implement the same http(s) scheme allow-list independently. Consider extracting a shared is_safe_web_url(url: str) -> bool (or similar) helper into a common module so the scheme policy (and any future changes to it, e.g. adding case-insensitivity) is maintained in one place instead of two.

The ast-grep "cleartext HTTP" hint on this line is a false positive — this code isn't making an HTTP call, it's just permitting http:// alongside https:// as a renderable link scheme.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/html/tool_formatters.py` around lines 865 - 875,
`_artifact_link` duplicates the same http(s) scheme allow-list already used by
`_markdown_safe_url`, so factor that policy into a shared helper such as
`is_safe_web_url(url: str) -> bool` in a common module and have both
`_artifact_link` and `claude_code_log/markdown/renderer.py` call it. Keep the
existing behavior of only emitting an `href` for safe web URLs, and ensure the
shared helper is the single place to update scheme rules such as
case-insensitive matching.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@claude_code_log/html/tool_formatters.py`:
- Around line 865-875: `_artifact_link` duplicates the same http(s) scheme
allow-list already used by `_markdown_safe_url`, so factor that policy into a
shared helper such as `is_safe_web_url(url: str) -> bool` in a common module and
have both `_artifact_link` and `claude_code_log/markdown/renderer.py` call it.
Keep the existing behavior of only emitting an `href` for safe web URLs, and
ensure the shared helper is the single place to update scheme rules such as
case-insensitive matching.

In `@claude_code_log/models.py`:
- Around line 1485-1502: The ArtifactInput docstring is inconsistent with the
model definition because favicon is not required in the schema. Update the
ArtifactInput class docstring to reflect the actual required fields, keeping
file_path as required and describing favicon as optional/defaulted, so the
documentation matches the Pydantic field defaults and optional parameters like
description, label, url, and force remain clearly optional.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68926176-6a38-43ad-b6dc-f53d41dbdac5

📥 Commits

Reviewing files that changed from the base of the PR and between 6496622 and e5db628.

📒 Files selected for processing (15)
  • claude_code_log/converter.py
  • claude_code_log/factories/tool_factory.py
  • claude_code_log/html/__init__.py
  • claude_code_log/html/renderer.py
  • claude_code_log/html/tool_formatters.py
  • claude_code_log/markdown/renderer.py
  • claude_code_log/models.py
  • dev-docs/messages.md
  • dev-docs/messages/tools/Artifact-tool_result.json
  • dev-docs/messages/tools/Artifact-tool_result.jsonl
  • dev-docs/messages/tools/Artifact-tool_use.json
  • dev-docs/messages/tools/Artifact-tool_use.jsonl
  • test/test_artifact_rendering.py
  • test/test_data/artifact_tool.jsonl
  • test/test_silent_skip.py

Address CodeRabbit review on #260:

- The http(s) scheme whitelist was implemented twice (_artifact_link in
  html/tool_formatters.py, _markdown_safe_url in markdown/renderer.py).
  Extract is_safe_web_url() into claude_code_log/utils.py (format-neutral,
  import-cycle-free) and delegate both call sites; the policy — including
  its deliberate fail-closed case-sensitivity — now lives in one place.
- Re-export it from claude_code_log.plugins (_PUBLIC_HELPERS + lazy
  PEP-562 resolution + TYPE_CHECKING import) and document it in
  dev-docs/plugins.md §4.1/§4.2: a plugin emitting a transcript-derived
  URL as an href / Markdown link target needs the same gate, since
  escaping can't neutralise a hostile javascript: scheme.
- Fix the ArtifactInput docstring: only file_path is required by the
  model; favicon is required by the live tool schema but defaulted here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cboos

cboos commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

(Claude) Both review comments addressed in c47d39c:

  • Duplicate scheme-guard logic: extracted is_safe_web_url() into claude_code_log/utils.py (format-neutral, import-cycle-free); both _artifact_link and _markdown_safe_url now delegate to it, so the scheme policy — including its deliberate fail-closed case-sensitivity — is maintained in one place. Also re-exported via claude_code_log.plugins and documented in dev-docs/plugins.md §4.1/§4.2, since a plugin emitting a transcript-derived URL as an href/link target needs the same gate (escaping can't neutralise a hostile javascript: scheme).
  • favicon docstring: reworded — only file_path is required by the model; favicon is required by the live tool schema but defaulted here so variant transcripts still parse.

Full just ci green on the new tip.

@cboos cboos merged commit 4037b26 into main Jul 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for the Artifact tool

1 participant