Using Claude Code: The unreasonable effectiveness of HTML
A 900-line terminal transcript is technically complete and practically unreadable. The same debugging session rendered as a 70 KB HTML file—with a timeline, grouped failures, request/response diffs, and collapsible stack traces—can turn a 30-minute review into a five-minute decision.
That is the unreasonable effectiveness of HTML in Claude Code: not using HTML merely to build websites, but using it as a universal interface for development work.
HTML gives an agent a cheap, expressive output surface. It supports hierarchy, tables, color, diagrams, forms, links, responsive layouts, and progressive disclosure without requiring a frontend project. Claude Code can generate one self-contained file, you can open it locally, and the artifact remains useful after the conversation disappears.
Why HTML works so well as an agent output format
Most agent output defaults to one of three forms:
- Terminal text, which is fast but visually constrained.
- Markdown, which is readable but weak for interactive or dense information.
- Application code, which may require dependencies, a build, and a running server.
A standalone HTML document sits between documentation and software. It has enough structure to communicate complex results, but almost no operational overhead.
| Output format | Best use | Main limitation | Setup required |
|---|---|---|---|
| Terminal text | Short answers, commands, status updates | Poor navigation and visual density | None |
| Markdown | Plans, documentation, code review notes | Limited interaction and layout | Markdown viewer |
| JSON | Machine-to-machine exchange | Awkward for humans | Parser or viewer |
| Standalone HTML | Reports, prototypes, comparisons, investigations | Harder to diff; must be handled safely | Browser |
| Full web app | Persistent tools and production workflows | Build and maintenance cost | Runtime, dependencies, deployment |
In practice, HTML becomes valuable when the answer has more than one level of detail. A summary can remain visible while logs, SQL plans, or payloads live inside <details> elements. A table can show the result, while JavaScript filters it without another model request.
This is not about making everything colorful. It is about increasing the amount of information a developer can inspect without increasing cognitive load at the same rate.
Start with a constrained HTML artifact
A vague prompt such as “make a dashboard” tends to produce decorative UI with invented metrics. I give Claude Code a contract that specifies the evidence, interaction, and operating constraints.
For example:
Analyze the test results in artifacts/test-results.json.
Create artifacts/test-report.html with:
- one self-contained HTML file
- no CDN assets, network requests, frameworks, or build step
- a summary showing passed, failed, skipped, and total duration
- failures grouped by package
- searchable test names
- collapsible stdout and stack traces
- links to source files using relative paths when available
- a neutral layout that works from 360px to 1440px
- semantic HTML and keyboard-accessible controls
Do not invent missing values. Label unavailable data as "not recorded".
Escape all test output before placing it in the document.
After writing the file, verify that every count matches the source JSON.
The last three instructions matter more than the visual design. Agent-generated reports often fail through plausible fabrication, unsafe interpolation, or inconsistent totals—not invalid CSS.
I also ask Claude Code to separate facts from interpretation. For an incident report, that might mean three explicit sections:
- Observed: values directly present in logs.
- Derived: calculations such as elapsed time or failure rate.
- Hypotheses: possible explanations requiring confirmation.
HTML makes those categories visually distinct, but the underlying epistemic boundary still needs to be in the prompt.
A useful pattern: data first, presentation second
For repeatable workflows, do not let the model bury all extracted facts inside markup. Have it produce structured data, validate that data, and then render HTML.
A compact contract could look like this:
{
"schema_version": 1,
"summary": {
"total": 142,
"passed": 137,
"failed": 3,
"skipped": 2,
"duration_ms": 48120
},
"failures": [
{
"name": "test_refreshes_expired_token",
"file": "tests/auth/test_refresh.py",
"line": 84,
"message": "expected 200, received 401"
}
]
}
Validate invariants before rendering:
import json
from pathlib import Path
report = json.loads(Path("artifacts/report.json").read_text())
summary = report["summary"]
assert summary["total"] == (
summary["passed"] + summary["failed"] + summary["skipped"]
)
assert summary["duration_ms"] >= 0
for failure in report["failures"]:
assert failure["file"]
assert isinstance(failure["line"], int)
Then generate the document with ordinary code. If values can contain user-controlled or tool-generated text, escape them:
from html import escape
def failure_row(item: dict) -> str:
name = escape(item["name"])
location = escape(f'{item["file"]}:{item["line"]}')
message = escape(item["message"])
return f"""
<tr>
<td><code>{name}</code></td>
<td><code>{location}</code></td>
<td>{message}</td>
</tr>
"""
A common gotcha is assuming that “local HTML” means “safe HTML.” Build logs can contain branch names, commit messages, API payloads, and other untrusted strings. Injecting those strings through innerHTML creates the same cross-site scripting problems found in a hosted application. Prefer escaped server-side rendering or DOM APIs such as textContent.
Make the artifact inspectable, not merely attractive
The most effective agent-generated HTML I use has a few consistent properties.
Progressive disclosure
Put the decision-relevant result first, then allow expansion:
<details>
<summary>Request payload and response headers</summary>
<h3>Request</h3>
<pre><code id="request-body"></code></pre>
<h3>Response headers</h3>
<pre><code id="response-headers"></code></pre>
</details>
Long stack traces, raw model responses, query plans, and environment details should not dominate the initial view.
Evidence attached to claims
If a card says “12 endpoints changed,” clicking it should reveal those 12 endpoints. If a performance report says one route is slower, show the compared durations and calculation. Avoid unexplained status badges.
Explicit empty and uncertain states
An empty array may mean “nothing failed,” “collection failed,” or “the field was absent.” Those are different states. Render them differently instead of defaulting all three to zero.
Local, reproducible assets
For disposable reports, inline CSS and small scripts are a good trade-off. External fonts, analytics, CDN scripts, and remote images make the artifact slower, less private, and less reproducible.
For larger tools, however, a single file becomes a liability. Once the HTML contains thousands of lines of embedded JSON and JavaScript, move to a small generated site or a real application with tests.
Verify HTML through a browser loop
Claude Code can inspect source files, but valid source does not guarantee a usable page. Layout overflow, inaccessible controls, and JavaScript errors only become obvious at runtime.
If the project already uses Playwright, ask Claude Code to perform a browser smoke test rather than installing a new stack unnecessarily:
python -m http.server 8000 --directory artifacts
Then test the artifact:
import { test, expect } from "@playwright/test";
test("test report exposes failures", async ({ page }) => {
await page.goto("http://127.0.0.1:8000/test-report.html");
await expect(page.getByRole("heading", { name: "Test report" }))
.toBeVisible();
await expect(page.getByText("3 failed")).toBeVisible();
await page.getByRole("textbox", { name: "Filter tests" })
.fill("refresh");
await expect(page.getByText("test_refreshes_expired_token"))
.toBeVisible();
});
What actually happens when this loop is skipped is predictable: the file looks convincing in source form, but a filter silently fails because an element ID changed, or the summary grid clips on a laptop-sized viewport.
At minimum, verify:
- The browser console has no errors.
- Counts agree with the source data.
- Search and filtering work.
- Controls are reachable by keyboard.
- The page works at narrow and wide viewport sizes.
- The file makes no unexpected network requests.
The same technique works with Claude, GPT, and Gemini APIs
HTML is an output pattern, not a Claude-specific capability. Claude Code makes it convenient because it can inspect a repository, write the artifact, and run local checks. In an API pipeline, I use the model for analysis and structured extraction, then use deterministic application code for validation and rendering.
That division reduces token usage and makes failures easier to diagnose:
logs + schema -> model -> validated JSON -> renderer -> report.html
Do not repeatedly send CSS boilerplate and a large historical report back to the model. Send the relevant source data and request a compact JSON result. Cache stable context, paginate large inputs, and read the actual usage fields returned by the API rather than estimating from character count.
The cost equation is straightforward:
request cost =
(input_tokens / 1,000,000 × input_price)
+ (output_tokens / 1,000,000 × output_price)
For example, if a workflow sends 80,000 input tokens and receives 6,000 output tokens, it consumes 0.08 million input tokens and 0.006 million output tokens per run. Multiply those quantities by the current rates for the selected model. Keeping a 25,000-token HTML template out of every request saves exactly 25,000 input tokens per run, regardless of provider.
Model choice should follow the task. Use a stronger reasoning model for ambiguous incident analysis or architecture comparison; use a faster, cheaper model for schema-constrained classification or summarization. AI Prime Tech can be useful when you want cheaper multi-model API access and need to compare Claude, GPT, and Gemini on the same report pipeline rather than coupling the workflow to one provider.
Where HTML is the wrong choice
HTML is less effective when the output must be consumed primarily by another program, reviewed line by line in Git, or maintained as a long-lived product.
There are also real limits:
- Generated markup can be verbose and produce noisy diffs.
- Inline scripts become difficult to test as complexity grows.
- Large embedded datasets can freeze the browser.
- Visual polish can hide weak or incomplete analysis.
- Opening untrusted artifacts with active JavaScript carries risk.
- A report is a snapshot unless its generation process is reproducible.
Treat the HTML as a view, not the source of truth. Keep the underlying JSON, commands, and generation instructions beside it. For sensitive reports, avoid embedding secrets and consider disabling scripts entirely. If an artifact must render untrusted HTML, isolate it with a restrictive sandbox and Content Security Policy rather than trusting the model to sanitize it perfectly.
Practical takeaways
- Ask Claude Code for self-contained HTML when results are dense, visual, or exploratory.
- Specify the data source, interactions, accessibility requirements, and “do not invent” rules.
- Generate validated structured data before rendering repeatable reports.
- Escape tool output and user-controlled strings; local files are not automatically safe.
- Attach evidence to summaries and distinguish observations, derivations, and hypotheses.
- Use a browser smoke test to catch layout and interaction failures.
- Keep stable HTML templates outside model prompts to reduce token cost.
- Move to a real application when the artifact develops persistent state, multiple users, or substantial business logic.
- Preserve the source data and generator so the report remains reproducible.
HTML is effective here because it is the browser’s native document format, not because an agent turns it into magic. Used deliberately, it gives coding agents a human-scale output channel: richer than a terminal, cheaper than an application, and concrete enough to verify.
One API key for Claude Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5, plus GPT & Gemini — up to 80% off official pricing, pay-as-you-go.
Get Your API Key →