Skip to content
Back to skills

Port Artifact

ASecurity

the user wants a Claude artifact (or a folder of HTML, JS, CSS, images and video) moved to drobek — text files unchanged via write_files, each binary uploaded at the same path, preview, publish on request

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
ai-agentsgoshellreactapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add freema/drobek --skill port-artifact --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Port Artifact?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Port Artifact
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/freema-port-artifact/badge)](https://www.skillsdirectory.com/skills/freema-port-artifact)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: port-artifact
description: the user wants a Claude artifact (or a folder of HTML, JS, CSS, images and video) moved to drobek — text files unchanged via write_files, each binary uploaded at the same path, preview, publish on request
---

# port-artifact — move a Claude artifact to drobek

## 1. When to use

The user has a Claude artifact (an HTML page, a React component, or a
folder like `index.html` + `film.mp4` + `poster.jpg` + `s1.jpg` + a script)
and wants it on drobek. YOU port it from the files in your environment: the
drobek server fetches nothing from claude.ai. Paths stay as they are — the
app serves text files and uploaded assets side by side at `/<path>`, so
`<video src="film.mp4">` and `img/s1.jpg` work unchanged. Never rewrite a
path, never inline a binary, never paste base64 into a tool call.

## 2. Minimal working code

A multi-file artifact ported as is (`create_app({ name, template: "html" })`,
then this `index.html`, `style.css` and `chapters.js` in ONE `write_files`):

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Summer at Grandma's</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces&display=swap" />
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <video id="film" src="film.mp4" poster="poster.jpg" controls preload="metadata"></video>
    <nav id="chapters"></nav>
    <img src="s1.jpg" alt="The garden" />
    <script src="chapters.js"></script>
  </body>
</html>
```

Then each binary, one upload URL per file (exact byte size, same relative path):

```json
{ "app_id": "…", "path": "film.mp4", "size": 26214400, "content_type": "video/mp4" }
```

```sh
curl -fsS -T film.mp4 '<upload_url>'     # 201 {"path":"/film.mp4",…}
stat -c %s s1.jpg || stat -f %z s1.jpg   # the exact size for the next create_asset_upload
```

A React artifact (one component, `export default function App`) goes into the
react-ts template: the component in `src/main.tsx`, rendered by `createRoot`.
`window.storage` becomes `localStorage` (one browser) or `drobek.data` (shared,
`skill_info('data')`):

```tsx
// src/main.tsx
import { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';

function App() {
  // artifact: await window.storage.get('count') / window.storage.set('count', …)
  const [count, setCount] = useState(() => Number(localStorage.getItem('count') ?? 0));
  useEffect(() => localStorage.setItem('count', String(count)), [count]);
  return <button onClick={() => setCount((c) => c + 1)}>Clicked {count} times</button>;
}

createRoot(document.getElementById('root')!).render(<App />);
```

## 3. API and types

The procedure (tool → what to check):

1. Ask the user: "Move <artifact> to drobek as a new app?" — and whether it
   goes to their personal workspace (`list_apps({})` lists the others).
2. `create_app({ name, template?, workspace? })` → `app_id`, `preview_url`,
   the briefing (read its limits). `html` for a page, `react-ts` for a component.
3. Text files (`.html .js .mjs .css .json .svg .txt .md .jsx .tsx`) with
   `write_files({ app_id, files, reasoning })`, content and paths unchanged,
   1–20 per call and at most 10 MiB of JSON per call (split bigger folders into
   several calls).
4. Every binary (video, audio, image, font) with
   `create_asset_upload({ app_id, path, size, content_type? })` →
   `{ upload_url, expires_at, max_bytes, asset_path, curl }`. Run the `curl -T`
   line in your sandbox. No shell or no network → give the user each
   `upload_url`: in a browser it is an upload page for that one file.
5. Check: `write_files` → `compile.ok: true`; `list_assets({ app_id })` lists
   every binary with its size; give the user the `preview_url` and ask them to
   play the video; `get_logs({ app_id, kind: "runtime" })` for browser errors.
   An artifact rarely has a description, a favicon or link-preview tags (readiness `missing_description`,
   `missing_favicon`): add them to its `<head>` — `skill_info('start')`, "Tab, search, shared link".
6. `publish({ app_id })` ONLY when the user explicitly asks → `published_url`.
7. Offer the public gallery once: show the description (≤ 160 chars) and ask;
   ONLY after an explicit yes `set_gallery_listing({ app_id, listed: true,
   description, user_confirmed: true })`.

Fix a wrong upload by uploading to the same path again (it replaces) or
`delete_asset({ app_id, path })`.

## 4. Rules and limits

What differs from the artifact sandbox (the app CSP):

- Scripts: only the app itself and `https://esm.sh` (inline scripts run).
  `cdnjs`/`unpkg`/`jsdelivr` `<script src>` is blocked: load the library as a
  module from esm.sh (`import * as THREE from 'https://esm.sh/three@0.160.0'`)
  or copy the library file into the app as a text file.
- React artifact packages (`lucide-react`, `recharts`, `d3`, `lodash`,
  `three`, `papaparse`, …) → pinned esm.sh URLs in `drobek.json` `imports`.
  Tailwind classes → Tailwind's browser build (`skill_info('ui')`).
  `@/components/ui/*` (shadcn) does not exist: plain elements instead.
- `fetch` reaches only the app itself and esm.sh: an external API goes
  through `drobek.proxy` with the owner's secret (`skill_info('proxy')`).
- Fonts (Google Fonts), CSS, images, `<video>`/`<audio>` from any https URL
  work; `<iframe>` only YouTube (`youtube-nocookie.com`), Vimeo, Google Drive
  (plus what the operator allows).
- No `window.claude.*` on drobek: `window.claude.complete` has no
  replacement (drop the feature, or call an LLM API through `drobek.proxy`,
  with `stream: true` on an upstream registered with `allow_streaming: true`
  so the answer renders as it streams);
  `window.storage` → `localStorage` or `drobek.data`; sign-in → `drobek.auth`,
  a form that must reach the owner → `drobek.forms`. `skill_info()` lists them.
- Limits (defaults; the briefing and `create_asset_upload` give this
  server's): text files 512 KiB each, 5 MiB and 200 files per version;
  assets `APP_ASSET_MAX_BYTES` 100 MiB each, `APP_ASSETS_QUOTA` 1 GiB per app,
  `APP_ASSET_UPLOADS_PER_HOUR` 60; an upload URL is single use, 30 minutes.
- Asset types come from the bytes: PNG, JPEG, GIF, WebP, AVIF, ICO, SVG, MP4 (H.264/AAC),
  WebM, M4A, MP3, Ogg, WAV, WOFF, WOFF2. No transcoding (MOV/HEVC: convert
  first). A big `data:` URI inside the HTML: save it as a file and upload it.
- An app text file at a path wins over an asset there (`asset_path_taken`).

## 5. Errors → fix

| error | cause | fix |
|---|---|---|
| `asset_too_large` | the file is over `APP_ASSET_MAX_BYTES` | compress it (lower bitrate) or link it from its https URL |
| `asset_type_not_allowed` | the bytes are not an allowed type for the extension | convert (MP4 H.264/AAC, WebM, JPEG, PNG, WebP); keep the right extension |
| `asset_quota_exceeded` | the app's assets would pass `APP_ASSETS_QUOTA` | `list_assets`, `delete_asset` what the page does not use |
| `asset_path_taken` | a text file of the app holds that path | delete that file with `write_files` or upload under another path |
| `asset_size_mismatch` | the PUT body is not `size` bytes | `stat` the file, ask for a new URL with the exact size |
| `upload_token_invalid` | the URL was used (even by a failed PUT) or is older than 30 minutes | a new `create_asset_upload` |
| `rate_limited` | over `APP_ASSET_UPLOADS_PER_HOUR` upload URLs | upload what you have URLs for; ask for more after the hour |
| `invalid_path` | `write_files` with a media extension (`.mp4`, `.mp3`) or `..` | binaries via `create_asset_upload`; app-relative paths |
| `limit_exceeded` | a text file over 512 KiB (inlined base64, a bundled library) | move the binary out; load the library from esm.sh |
| `unresolved_import` | a React artifact package not in `drobek.json` | add its pinned esm.sh URL |
| `secret_in_source` | an API key in the artifact's code | remove it; the owner sets it in the dashboard (`drobek.proxy`) |
| `user_confirmation_required` | gallery listing without the user's yes | ask; send `user_confirmed: true` only after a yes |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.