composing-html — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited composing-html (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Produce single-file HTML artifacts without hand-writing the page chrome. The composer supplies <!DOCTYPE>, <head>, inlined CSS, base.js, design tokens, masthead, and colophon. You supply a title and the body content.
The product is the chrome and inventory below — primitives you can drop into any artifact without re-deriving what a card, badge, or eyebrow looks like. Templates are shortcuts on top of this, useful when the same artifact shape repeats; see Templates near the end.
freeform gives you the whole chrome with one content slot — body_html — for the page body. Reach for it first. Reach for a template only when the structure repeats across artifacts (see Templates near the end).
There are two ways to invoke it. Use the `--set` flow for anything with a substantial body — it sidesteps the JSON-string escaping that bites heredoc-style spec writing (newlines, quotes, </& inside multi-line HTML).
--set1. Write the body to a .html file directly (no JSON, no escaping).
2. python scripts/build.py build freeform \
--set title='My Page' \
--set subtitle='Optional subhead' \
--set [email protected] \
--out artifact.html--set KEY=VALUE assigns a literal string; --set KEY=@FILE loads the file contents verbatim into that spec field. Repeat for any field. Works for body_html, extra_css, extra_js, eyebrow, page_class, and the same *_html fields in any other template (summary_html, intro_html, details_html, …).
1. python scripts/build.py describe <template> # required keys + skeleton
2. write spec.json
3. python scripts/build.py build <template> --spec spec.json --out artifact.htmlFor templates with typed slots (pr_review.findings[], slide_deck.slides[], status_report.metrics[]), the spec file is the right shape — the template reasons over the structure. For freeform, the spec is mostly a thin config wrapper around one HTML string; the --set flow above is usually less friction.
You can mix both: small spec.json for metadata, --set [email protected] for the heavy bit. --set overrides any matching field from --spec.
cat > spec.json <<EOF { "body_html": "<multi\nline>\n..." } EOF does not produce valid JSON — JSON strings can't contain raw newlines or unescaped quotes. Either:
--set [email protected] (recommended), orjson.dump(spec, f) so escaping is automatic.Everything in this section is loaded into every artifact via inlined CSS and base.js. Use these tokens and classes inside body_html (or any template's *_html field) without re-declaring them.
| Token | Hex | Use |
|---|---|---|
--ivory | #FAF9F5 | Page background |
--paper | #FFFFFF | Card background |
--slate | #141413 | Headings, inverted background |
--clay | #D97757 | Brand accent (lines, primary actions) |
--clay-d | #B85C3E | Hover/dark variant |
--oat | #E3DACC | Soft contrast surface |
--olive | #788C5D | Success, secondary accent |
--rust | #B04A3F | Errors, destructive |
--moss | #4A6B3A | Success text |
--g100 … --g700 | grays | Surfaces, borders, body text |
Semantic aliases: --ok, --warn, --err, --info.
--serif — display headings (h1, h2, big numerics).--sans — body text (default).--mono — code, eyebrows, badges, captions.--radius-sm (6px) · --radius (10px) · --radius-lg (16px) · --border · --border-soft · --shadow-card · --shadow-pop.
.page — main column (1080px max). Variants: .page--wide (1280px),.page--narrow (720px). Set via the page_class spec key.
.masthead — header strip with .eyebrow + <h1> + .subtitle(auto-rendered from title/subtitle/eyebrow unless show_masthead is false).
.grid .grid--2|3|4|auto — responsive CSS grid..stack, .row — vertical / horizontal flex..card, .card--soft, .card--elev — content containers..rule — <hr> underline below <h2>..colophon — footer strip (auto-added by composer).<div class="eyebrow">SECTION</div> — small all-caps labelwith a leading clay rule.
<span class="badge badge--ok|warn|err|info|clay">v1.0</span>.<span class="kbd">⌘K</span>.<ul class="bullets"><li>…</li></ul> — clay dots.<code> and block <pre><code>. Block code gets acopy button automatically via base.js.
<details><summary>…</summary>…</details> styled.<div class="tabgroup">
<div class="tabs">
<button data-target="a">Tab A</button>
<button data-target="b">Tab B</button>
</div>
<div class="tab-panel" data-id="a">…</div>
<div class="tab-panel" data-id="b">…</div>
</div>base.js wires this automatically and selects the first tab by default.
<div data-sortable="true">
<div draggable="true">…</div>
<div draggable="true">…</div>
</div>Optional cross-zone drops: add data-zone="<id>" to each container.
<input type="range" data-bind="size" min="0" max="100" value="50" data-format="number" data-unit="px">
<span data-out="size"></span>
<style>.box { width: var(--bind-size, 50px); }</style>The CSS custom property --bind-<name> is updated on every input event, and any [data-out="<name>"] element receives the formatted value.
Spend output tokens on content, not chrome:
composer adds all of them. If you find yourself writing a complete page, you missed the skill. <!-- rule:chrome-leak -->
var(--clay),.card, .badge--warn, .bullets, etc. are already loaded. Don't hardcode hex/rgb() colours, inline font-family/font-size, or reference tokens that aren't in the palette. <!-- rule:hardcoded-color rule:inline-typography rule:undefined-token -->
<section>, <h2>,<ul class="bullets"> directly. No translation layer.
user-supplied content yourself. All other string values are HTML-escaped automatically.
After building, lint the artifact before presenting it:
python scripts/build.py check artifact.htmlThe checker is deterministic — no model call, stdlib only. It doesn't grade taste (the fixed chrome already prevents the usual AI tells); it flags content that breaks out of the design system or wires base.js hooks to nothing — the failure modes the chrome can't prevent on its own:
| rule | catches | severity |
|---|---|---|
chrome-leak | <html>/<head>/<link> (and top-level <style>/<script>) in body_html | error |
undefined-token | var(--typo) — a token not in the palette or declared here | error |
broken-tabs | data-target with no matching .tab-panel[data-id] | error |
hardcoded-color | #hex / rgb() literals instead of palette tokens | warn |
inline-typography | font-family / font-size overriding the type stacks | warn |
undefined-token for --bind-* | (allowed — created by data-bind) | — |
nested-card | .card inside .card | warn |
broken-bind | data-bind with no consumer, or orphan data-out | warn |
broken-sortable | data-sortable with no draggable children | warn |
heading-skip | heading levels that jump (h1 → h3) | warn |
img-no-alt | <img> without an alt attribute | warn |
Exit code is non-zero when any error-severity rule fires. The output rules above carry <!-- rule:ID --> anchors tying each guidance line to its check, so the teaching and the enforcement stay in sync. Full-artifact vs body fragment is auto-detected; force with --full / --fragment. --json emits machine-readable findings. Contrast ratios are intentionally not checked — the token pairs are pre-vetted and regex can't judge author-introduced pairs without false positives.
Edit the spec, re-run build, open in a browser. If a layout pattern repeats across multiple artifacts, that's when a template earns its keep — otherwise stay in freeform.
When the same artifact shape recurs (status reports week after week, PR reviews across many PRs, slide decks with consistent navigation), a template's fixed slot map is worth the translation cost. It enforces cross-artifact consistency and skips the layout decisions you'd otherwise re-derive each time.
Use a template only when:
Otherwise: freeform.
1. python scripts/build.py list # all templates, one-line summaries
2. python scripts/build.py describe <template> # required keys + JSON skeleton
3. write spec.json # only your content + parameters
4. python scripts/build.py build <template> --spec spec.json --out artifact.htmldescribe prints a valid-JSON starter skeleton you can edit in place. For worked examples, see references/templates.md — but only after picking a template; reading it cold wastes context.
For templates with prose-heavy *_html slots (e.g. summary_html, intro_html, details_html), the same --set KEY=@FILE mechanism from the freeform workflow applies — load the prose from a .html file rather than escaping it into the JSON spec.
There are 21 templates, grouped into 9 categories plus freeform:
report.* — status_report, incident_reportreview.* — pr_review, code_walkthrough, module_mapeditor.* — triage_board, flag_editor, prompt_tunerdeck.* — slide_deck (arrow-key + space navigation)design.* — design_system, component_variantsexploration.* — comparison_grid, design_directions, implementation_planresearch.* — feature_explainer, concept_explainerdiagram.* — svg_figure_sheet, flowchartprototype.* — animation_sandbox, click_flowSome templates with prose-heavy slots take raw HTML in keys ending with _html (e.g. summary_html, intro_html, details_html). Same rules as freeform.body_html: use the inventory above, escape user-supplied content.
tests/test_smoke.py covers every template with a representative spec plus explicit security regressions (table escaping, script-tag breakout in prompt_tuner, attribute injection in flag_editor, CSS-color injection, spec mutation in module_map). tests/test_checker.py covers the check linter — one assertion per rule (fires on the violation, silent on the clean case). Run with:
python composing-html/tests/test_smoke.py # no pytest required
python composing-html/tests/test_checker.py # no pytest required
python -m pytest composing-html/tests -q # if pytest is availableWhen adding or changing a template, add a spec entry and any regression asserts before merging. When adding a checker rule, add it to both scripts/checker.py and a <!-- rule:ID --> anchor in the relevant guidance line, plus a test assertion.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.