Skip to main content
claude-artifact-cli: publish Claude Artifacts from your terminal

claude-artifact-cli

Publish, update, pull and inspect Claude Artifacts from the terminal, and work with their comments, database, assets, types and sharing, using the Claude Code login already on your machine.

PyPI Python CI License Stars

What is this?

claude-artifact publishes an HTML page (plus any CSS, JS and images it uses) as a Claude Artifact and prints the link, and pulls an artifact's files back down to edit and republish. It also reads and answers comment threads, reads and writes an artifact's database, manages its uploaded assets, starts artifacts from types (Slides, Docs, Dashboard, …), declares runtime capabilities, and handles sharing, renaming, pinning, history and deletion. It talks to the same API the Claude Code Artifact tool uses and reuses the claude.ai login from claude /login. No API key, no config file, no dependencies beyond Python 3.10+.

Use it where the built-in tool can't reach: other coding agents (Droid, Codex, Cursor, OpenCode), subagents, shell scripts, Makefiles, git hooks and CI jobs.

Quick Start

uv tool install claude-artifact-cli     # or: pipx install claude-artifact-cli
claude-artifact whoami                  # confirms the Claude Code login works
claude-artifact publish report.html --favicon 📊
# https://claude.ai/code/artifact/1c5586a4-acec-4266-a9aa-03dfa0912b43

Run it once without installing:

uvx --from claude-artifact-cli claude-artifact publish report.html

Commands

claude-artifact publish FILE|DIR [options]   publish or update an artifact
claude-artifact pull SLUG_OR_URL [DIR] [--force] [-q]
                                            download the artifact's files into DIR
claude-artifact status [PATH] [--slug S | --url U]
                                            compare local files with what's live
claude-artifact list [--scope mine|shared|all] [--limit N]
claude-artifact read SLUG_OR_URL [-o FILE]  metadata + published file manifest
claude-artifact read SLUG_OR_URL --path P [--path P ... --out-dir DIR]
                                            a published file's exact bytes
claude-artifact versions SLUG_OR_URL        version history (* marks live)
claude-artifact rename SLUG_OR_URL TITLE
claude-artifact pin|unpin SLUG_OR_URL
claude-artifact delete SLUG_OR_URL --yes    delete for everyone; no undo
claude-artifact share SLUG_OR_URL [--private | --org | --add UUID ... [--access view|comment] | --remove UUID ...]

claude-artifact comments list SLUG_OR_URL [--all | --awaiting] [--full]
claude-artifact comments reply SLUG_OR_URL THREAD TEXT|- [--id UUID]
claude-artifact comments resolve SLUG_OR_URL THREAD ...
claude-artifact watch SLUG_OR_URL [--interval S] [--count N] [--no-comments]

claude-artifact db whoami|get|list|query|set|update|delete|edit|batch SLUG_OR_URL ...
claude-artifact assets list|upload|get|delete|copy SLUG_OR_URL ...

claude-artifact types list | show TYPE | read TYPE PATH | instances TYPE
claude-artifact create TYPE [--title T]     new artifact from a type (slides, docs, ...)
claude-artifact contract [VERSION] [--prompt]
                                            runtime capabilities and the page-author guide

claude-artifact whoami                      check that auth works
claude-artifact update                      upgrade to the latest release now
claude-artifact --version

The URL goes to stdout and progress goes to stderr, so URL=$(claude-artifact publish page.html -q) captures only the link. Add --json to any command that prints data for raw output. Exit codes: 0 success, 1 API error, 2 auth error, 3 version conflict (HTTP 409: a newer artifact version, or a database document that changed since --if-version), 130 interrupted.

Wherever a command takes a slug or URL (SLUG_OR_URL, --slug, --url), it accepts any of:

  • https://claude.ai/code/artifact/<uuid>
  • https://claude.ai/artifact/<short id>, the form claude.ai and Claude Code's Artifact tool show
  • a bare UUID or a bare short id

publish

# new artifact; the title comes from the page's <title>
claude-artifact publish dashboard.html --favicon 📊 --description "Q3 numbers"

# update in place, same URL; published files you don't pass are kept
claude-artifact publish dashboard.html --url https://claude.ai/code/artifact/<slug>
claude-artifact publish dashboard.html --slug <slug>

# update, and make these files the whole artifact (anything else is removed)
claude-artifact publish dashboard.html --slug <slug> --mode replace --file app.css

# multi-file page
claude-artifact publish index.html \
  --root ./site \
  --file app.css --file app.js --file img/logo.png

# rename a file's published path, and drop one
claude-artifact publish index.html --slug <slug> \
  --file styles.css=src/main.css \
  --remove old.js
Option Meaning
--slug, --url Update this artifact. Without one, a publish creates a new artifact (a directory with .artifact.json updates its own).
--title Defaults to the page's <title>, else the file or directory name (for a typed directory, the title saved in .artifact.json).
--favicon Emoji for the tab. Set it on a first publish.
--description One-sentence subtitle.
--icon, --label Generic icon word; short name for this publish.
--file PUB[=SRC] Supporting file. Without =, published and source paths match. Repeatable. Not allowed with a directory.
--root Base directory for --file sources. Published paths are unchanged. Not allowed with a directory.
--remove PATH Delete a published file. Needs an existing artifact and patch mode; with no --mode, --remove uses patch.
--mode patch|replace patch overlays the manifest onto the base version, so published files you don't pass are kept. Default when updating a page. replace makes the manifest the whole artifact, so any published file you leave out is removed. Default for a new artifact and for the first publish of a directory. A directory with .artifact.json patches (see below).
--base-version The version this publish was made against. If a newer version is live, the publish is refused with exit code 3. Defaults to the version in a directory's .artifact.json (when publishing to the slug saved there). Without one, patch uses the live version and replace is not checked, so a concurrent publish goes undetected.
--force Publish even if a newer version is live, discarding it.
--capability NAME[=JSON] Declare a runtime capability the page uses: db, user, assets, comments, artifact, downloads, … with its config (default {}), e.g. --capability 'db={"rules":[…]}'. Repeatable. The set you pass replaces the stored declaration; a publish without --capability keeps it. Names are checked against the contract. claude-artifact contract --prompt documents each one.
--clear-capabilities Drop every declared capability.
--contract VERSION Pin the runtime contract, or latest to upgrade. Default: the artifact's stored contract; a declaration on an artifact without one uses the latest.
--copy SOURCE PUB[=SRC] Have the server copy published file SRC (default PUB) from artifact SOURCE into this one at PUB. Nothing is downloaded or re-uploaded. Repeatable. The server copies scripts, styles, data, fonts and images, not HTML or XML.
--note TEXT A note recorded with this version; versions shows it.
--thumbnail IMAGE, --thumbnail-dark IMAGE Gallery thumbnails.
-q, --quiet Print only the URL.

--json prints the deploy response with the artifact url added: {"url", "slug", "version", …}.

Publish a directory

claude-artifact publish ./site --favicon 📊   # new artifact; writes ./site/.artifact.json
claude-artifact publish ./site                # later: updates the same artifact

Publishes every file under the directory, recursively, skipping dotfiles and dot-directories. index.html is required at the top level (except for a typed artifact, below); symlinks are refused. The first publish of a directory replaces: the directory is the whole artifact. Once .artifact.json lists its files, publish DIR patches: it sends the directory's files and removes the ones deleted locally since the last pull or publish, leaving files the directory never held (dot paths, a type's files) alone. --mode replace forces a full replace.

After each successful publish (and each pull) the CLI writes DIR/.artifact.json:

{
  "slug": "…",
  "url": "https://claude.ai/code/artifact/…",
  "version": "…",
  "title": "…",
  "files": { "index.html": "<sha256>", "app.css": "<sha256>" }
}

files maps each published path to the sha256 of what was published or pulled; pull uses it as the merge base. It is never published (dotfile). The next publish DIR updates that slug without --slug and sends the saved version as the base version, so if someone else published in between, the publish fails with exit code 3 instead of overwriting their version. Commit it or ignore it as you prefer.

Typed artifacts. An artifact made from an Artifact type (Slides, Design, …) holds files the type supplies, index.html among them. They stay fixed and pull leaves them out, marking the directory "typed": true in .artifact.json. publish DIR on a typed directory patches only the artifact's own files: no index.html is needed, a local index.html is refused, --mode replace is refused, and a file deleted locally since the last pull or publish is removed from the artifact. Start from pull; the directory must be linked to the artifact.

pull

claude-artifact pull <slug-or-url>              # into ./<slug>
claude-artifact pull <slug-or-url> ./site
claude-artifact publish ./site                  # after editing: same artifact, pulled version as base

Downloads the live version's files into DIR (default: a directory named after the slug), writes DIR/.artifact.json, and prints DIR on stdout. stderr reports the version, files written and removed, local edits kept, and files left out: those the artifact's type supplies, and dot paths such as .well-known/…, which directories never hold (read --path still fetches them). -q silences it.

Pulling into a directory already linked to the same artifact merges file by file against the hashes in .artifact.json:

Local Live Result
unchanged changed live version written
changed unchanged local edit kept
unchanged deleted file removed, with any directory left empty
untracked not published kept
changed changed conflict

A file added or deleted counts as changed.

A conflict lists the files and writes nothing. To resolve it, copy your versions of those files out of the directory, pull again with --force (it takes the live version of conflicting files and still keeps your other edits), merge your changes back in, and publish. Or pull into another directory to compare by hand. A non-empty directory that isn't linked to this artifact is refused unless --force, which writes the artifact's files over it and leaves other files alone.

status

claude-artifact status                          # current directory, slug from .artifact.json
claude-artifact status ./site
claude-artifact status page.html --slug <slug>  # compares page.html with the published index.html only
claude-artifact status ./site --json
https://claude.ai/code/artifact/<slug>
  live version  1789541006-9f63
  base version  1789540112-04aa  (someone published since - merge first)
  same          app.css
  local-only    img/new.png
  changed       index.html
  remote-only   old.js

Compares the sha256 of each local file with the published manifest; for older artifacts whose manifest has no sha256, it fetches the file to compare. Files an Artifact type supplies, and dot paths, are ignored: directories never hold them. The base version line appears only for a directory with .artifact.json, and reads (up to date) when it matches the live version. When anything differs, stderr suggests merge the live version in: claude-artifact pull <slug> DIR for a linked directory, else live copy: claude-artifact pull <slug> <another-dir>. --json prints {"slug", "url", "live", "base", "behind", "files": [{"state", "path"}]}. PATH defaults to .; --slug/--url override .artifact.json.

On exit code 3 from publish:

  • Directory: claude-artifact pull <slug> DIR merges the live version in, resolve any conflicts, then claude-artifact publish DIR.
  • Page: compare with status, merge the live changes by hand, then publish again with --base-version <live>.

Either way, --force publishes anyway and discards the other version.

list and read

claude-artifact list --scope shared
claude-artifact read <slug-or-url>          # title, version, role, file manifest
claude-artifact read <slug> -o meta.json   # also save the metadata JSON

claude-artifact read <slug> --path index.html > index.html            # one file's exact bytes
claude-artifact read <slug> --path index.html --path app.css --out-dir ./out   # saves, prints the paths

read returns metadata, paths, sizes, content types and sha256 hashes. Short-lived tokens in responses (assetToken, subscriptionToken, and __frame_t= on list thumbnail URLs) are replaced with [redacted] before they reach stdout or -o.

read --path P prints the published file's exact bytes to stdout instead, checked against the manifest's sha256. Repeat --path with --out-dir DIR to save several files under DIR at their published paths; it prints each saved path. --path doesn't combine with --json or -o, more than one --path needs --out-dir, and --out-dir needs --path.

read also shows the artifact's runtime contract and declared capabilities. list --pinned shows only pinned artifacts.

Never WebFetch or curl a claude.ai artifact URL: it is a login-walled app shell and returns no page content.

Comments

claude-artifact comments list <slug>             # open threads, with authors and anchors
claude-artifact comments list <slug> --awaiting  # threads a person sent to Claude that have no answer yet
claude-artifact comments reply <slug> <thread> "Fixed in the latest version."
echo "Long answer" | claude-artifact comments reply <slug> <thread> -
claude-artifact comments resolve <slug> <thread>

list shows open threads; --all adds resolved ones and --full keeps long comments whole. Each thread prints its id, its state (resolved, sent to Claude, awaiting Claude), what it is anchored to, and every comment.

reply posts as Claude, and only on a thread a person sent to Claude ("Send to Claude" on the artifact's page); anything else is refused, as it is for Claude Code's Artifact tool. When the thread is waiting on Claude, the reply marks that request answered. Each reply carries a client id (--id, default a fresh UUID); resending the same id posts nothing new, so a retry after a network error is safe. resolve works on the same threads. Starting a thread, and replying or resolving as yourself, need a claude.ai browser session: do those on the artifact's page.

Watch

claude-artifact watch <slug>                     # prints events until Ctrl-C
claude-artifact watch <slug> --json --count 1    # wait for the next event, print it as JSON, exit

Polls every --interval seconds (default 30) and prints one line per new version, new comment ([to Claude] when a person sent it to Claude) and resolved thread. --json prints one JSON object per line: {"event": "version", "version", "previous"}, {"event": "comment", "thread", "comment", "author", "text", "to_claude", "awaiting_claude"} or {"event": "resolved", "thread", "at"}. --no-comments watches versions only. Publishing identical content creates no new version, so it prints nothing.

Database

An artifact's database holds shared and per-person JSON documents its page reads with claude.use("db"). The page's published version must declare the capability: publish … --capability db --capability user.

claude-artifact db set    <slug> tasks t1 --data '{"title": "Ship it", "done": false}'
claude-artifact db update <slug> tasks t1 --data '{"done": true}' --if-version 1
claude-artifact db get    <slug> tasks t1
claude-artifact db list   <slug> tasks --all
claude-artifact db query  <slug> tasks --where done == false --where n '>=' 2 --order-by n:desc
claude-artifact db edit   <slug> notes n1 --field body --old "teh" --new "the" --replace-all
claude-artifact db delete <slug> tasks t1
claude-artifact db batch  <slug> @writes.json    # [{"op":"set","collection":"tasks","doc_id":"t2","data":{…}}, …]
claude-artifact db whoami <slug>                 # your id, as in data/users/<id>
Rule
Paths A collection is 1-15 /-separated segments with an odd count (tasks, boards/b1/columns); a document is collection + doc id. Segments use letters, digits and _ - . ~ : @ +.
Per-person data data/users/<id> is private to that person, even from the owner. me stands for you: db get <slug> data/users/me prefs.
set / update set replaces the document; update merges fields, and {"__delete__": true} removes one. --data takes JSON, - for stdin, or @FILE.
--if-version N The write applies only if the document is still at version N. Otherwise nothing is written and the command exits 3.
edit Replaces text inside one string field without resending the document.
query --where FIELD OP VALUE with == != < <= > >= in not-in array-contains. VALUE is parsed as JSON when it can be, else taken as a string.
Paging list and query print --cursor for the next page; --all follows every page.
batch 1-50 set/update/delete writes, {op, collection, doc_id, data?, if_version?}, each document at most once, up to 1 MiB, in one request.

get prints the document's data (stderr has its version); a missing document exits 1. List and query print id, v<version> and the data, tab-separated.

Assets

The asset store holds files a page uploads or references by id: images, SVG, video, PDF, fonts, CSS/JS, CSV, Markdown, JSON and text, up to 20 MiB each (SVG 2 MiB, sanitized). Declare --capability assets for the page to use them.

claude-artifact assets upload <slug> logo.png chart.svg    # prints id, url, file
claude-artifact assets list <slug>
claude-artifact assets get <slug> <id> -o logo.png        # bytes, checked against the sha256
claude-artifact assets copy <slug> --from <other-slug> <id> ...   # 1-10 ids; prints old -> new
claude-artifact assets delete <slug> <id>

A page serves an asset at /_blob/<id>. --type overrides the content type taken from the extension.

Types

Artifact types (Slides, Docs, Dashboard, Design, Motion, …) are ready-made pages that take your content as data.

claude-artifact types list
claude-artifact types show slides                 # release and the files it ships
claude-artifact types read slides SKILL.md         # how to fill one
claude-artifact create slides --title "Q3 review"  # prints the new artifact's URL
claude-artifact pull <new-slug> ./deck              # then add your files, publish ./deck

TYPE is a title (case-insensitive), slug or URL. create sends an idempotency key (--idempotency-key to reuse one), so a retried create doesn't make a second artifact. types instances TYPE lists artifacts made from a type.

Contract and capabilities

claude-artifact contract             # latest runtime contract and its capability names
claude-artifact contract --prompt    # the guide for page authors: claude.use(...), each capability's config

Sharing and housekeeping

claude-artifact share <slug>                    # audience, people, viewers, commenters
claude-artifact share <slug> --org              # everyone in your organization
claude-artifact share <slug> --private          # only you
claude-artifact share <slug> --add <account-uuid> --access view
claude-artifact share <slug> --remove <account-uuid>
claude-artifact versions <slug>
claude-artifact rename <slug> "New title"       # recorded as a new version
claude-artifact pin <slug>
claude-artifact delete <slug> --yes

Sharing is read-modify-write: a change made by someone else in between is refused rather than overwritten. People are named by claude.ai account UUID (owner_account in list --json, author.account in comments for org members); there is no lookup by email. delete refuses without --yes.

Updates

A uv tool or pipx install keeps itself current. At most once a day, a command checks PyPI in the background (2-second timeout, never in CI). When a newer release is out, it starts uv tool upgrade claude-artifact-cli (or pipx upgrade claude-artifact-cli) in a detached process after the command finishes and says so on stderr; -q keeps that quiet. The upgrade's output goes to ~/.config/claude-artifact-cli/update.log. A plain pip install isn't changed behind your back: it prints a notice instead. A failed check or upgrade never changes the command's output or exit code.

claude-artifact update     # upgrade now, in the foreground
Variable Effect
CLAUDE_ARTIFACT_NO_AUTO_UPDATE=1 Check and print a notice, but never upgrade.
CLAUDE_ARTIFACT_NO_UPDATE_CHECK=1 No check at all. CI set does the same.

Auth

The token is looked up in this order:

  1. --token
  2. $CLAUDE_ARTIFACT_TOKEN, then $CLAUDE_CODE_ARTIFACTS_API_TOKEN
  3. macOS Keychain, generic password service Claude Code-credentials
  4. ~/.claude/.credentials.json

It must be the claude.ai OAuth token from claude /login. The artifacts routes reject an ANTHROPIC_API_KEY with a 401. The token is short-lived; Claude Code refreshes it on launch and this tool re-reads it on every run, so there is nothing to rotate. whoami shows only the token's length and last few characters.

Agent skill

skills/claude-artifact-cli/SKILL.md teaches any coding agent (Droid, Codex, Cursor, OpenCode, Claude Code subagents) when and how to use the CLI. Install it in the shared agent skills directory:

mkdir -p ~/.agents/skills/claude-artifact-cli
curl -fsSL https://raw.githubusercontent.com/nikships/claude-artifact-cli/main/skills/claude-artifact-cli/SKILL.md \
  -o ~/.agents/skills/claude-artifact-cli/SKILL.md

Troubleshooting

Symptom Cause
401 unauthorized An API key instead of the OAuth token, or an expired login. Start Claude Code once or run claude /login.
removing a file needs --mode patch --remove with --mode replace. In replace mode, leave the file out instead.
removing a file needs --slug/--url --remove on a new artifact.
404 … isn't yours to update Wrong slug, deleted artifact, or another org.
HTTP 409: a newer version (live version …), exit code 3 Someone published since your base version. For a directory, claude-artifact pull <slug> DIR, resolve conflicts, publish DIR. For a page, status, merge, republish with --base-version <live>. Or pass --force.
not a valid artifact slug or URL Not one of the accepted forms (see Commands).
CSS, JS or images gone after an update Published with --mode replace (or with 0.1.x, where replace was the default) without passing them. Republish them.
no index.html in DIR A directory publish needs index.html at its top level. For an artifact made from a type, pull it into the directory first.
… is a symlink; copy the file in instead Directory publishes and pulls refuse symlinks.
changed both locally and in the live version: pull conflict; nothing was written. Resolve as under pull.
… is not empty and was not pulled from or published to this artifact pull into an unrelated directory. Pick another, or pass --force.
… exists and is not a directory pull target is a file.
… is a symlink; refusing to write through it A symlink on the path of a file being written. Remove it.
… is not a clean relative path, refusing to write unsafe path A published path with .., an empty segment, a backslash, % or a control character. The CLI won't read or write it.
version … was published while reading … A publish landed mid-read. Run the command again.
… doesn't match its published sha256 The bytes read don't match the manifest. Run the command again.
no file '…' in version … Not a published path. claude-artifact read <slug> lists them.
this login can't read the artifact's files (the server issued no asset token …) No read access to the bytes, e.g. a public artifact from another organization.
--path writes file bytes; it doesn't combine with --json or -o Drop --json/-o; use --out-dir to save.
more than one --path needs --out-dir, --out-dir goes with --path As stated.
an artifact made from a type can only be patched --mode replace on a typed directory. Leave --mode off.
…/index.html belongs to the artifact's type and can't be published; remove it A typed directory holds an index.html. Delete it.
429 … daily publish cap Plan limit. Resets at UTC midnight.
Claude can only reply on a thread a person sent to Claude comments reply/resolve on a thread without "Send to Claude". Ask the person to send it to Claude, or answer on the page.
this artifact's published version doesn't declare the db capability Republish with --capability db --capability user.
the document changed since --if-version, exit code 3 Someone wrote the document since you read it. db get it again and redo the write.
… names a document, not a collection A collection path needs an odd number of segments. Split off the last segment as the doc id.
contract … has no capability … A misspelled --capability. The error lists the valid names.
deleting … can't be undone; pass --yes delete needs --yes.

The protocol

Reverse-engineered from the Claude Code CLI v2.1.273 binary and checked against v2.1.287, where artifacts are called frames. API calls go to api.anthropic.com, even though the artifact is served from claude.ai; file bytes come from a per-artifact content host.

POST /api/frame/deploy/direct     publish
POST /api/frame/deploy/prepare    content-hash preflight (bodies over ~15 MB)
POST /api/frame/upload            stage blobs the preflight asked for
GET  /api/frame/{slug}?via=model_read   boot: metadata, file manifest, assetToken
GET  /api/frame/read/{slug}       ownership, stored contract and capabilities
GET  /api/frame/frames?limit=N    list
GET  /api/frame/versions/{slug}   version history
POST /api/frame/retitle/{slug}    rename                    {"title"}
POST|DELETE /api/frame/favorite/{slug}   pin / unpin
DELETE /api/frame/{slug}          delete
GET|PATCH /api/frame/perm/{slug}  sharing (PATCH needs If-Match: <version from GET>)

GET  /api/frame/comments/{slug}?names=1                     threads
POST /api/frame/comments/{slug}/{thread}                    reply as Claude
POST /api/frame/comments/{slug}/{thread}/session-resolve    resolve
POST /api/frame/db/agent                                    database
POST /api/frame/blob/{slug}/agent-upload|agent-list|agent-copy, /{id}/agent-delete   assets

GET  /api/frame/types[/{slug}[/files/{path}|/instances]]    types
POST /api/frame/types/{slug}/create?idempotency_key=K       new artifact from a type
GET  /api/frame/contract/{latest|version}[/prompt]          runtime contract

GET  https://{slug}.frame.claudeusercontent.com/_f/{ver}/_src/{path}?__frame_t=<assetToken>
                                  published source bytes (see Reading files)
GET  https://{slug}.frame.claudeusercontent.com/_f/{ver}/{path}?__frame_t=<assetToken>
                                  served copy
GET  https://{slug}.frame.claudeusercontent.com/_f/{ver}/_blob/{id}?__frame_t=<assetToken>
                                  an uploaded asset

Every route above answers the OAuth bearer directly. Claude Code routes some of them through a relay in cloud sessions; on a local machine it calls them directly too.

Headers on api.anthropic.com calls:

Authorization: Bearer <claude.ai OAuth access token>
anthropic-beta: oauth-2025-04-20
Accept: application/json, application/vnd.ant.frame-refusal+json
X-Frame-CP: go
X-Frame-Surface: code
X-Frame-Platform: cli
X-Frame-Client-Version: 2.1.287

Only Authorization and anthropic-beta are load-bearing.

Deploy body

{
  "slug": "…",                       // omit to create a new artifact
  "title": "My Page",
  "favicon": "📊",
  "description": "…",
  "mode": "replace",                 // or "patch" (requires baseVersion)
  "baseVersion": "1789541006-9f63",  // refused with 409 if a newer version is live
  "force": true,                     // publish anyway
  "manifest": {
    "index.html": { "content": "<!doctype html>…", "contentType": "text/html" },
    "logo.png":   { "sha256": "<64 hex>",          "contentType": "image/png" },
    "old.js":     null
  }
}

A manifest value with content inlines the file; one with sha256 references a blob already staged via /upload; null deletes that path (patch mode only).

Wire encoding. These content types go as raw UTF-8 strings; everything else is base64: text/html, text/css, text/plain, text/markdown, text/csv, text/javascript, application/javascript, application/json, application/manifest+json, application/xml, text/xml, image/svg+xml.

Optional deploy fields: description, icon, label, note, thumbnail and thumbnail_dark (base64 images), and contract with capabilities ({"db": {}, "user": {}}). capabilities requires contract (HTTP 400: capabilities requires contract); a deploy without capabilities keeps the stored declaration, and {} clears it. A patch needs a non-empty manifest.

Response: {"slug", "version", "manifest", "history", "contract", "capabilities", …}. The page lives at https://claude.ai/code/artifact/<slug>.

Modes and conflicts. replace makes the manifest the whole artifact; patch overlays it onto baseVersion, keeping paths it doesn't name. Both modes honor baseVersion: if the live version is newer, the deploy is refused with

HTTP 409
{"conflict": true, "live": "<live version>", …}

patch requires a baseVersion; when the caller has none, the CLI reads the live ver from GET /api/frame/{slug}?via=model_read and uses that, which cannot detect a concurrent publish. The CLI exits 3 on a 409.

Large publishes

Over ~15 MB inline, the client switches to prepare → upload → deploy. prepare takes {slug?, shas:[…]} and answers {slug, missing:[…]}. The missing blobs go to /upload in batches of at most 15 MB, and the final deploy references them by sha256: the hex sha256 of the raw file bytes.

Copying files from another artifact

A manifest entry can reference a file another artifact already published. First POST /api/frame/upload with {"slug", "files": [{"path": "<dest>", "from": {"slug", "path", "ver"}}]} (a new artifact reserves its slug with /deploy/prepare first), then deploy with "<dest>": {"sha256": "<source sha256>", "contentType": "…"}.

Comments

GET /comments/{slug}?names=1 answers {"threads": [...], "names": {account: name}, "viewer"}. A thread is {id, created_at, anchor: {path, label, file, file_sha, …}, comments: [...], resolved_at?, resolved_by?, claude_activated_at?}; a comment is {id, author: {account, role?}, text, created_at, to_claude_at?}. author.role is set ("assistant") on Claude's comments. to_claude_at marks a comment a person sent to Claude.

The OAuth token can reply and resolve only on threads with claude_activated_at (a person used "Send to Claude"); elsewhere the server answers 403 Claude is not activated on this thread. A reply is POST /comments/{slug}/{thread} with {"id": "<client uuid>", "text", "answers_summon"?: true}; it answers 201 {"thread_id", "comment_id", "replayed"?} and the same id again answers replayed: true without posting. Resolve is POST …/session-resolve with {"resolved": true}. Starting a thread (POST /comments/{slug}) and /resolve answer 403 this comment action requires a claude.ai session.

Database

POST /api/frame/db/agent with {"slug", "verb", "args"}:

verb args
whoami {} → {"id"}
get {"path": "coll/doc"} → {exists, data, version, updatedAt}
list, query {"collection", "limit"?, "cursor"?, "where"?: [{field, op, value}], "orderBy"?: {field, direction}} → {docs: [{id, data, version, updatedAt}], nextCursor}
set {"path", "data", "replace": true, "ifVersion"?}
update {"path", "data", "ifVersion"?}
delete {"path", "ifVersion"?}
str_replace {"path", "field", "old_str", "new_str", "replace_all"?, "idem": <16 random bytes, base64url>, "ifVersion"?}
batch {"ops": [{"op", …the args above}]} → {results: [{version?}]}

op is eq ne lt lte gt gte in not-in array-contains. Writes answer {version, usage}. Errors are {"error": {"code", "message", …}}: 404 not_declared when the published version doesn't declare db, 409 version_mismatch (with the current version) when ifVersion is stale.

Assets

agent-upload takes the raw file bytes with the asset's Content-Type and answers {opaque_id, url: "/_blob/<id>", size_bytes, content_type, sha256}. agent-list takes {"limit", "after"?} and answers {assets, usage, next?}; agent-copy takes {"from": <slug>, "ids": [...]}; /{id}/agent-delete takes {} and answers {"deleted"}.

Reading files

The boot, GET /api/frame/{slug}?via=model_read, returns ver, the files manifest (path, size, contentType, sha256) and a short-lived assetToken. Bytes come from the content host https://{slug}.frame.claudeusercontent.com:

  • GET /_f/{ver}/_src/{path}?__frame_t=<assetToken> is the stored source, byte-identical to what was published. It works for an owner, writer, commenter or reader. Claude Code skips it only for an artifact shared in from outside the organization (boot mode: "external") when perm.role isn't writer; the CLI does the same.
  • GET /_f/{ver}/{path}?__frame_t=<assetToken> is the served copy, the fallback when _src doesn't answer 200. It is identical to the source for every file except the page, which is served at /_f/{ver}/. The served page has the frame runtime injected between <!-- frame-runtime --> and <!-- /frame-runtime -->, and may end with a <script type="application/json" id="__frame_comments__"> trailer. Stripping both restores the source exactly.
  • Send no Authorization header to the content host; the asset token in the query is the only credential. Send a non-default User-Agent: the CDN rejects Python-urllib's default with Cloudflare error code: 1010 (HTTP 403).
  • A missing file is 404. A missing or expired token is 403; the CLI boots again once for a fresh token, and fails if a new version went live in between. Redirects aren't followed.
  • Path segments are percent-encoded. The CLI refuses paths with .., empty segments, backslashes, % or control characters.
  • The CLI verifies every file against the manifest's sha256. Manifests of older artifacts lack sha256; status fetches those files to compare.
  • Manifest entries with src: {slug, ver} belong to the Artifact type the artifact was made from, index.html included. They aren't the artifact's own files: pull and status skip them, and a typed publish patches around them.

The assetToken is redacted from every output and never written to disk.

Artifact URLs

https://claude.ai/code/artifact/<uuid> carries the slug, a UUID. https://claude.ai/artifact/<short id> carries the same UUID as base58 (Bitcoin alphabet). The CLI decodes either, and accepts a bare UUID or short id.

Not supported

  • Starting comment threads, or commenting as yourself. Those routes need a claude.ai browser session.
  • Artifacts shared from other organizations in list. GET /frames/external needs a browser session.
  • Push notifications. Claude Code's watch subscribes a webhook (/subscribe/{slug}) minted by its own session; watch polls instead.
  • Sharing by email. The route takes account UUIDs; Claude Code resolves names on an approval card the CLI has no equivalent of.

Project Structure

.github/
└── workflows/
    └── publish.yml          lint, tests, auto-version, PyPI publish, GitHub release
assets/
└── header.png
skills/
└── claude-artifact-cli/
    └── SKILL.md             agent skill
src/
└── claude_artifact_cli/
    ├── __init__.py          version
    ├── __main__.py          python -m claude_artifact_cli
    ├── api.py               frame API client
    ├── auth.py              token lookup
    ├── cli.py               argparse entry point; publish, read, pull, status
    ├── commands.py          comments, db, assets, types, sharing, history, watch
    └── update.py            daily update check, background upgrade, `update`
tests/                       unit tests (offline; transport mocked)
AGENTS.md
LICENSE
README.md
pyproject.toml

Documentation

Resource Description
SKILL.md Agent skill: when to use the CLI and how
AGENTS.md Constraints and commands for coding agents working in this repo
publish.yml CI and release pipeline
The protocol Endpoints, headers, deploy body and file reads

Contributing

Issues and pull requests are welcome. Keep the package dependency-free and run uvx ruff check . and PYTHONPATH=src python -m unittest discover -s tests -v before pushing. Every merge to main that touches src/ or pyproject.toml is released to PyPI automatically with a patch bump.

License

Apache-2.0


Star History Chart

Metadata

Release files for claude-artifact-cli 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for claude-artifact-cli 0.3.0
File Size Uploaded
claude_artifact_cli-0.3.0.tar.gz 64.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for claude-artifact-cli 0.3.0
File Interpreter ABI Platform
claude_artifact_cli-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 120.4 kB

Release files / claude_artifact_cli-0.3.0.tar.gz

Download URL claude_artifact_cli-0.3.0.tar.gz
Size 64.7 kB
Tags Source
SHA-256 checksum
How to use checksums
8c828cdf8ab5cee3794a9bce98b013f9ea56b1f2d708fd0c0c00a07741371079
BLAKE2b-256 checksum
How to use checksums
159b158788c034b19716b1a467dd3290cdc68c9a62f61e837e8bf85f628c5181
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / claude_artifact_cli-0.3.0-py3-none-any.whl

Download URL claude_artifact_cli-0.3.0-py3-none-any.whl
Size 55.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1960f2542c37e0f3e07321d4d5839c886e66b6190ab8fe98e35ae57210d72204
BLAKE2b-256 checksum
How to use checksums
24810556ced5ac3b9f8ec3e8e89a0e3254689ee3eb8e0555bd79aa19684d5c79
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page