Files
AgentHub/docs/plans/artifacts/README.md
T
nntrivi2001 c356cf96a1 fix(artifacts): a page published from its session's scratchpad keeps a viewable copy
The Artifact tool writes a page into the session's own Claude Code
scratchpad by default (<tmp>/claude-<uid>/<project>/<session>/scratchpad),
but pageCopy took the file only from the workspace folder, so such a page
got no copy and Preview said "the file ... is gone" while it was still there.

pageCopy now also accepts that ONE scratchpad, named from the publishing
session's id, its cwd and the workspace account's uid; before reading,
each of its four folders must be a real directory of that account, and each
file is realpath-confined, opened O_NOFOLLOW and must be a regular file of
that uid. Supporting files come only from the page's own root. The older-
page backfill (POST /api/artifacts/:id/copy) uses the session's cwd too.
The noCopy message now says the file may also lie outside both.
2026-11-05 11:20:34 +07:00

5.3 KiB

Artifacts hub — plan

Drafted 2026-10-14. Status: all three phases built 2026-10-14 (the session view's own Artifacts tab is not built; the page has the per-workspace file picker).

Goal

One place to keep what agents PRODUCE, and (in server mode) to share it:

Kind What it is Where it comes from
page A claude.ai Artifact a session published (claude.ai/…/artifact/…) the Artifact tool's (or a Claude Docs doc-creating batch's) PostToolUse hook, or a link pasted by hand
file A file an agent wrote (HTML, Markdown, PDF, image, CSV, Office…) Write/Edit/NotebookEdit hook events, kept on the user's pin; a pptx/docx/xlsx/pdf/html the session created or changed, captured at its Stop (auto); or uploaded from the computer
skill A Claude Code skill folder (SKILL.md + files) the user's own ~/.claude/skills/<name> or a workspace's .claude/skills/<name>; or uploaded (a Markdown file, files, a folder, a .zip/.skill — several skills at once too) or written in the dialog, under a name the user chooses
plugin A plugin by marketplace source + name (no files) picked from the list a marketplace link answers (POST /api/artifacts/plugin/inspect), or typed as name@marketplace
  • Local mode: one personal library, no sharing UI.
  • Server mode: every artifact is the OWNER's and private by default. The owner can Share it with everyone — it appears in the Hub for every signed-in user — or with chosen people only (artifact_shares; they find it under Shared with you). A page keeps a viewable copy — its HTML file (from the workspace folder, or from the publishing session's own Claude Code scratchpad, where the Artifact tool writes a page by default), or the copy Claude exports of a Doc / Slides / Design (artifacts-export/) — so the people it is shared with need no access to the claude.ai account that published it. A shared skill or plugin can be installed into the viewer's own account.

Rules (binding, same spirit as CLAUDE.md)

  • What is shared is a SNAPSHOT, never the owner's live file. Sharing a file/skill copies it (size-capped, regular files only, no symlinks followed) into <data dir>/artifacts/<id>/, written once like session_snapshots. Viewers read only that copy, so no route ever reads another user's home for them. Re-sharing makes a new snapshot.
  • Nothing shared runs on the dashboard's origin. HTML/SVG previews are served under the /git/raw headers (sandbox CSP + nosniff) and shown in a sandboxed iframe; a type that sandbox cannot show (Office files, zip, PDF — the sandbox blocks both a download and the PDF viewer) gets Download only, never a blank Preview. A page is only a link out, accepted only as https://claude.ai/….
  • Installing is the viewer's explicit click, into the viewer's own account only. For a skill, the hub writes the snapshot's files into ~/.claude/skills/<name>/ of the VIEWER (S3: as their member, through write-as). It never overwrites an existing skill without conflict: replace, and the old one goes to the trash first. For a plugin, the hub runs claude plugin marketplace add <source> + claude plugin install <name>@<market> --scope user as the viewer (the applyPluginBaseline path). The marketplace source passes an allowlist (owner/repo or a validateRemoteUrl URL), and no argv comes from the request beyond those two validated values.
  • Before installing, the dialog lists every file of the skill (a skill may carry scripts that the viewer's agent will run). Every share, unshare and install is written to audit_log.
  • Visibility follows ownership: a developer sees their own artifacts plus those shared with everyone or with them by name; admin/analyst see per scope.js, like sessions. Unshare removes an artifact from the Hub but does not uninstall copies others already installed.

Data

New table artifacts (CREATE TABLE IF NOT EXISTS in server/db.js):

id, kind, title, description, owner_id, lane_id, session_id, source (path | url | market spec), url, snapshot (dir name | NULL), size, shared (0/1), shared_at, installs, created_at, updated_at, plus an index on (owner_id, kind) and on (shared, kind).

Phases

  1. Library (local + server, private). Table, server/lib/artifacts.js, /api/artifacts (list/get/create/patch/delete). page rows are captured from the Artifact PostToolUse hook (URL taken from tool_response by an https://claude.ai/…artifact/… match). file candidates are listed per workspace from Write events with a document extension, and the user pins them. New Artifacts page (sidebar) plus an Artifacts tab in the session view's right panel. Scope wiring: PATH_RULES, broadcastAudience.
  2. Hub (server mode). Share/Unshare buttons, snapshot, Hub tab, sandboxed preview, download, artifact_shared broadcast.
  3. Skills & plugins. "Share a skill/plugin" picker, the install flow (S3 included), conflict handling, audit.

Each phase ships with its server and client tests and an update to docs/API.md.

Open questions

  • No Artifact tool call has happened on this machine yet, so the exact hook payload of a publish is unverified. Phase 1 matches the URL loosely and keeps "paste a link" as the fallback.
  • Should an admin be able to disable Hub installs for everyone (one switch in Settings)? Proposed: yes, on by default.