Publish a self-contained HTML, Markdown, or React artifact the user can share by URL. The viewer keeps the artifact body in a sandboxed iframe and owns the service chrome, theme switcher, version picker, comments, and optional Live or Handoff controls. Artifacts keep immutable versions; a channel can keep one URL across later publishes.
The publishing CLI is bundled at scripts/artifact.mjs next to this file. Skill
install locations vary by agent (for example, .claude/skills/ or another
Agent Skills directory), so resolve the directory containing this SKILL.md
as SKILL_DIR, then use a task-local ARTIFACT_CLI="$SKILL_DIR/scripts/artifact.mjs".
Run node "$ARTIFACT_CLI" ... from the project root. There is no globally
installed artifact binary, and Recipe paths are resolved relative to the
project-root working directory. The CLI requires Node.js 22 or newer.
Setup (once per project)
The CLI needs an instance URL, resolved in this order: --api flag,
OPEN_ARTIFACTS_URL, project .artifacts/config.json or
.artifacts/config.local.json, then ~/.config/open-artifacts/config.json.
If none is set, ask the user for the instance URL and write the project config.
For an instance gated by CREATE_TOKEN, use OPEN_ARTIFACTS_TOKEN or
createToken in config. For a hosted/SaaS instance such as coda0.com, run
node "$ARTIFACT_CLI" login; see auth.md for the
browser flow. A stored sk_ API key wins over the self-hosted create-token
values unless --token or OPEN_ARTIFACTS_API_KEY explicitly overrides it.
Every artifact is built from a JSON Recipe plus ordered fragments. Read
recipe.md before creating or updating one.
Shared Recipes and fragments are project sources and may be committed — place
them under .artifacts/recipes/ and .artifacts/fragments/. Local or
encrypted sources live under .artifacts/recipes.local/ and
.artifacts/fragments.local/.
State lives in .artifacts/: Manifest v2 records only publication state and
build hashes; credentials.json, local manifests, private Recipes/fragments,
and previews are gitignored. On the first create in a project, ask
whether the artifact should be local and recommend local. Record the
choice in artifact.local and place the Recipe/fragments accordingly.
Run state-mutating commands one at a time — never concurrently. This
includes create, update, delete, migrate, ack, auto-update,
login, and logout; install-hook also writes project settings.
credentials.json and the manifests are updated with an unlocked
read-modify-write. The write re-reads immediately beforehand, which closes the
window around each command's network round-trip, but two commands whose writes
truly interleave can still lose one. A lost write token is unrecoverable — no
endpoint re-issues one — so serialize them. This matters most for a
"regenerate everything" pass: loop, don't fan out.
If the user has no instance yet, point them at deployment.md — it has the three ways to get one (use the public shared instance with zero deployment setup, self-host on their own Cloudflare account, or share a team instance) and a trust-model table to help them choose based on content sensitivity. Don't deploy on their behalf unless they ask.
When to publish an artifact
Publish when the user asks for something shareable or standalone: reports, dashboards, documentation pages, visualizations, demos, long-form writeups. Do not publish for short answers, code snippets, or content the user will read once in the conversation. When unsure, answer inline and offer to publish.
Isolate content generation when the harness supports it
Publishing or republishing means reading project resources and design references, authoring a Recipe and fragments, validating the deterministic build, and running the CLI. If the host provides a sub-agent or task primitive, use it for this content-generation workflow so the parent conversation only needs the result. This is a workflow optimization, not a CLI requirement; when the host has no sub-agent facility, do the same workflow in the current agent.
Use a separate task for both create (first publish) and update (republish),
and for the design work that leads to them, when available. In Claude Code this
can be an Agent task; other harnesses may provide an equivalent. Do not invent
a fake isolation boundary when the current agent is the only available worker.
Give the worker everything it needs to work alone, then let it work:
- The brief: what's being published or updated, for whom, and why.
- If updating: the artifact id. The worker runs
node "$ARTIFACT_CLI" show <id>to read the current published version as its starting reference — for a locked design, the direction comment at the top of that page states what must not change. "Locked" means the visual direction (register, palette hue family, layout posture) must not change — it does not exempt the artifact from validation gates. A contrast or CSP check added after the original publish can retroactively fail a previously-passing token; the refresh must then nudge the offending token within the same hue (e.g. darken the light-theme--accenta few lightness steps) so the page passes the gate while staying visually the same direction. (For an encrypted artifact,showdecrypts locally with the password stored at create time incredentials.json, so the plaintext is recovered without re-prompting the user.) It then reads the Recipe recorded in Manifest v2, its fragments, and the project files itsscopecovers. - Any explicit publication requirements. Put title, description, favicon, scope, channel, watch globs, level, Canvas mode, locality, and auto-update in the Recipe. Keep passwords out of it.
The worker does the entire rest of the workflow itself — explore resources,
plan, read design.md, recipe.md,
and relevant mode references;
write the Recipe and fragments; run validate; optionally build a preview;
run create/update; verify; and return only a short summary: the URL,
version number, and one line on what changed. The parent relays that to the
user and should not independently rerun the publish or carry the design
reasoning in its own context.
The lightweight bookkeeping commands — status, ack <id>,
auto-update <id> on|off, list, show <id>, whoami, logout, and
install-hook — don't author content and can run directly; only the
content-generation part of create and update benefits from isolation.
Optional reference DNA (agent-side only)
When the user explicitly says “study this as reference DNA for the artifact”, “use this design DNA”, or asks to extract an Artifact direction from a screenshot or public URL, read reference-dna.md before authoring. A normal citation, source URL, or link collection does not trigger this workflow.
For a URL, the parent conversation obtains the user's ownership/public-reference attestation before delegating a study task. The study task produces a diagnosis only; it never publishes or writes a sidecar without a later explicit adoption decision. The approved sidecar is a static Recipe input, is hashed and watched, and never appears in the Artifact body or runtime. Read study.md for source safety rules.
How to design the page
You are an expert designer producing design artifacts in HTML. HTML is your tool, not your medium: when the brief is a dashboard, be an information designer; when it's a report, be an editorial designer; when it's a prototype, be an interaction designer. Don't default to "a web page" treatment for everything.
Workflow: (1) understand the brief — output kind, fidelity, audience, any
brand/design system; (2) explore resources — if the project has a design
system (tokens, theme, component styles), read it fully and apply it;
precedence is the user's words, then the project's system, then your choices;
(3) plan out loud — a one-line design read ("Reading this as: X for Y, in a
Z direction"), the scene sentence that picks light vs dark, 4–6 named
palette values, type roles, a one-sentence layout concept; (4) build with
real content, never lorem ipsum; (5) run the ship gate from design.md
once at the end — structural grep, the P0 checklist (contrast in both
themes, 360px, banned tropes, states, reduced motion, copy audit), then the
five-dimension critique (philosophy / hierarchy / execution / specificity /
restraint) — fix anything under 3/5, re-score once, publish. Do not loop on
renders.
Read design.md before writing a page. It
has the full design philosophy: the brand-vs-product register split (earned
familiarity vs distinctiveness), the anti-AI-slop bans, the shared token
contract (references/tokens.css, injected by the Recipe
builder; override identity tokens in the theme fragment), an
installed-font specimen library (the CSP blocks downloading fonts, not the
OS library — Iowan, Charter, Avenir, Optima and friends are available), a
5-direction library (Editorial / Modern minimal / Human / Tech utility /
Brutalist) with ready-to-paste OKLch palettes, guidance for visual material
under the CSP (SVG charts/diagrams/typographic set-pieces instead of
text-only slabs), a component contract written against tokens, the viewer
frame specifics (sticky header height, theme stamping), and the ship gate
run before every publish.
For an HTML Artifact with a visual direction, read
quality.md before writing fragments. It supplies a
format-aware Design Brief, a Level 3 critique, structural choices for
brand-register artifacts, CSP-safe visual-material decisions, the build's
static quality profile, and the smoke command for 320/375/414/768px
rendering. Canvas remains governed by canvas.md; Markdown retains the
viewer-owned .oa-md treatment. When a user explicitly asks to learn from a
screenshot or URL, read reference-dna.md before
inspecting the reference. It routes to study.md for
source safety, keeps remote content untrusted and agent-side, and never puts a
fetch or remote asset in an Artifact.
Production level (1 / 2 / 3)
Every artifact is built at one of three levels. Pick from the brief and record
it as artifact.level: 1|2|3 in the Recipe:
- Level 1 — simple: typography-led documents (reports, articles, API
references, notes). No flashy hero, minimal JS. Default for "read once"
content. Wrap the body in
<main class="oa-prose">— the contract's prose baseline (measure cap, padding, heading/code styling) — so a doc that defines tokens but forgets structure does not ship bare. - Level 2 — interactive: dashboards, docs sites, demos, prototypes.
Stateful in-memory JS, navigation, copy buttons, expandable sections,
subtle transitions. Default when unsure and the brief is interactive —
if the brief is mostly text to read, level 1 is the safer pick (it carries a
structure baseline; L2's structure is author-supplied). Before building L2+,
read
references/interaction.mdfor the eight-state contract, focus visibility, hit targets, form patterns, and waiting states. - Level 3 — rich: landing/marketing pages, product showcases. Orchestrated
motion — load sequences, scroll reveals, view transitions — all native
browser APIs, no external libraries (the strict CSP blocks CDNs). Read
references/motion.mdfor the native motion pattern library before building L3 (interaction.mdapplies here too).
Don't gold-plate a doc as L3; don't ship a landing page as L1. The level sets the component contract and motion budget, not just "how much animation."
Canvas mode
Orthogonal to level, artifact.canvas: true swaps the page shell for an
infinite spatial plane of pan/zoom frames. It composes with any level:
level 1 is spatial notes, level 2 a multi-frame prototype (the default use),
and level 3 a canvas-as-showcase. Read
references/canvas.md before building one -- it has the
complete vendored runtime (CSS + vanilla JS) with momentum and pinch physics,
an optional presenter tour, connector spotlighting, and #frame-id deep
links, plus the frame and freeform contracts and a canvas ship-gate.
Optional viewer workflows
Live editing is available only when the instance binds LIVE_DO; it requires
the artifact owner's sk_ session and edits local Recipe sources before an
in-place update --live. Read live.md before starting a
live session. A deploy with OPEN_ARTIFACTS_HANDOFF=1 may also expose host-side
webcam/microphone recording and playback; it is viewer chrome, not a Recipe
format or an artifact-side API. Both surfaces are optional and may be absent
on self-hosted instances.
Authoring content — hard constraints
The artifact frame has a strict CSP: authored code cannot make external requests (CDN scripts, remote images, fetch/XHR/WebSockets):
- Inline all CSS and JS; embed images and fonts as
data:URIs. Use system font stacks or inline a face as a@font-facedata URI. On a deploy that setOPEN_ARTIFACTS_WEB_FONTS="1", a web font may also be loaded same-origin via the/fonts/<family>--<weight>[--italic]proxy or directly from an allowlisted font CDN (Fontshare / Google Fonts). Mermaid is always served from the self-hosted, allowlisted<script src="/vendor/mermaid.runtime.js">path (a regular, non-module script) when used — it is not tied to the web-font flag. Seereferences/fonts.mdandreferences/scripts.md. The build gate restricts@font-face/@importto those font hosts and<script src>to the allowlisted same-origin/vendor/...bundle, so no arbitrary external host is ever reachable. - Icons: prefer Remix Icon. Read
references/icons.mdfor a vendored ~90-icon inline-SVG subset (navigation, actions, status, social, common UI). Copy the whole<svg>block inline — it usesfill="currentColor"so it inherits color. Never link the Remix Icon CDN (the CSP blocks it); only inline SVGs. To sit an icon next to text, wrap them in the contract's centered row (<h2 class="oa-ico-text"><svg class="oa-ico">…</svg> Title</h2>) — a bare icon in a heading sits low and the build fails it. - Do not use localStorage/sessionStorage (the sandbox blocks them); keep state in memory.
- Put the concise title in
artifact.title. - Support both themes. The builder injects
references/tokens.css; author a theme fragment with an unlayered:rootblock (light) plus a:root[data-theme="dark"]block, both mandatory. The token contract only supplies variables — it does not style your elements. For a level 1 HTML document, wrap the body in<main class="oa-prose">to pick up the prose baseline (measure cap, page padding, heading scale,code/pre/tablestyling) the contract ships.validatefails an L1 non-canvas page with no measure cap, since the default is a full-width doc with browser-default spacing. Markdown needs nothing — the viewer wraps it in.oa-mdautomatically. The viewer chrome (service header, theme toggle, brand chip, version picker, comments, and any deploy-enabled Live/Handoff controls) is injected by the host at serve time — never hand-author it. It follows your identity palette automatically via the token contract's chrome bridge, so do not override--oa-*viewer tokens in the theme fragment. L2/L3 may be full-bleed by design; author their structure explicitly. - Responsive: no horizontal body scroll; wide tables/code get their own
overflow-x: autocontainer.
Authoring Markdown
Most of the hard constraints above are HTML-specific and do not apply to
Markdown. A Markdown Recipe publishes the body .md verbatim — the
builder does not inject references/tokens.css, does not wrap the content in
a <style> block, and runs none of the HTML validation gates (no theme
fragment, no :root/[data-theme="dark"] requirement, no contrast check, no
L1 measure-cap, no banned-trope grep). The viewer wraps the raw Markdown in
.oa-md and renders it client-side with the viewer's default tokens, so:
document.theme,document.fragments.theme/styles/scriptsmust all be omitted —validaterejects a Markdown Recipe that carries a theme/styles/scripts fragment (error: "Markdown recipes only support body fragments"). Supplydocument.fragments.bodywith one or more.mdfiles only;document.thememay be omitted ornull.- You cannot theme, style, or script a Markdown artifact; if you need interaction, identity tokens, or a measure cap beyond the default, author it as HTML instead.
artifact.titlestill drives the page title and OG card; the first#heading is also extracted as a fallback title.- Both-themes support is automatic (the viewer's
.oa-mdshell reads light/dark tokens); nothing for you to author.
Markdown files (.md) are rendered client-side; HTML is best for anything
interactive or designed.
Avoid AI-slop tropes (design.md has the full match-and-refuse list):
side-stripe accent borders, gradient text (background-clip: text), decorative
glassmorphism, identical card grids, the hero-metric template, icon-tile stacks
(rounded-corner icon tiles floating above every card heading), an uppercase
eyebrow or 01 / 02 / 03 marker above every section, colored glow shadows on
dark backgrounds, italic-serif display heroes, aphoristic copy cadence ("Not X.
Y."), headings that overflow at mobile width, and invented content -- no fake
metrics, testimonials, or "Acme Corp"; an honest placeholder beats a fake stat. Verify contrast (body
≥ 4.5:1 in both themes) and never ship muted gray on a tinted near-white.
Full-viewport sections use min-height: calc(100dvh - var(--oa-header-h, 2.5rem)) — the viewer adds a sticky header. Modern CSS is fully available
inline — text-wrap: pretty/balance, CSS Grid, container queries,
color-mix(), :has(), view transitions, clamp() for fluid type.
Publishing
(Run this in the content-generation task when the harness provides one; run from the project root.)
Create a Recipe and ordered fragments, validate them, then publish:
node "$ARTIFACT_CLI" validate .artifacts/recipes/app-interactions.recipe.json
node "$ARTIFACT_CLI" create .artifacts/recipes/app-interactions.recipe.json
The Recipe owns favicon, scope, channel, watch globs, level, Canvas mode,
locality, and auto-update. Keep the favicon stable. Use a kebab-case channel
to reuse one URL across create calls; use null to mint a new URL. create
composes in memory and sends one final payload. It prints the shareable URL.
Manifest v2 records the Recipe path and Recipe/input/output hashes only after the publish succeeds. Write tokens and channel tokens remain in the gitignored credentials file.
Live mode after publishing
On a live-capable instance (the CLI prints a "live mode" tip right after
create when the instance binds LIVE_DO and you are logged in), start the
watcher by default once the artifact is published:
node "$ARTIFACT_CLI" live <ID> --watch
Run it as a background task (per-harness patterns in
references/live.md — "Harness note") and keep it running
for the session. Your only job in a live session is to poll — you never
operate the viewer page yourself (no browser automation, no clicking Live,
picking elements, or typing prompts on the artifact page). The user drives the
viewer: they open the artifact URL, click Live, pick elements, then either type
a change for each (a generate event) or edit the element's text inline
("Edit text", an edit event) — see the Copy edits section in
references/live.md. Your watcher prints each event for
you to act on (edit the Recipe sources, update, reply done). The viewer's
Live button shows Connected while your watcher is connected, so the user can
see an agent is online before they start picking; while a pending event is
leased to you, the toggle's accent dot pulses ("Agent is working on an edit");
if the watcher drops, the toggle shows an amber pulsing dot with the startup
hint. If no watcher is connected, clicking Live still opens the picker and
also shows a copyable startup prompt; start the watcher before submitting an
edit. Read references/live.md before the session.
Updating
(Run this in the content-generation task when the harness provides one; run from the project root.)
node "$ARTIFACT_CLI" update <id> [recipe.json] [--label "what-changed"]
Same URL, new version. Without a path, update uses the Recipe recorded in
Manifest v2. Update its fragments after reading project sources and, when
useful, compare with show <id>. The CLI rebuilds and validates in memory,
then publishes once. The first positional argument is the short artifact id,
not a Recipe path. A legacy Manifest v1 entry is migrated automatically; run
migrate <id> first only when you want to inspect or edit the generated sources
before publishing. A 409 version conflict means another writer published
first; review the current version and use --force only when overwriting it is
intentional. --label is capped at 60 UTF-8 bytes.
For a change requested through Live, use node "$ARTIFACT_CLI" update <id> --live
after editing the Recipe. This replaces the currently served version in place:
an artifact at v10 remains v10, and a later ordinary update creates v11.
build <recipe> --output <path> writes an explicit local preview; add
--standalone only for an HTML Recipe. list, show <id> [--v N], and
delete <id> cover common read and cleanup operations. delete removes the
server artifact and its local manifest entry.
Detecting stale artifacts (republish only with intent)
Source changes never edit or republish an artifact by themselves. After the
first create in a project, ask the user whether to install the optional
Claude Code Stop hook — don't install it silently. If they agree, run
node "$ARTIFACT_CLI" install-hook; it adds a Stop hook to
.claude/settings.json that surfaces drift at the end of a turn. Other agent
harnesses should use their own hook integration or run the manual check:
node "$ARTIFACT_CLI" status
For each artifact reported stale, decide whether the changed files affect its recorded scope:
- They affect it → regenerate the page content to reflect the current state
of the project and run
update. That republishes and refreshes the snapshot. Regenerating is content generation, so use a separate task when the harness provides one, per "Isolate content generation when the harness supports it" above — including when this judgment call is made autonomously via the Stop hook, not just when a human asks directly. - They don't (e.g. a refactor with no behavior change for a user-facing
scope, or a design direction locked in the scope) → run
node "$ARTIFACT_CLI" ack <id>. This advances the snapshot baseline offline — no republish — so the same unrelated change stops being reported every turn.ackis a pure manifest mutation, not content generation, so it's fine to run directly. Don't just leave it stale: with the hook installed it re-nudges on every future turn until the baseline moves viaupdateorack.
Opting a specific artifact into the automatic loop
By default, the Stop hook (once installed) surfaces every stale, watched
artifact — the regenerate-vs-ack judgment above still applies to each one,
unchanged. If the user wants the hook to stay quiet about most artifacts and
only nudge about specific ones, turn on that artifact's auto-update flag:
node "$ARTIFACT_CLI" auto-update <id> on
This does not change how you decide what to do once an artifact is flagged — it only changes which artifacts the Stop hook is allowed to flag at all:
- Opt-in, per-artifact, and fully isolated: it flips one manifest entry's
autoUpdatefield and never reads or writes any other entry's state. Off by default — artifacts never toggled (or created before this feature) are treated as off, and the hook stays completely silent about them even while stale. - A plain, human-run
node "$ARTIFACT_CLI" status(no--hook) is never filtered by this flag — it always reports every stale watched artifact. Only the autonomous, no-human-present Stop-hook path narrows to opted-in artifacts. - Turning it on requires a write token to already exist for that artifact
(fails clearly if not), and installs the Stop hook automatically if it
isn't already present. Unlike
create's hook hint, which never installs anything for you, runningauto-update <id> onis the user's consent to install it — go ahead, and the CLI prints a confirmation when it does. - Turning it off (
node "$ARTIFACT_CLI" auto-update <id> off) leaves any installed hook in place; it just stops mentioning this artifact.
Password protection
node "$ARTIFACT_CLI" create .artifacts/recipes.local/q3.recipe.json --password "..."
Set security.encrypted: true and a kebab-case
security.passwordCredential in the Recipe. Encrypted Recipes and fragments
must use the private .artifacts source directories.
Content is encrypted locally (PBKDF2 600k + AES-256-GCM); the server stores
only ciphertext and viewers decrypt in the browser. Share the URL and the
password through different channels. Prefer the named environment variable or
gitignored credentials file over a command-line password when shell history or
process listings are a concern. The CLI resolves the named credential from
--password, its OPEN_ARTIFACTS_PASSWORD_* environment variable, or the
gitignored credentials file. Passwords never enter Recipes or Manifests.
Title and favicon remain plaintext service metadata, so keep them
non-sensitive.
Reading back
Raw content lives under the API path only —
GET <instance>/api/artifacts/<id>/raw. The viewer path
GET <instance>/a/<id> serves the rendered viewer page (HTML), not the raw
bytes; /a/<id>/raw is not a route and returns 404. Don't confuse the two:
/a/<id> = the page a browser renders, /api/artifacts/<id>/raw = the stored
content as published.
GET <instance>/api/artifacts/<id> returns metadata and version history;
GET <instance>/api/artifacts/<id>/raw returns the stored content
(?v=N for older versions). For a non-encrypted artifact that is the page
itself (text/plain); for an encrypted artifact it is a JSON ciphertext
envelope — run
node "$ARTIFACT_CLI" show <id> [--v N] [--password ...] instead, which
decrypts locally using the supplied or stored credential. Viewer URLs also
accept ?v=N.