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.
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> DIRmerges the live version in, resolve any conflicts, thenclaude-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:
--token$CLAUDE_ARTIFACT_TOKEN, then$CLAUDE_CODE_ARTIFACTS_API_TOKEN- macOS Keychain, generic password service
Claude Code-credentials ~/.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 (bootmode: "external") whenperm.roleisn'twriter; the CLI does the same.GET /_f/{ver}/{path}?__frame_t=<assetToken>is the served copy, the fallback when_srcdoesn'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
Authorizationheader to the content host; the asset token in the query is the only credential. Send a non-defaultUser-Agent: the CDN rejects Python-urllib's default with Cloudflareerror code: 1010(HTTP 403). - A missing file is
404. A missing or expired token is403; 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 lacksha256;statusfetches those files to compare. - Manifest entries with
src: {slug, ver}belong to the Artifact type the artifact was made from,index.htmlincluded. They aren't the artifact's own files:pullandstatusskip 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/externalneeds a browser session. - Push notifications. Claude Code's watch subscribes a webhook (
/subscribe/{slug}) minted by its own session;watchpolls 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
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)
| File | Size | Uploaded | |
|---|---|---|---|
| claude_artifact_cli-0.3.0.tar.gz | 64.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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