Skip to content
Back to skills

Write Addon

ASecurity

Write, extend or fix a claude-ui addon: a folder with addon.json and sandboxed HTML views that adds a colour theme, a pane next to the chat, a band above the prompt box, a slash command `/<addon>:<cmd>` or a renderer for a code-fence language or a tool's result to the claude-ui web app. Scaffolds the manifest, copies the typed addon SDK (claude-ui-addon.js + .d.ts), writes `// @ts-check` view scripts and type-checks them. Use when the user asks for a claude-ui addon, theme, pane, band, addon ...

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 10, 2026
developmenttypescriptgoshellnodegitapi

Works with

  • claude code
  • api

Security analysis

A100/100

Scanned November 10, 2026

npx -y skills add cuonghuunguyen/claude-code-ui --skill write-addon --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Write Addon?

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

Security grade badge for Write Addon
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/cuonghuunguyen-write-addon/badge)](https://www.skillsdirectory.com/skills/cuonghuunguyen-write-addon)

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: write-addon
description: >-
  Write, extend or fix a claude-ui addon: a folder with addon.json and sandboxed HTML views that
  adds a colour theme, a pane next to the chat, a band above the prompt box, a slash command
  `/<addon>:<cmd>` or a renderer for a code-fence language or a tool's result to the claude-ui web
  app. Scaffolds the manifest, copies the typed addon SDK (claude-ui-addon.js + .d.ts), writes
  `// @ts-check` view scripts and type-checks them. Use when the user asks for a claude-ui addon,
  theme, pane, band, addon command or renderer, or to change one. Not for Claude Code plugins,
  skills, hooks or mods: those change Claude Code, an addon changes the claude-ui browser app.
---

# Write a claude-ui addon

An addon is a folder. claude-ui serves its files as committed (no build) into iframes with
`sandbox="allow-scripts"`: no cookies, no storage, no parent DOM, no inline scripts, no network
except the `net:` origins the owner approves. Everything else goes through the bridge, which the
SDK wraps. Full reference: `docs/site/features/addons.md` in the claude-ui repository
(https://github.com/cuonghuunguyen/claude-code-ui/blob/main/docs/site/features/addons.md).
Complete working example: `examples/addons/example/`.

## 1. Pin down what the addon does

Map each wish to a part and the capabilities it needs. Ask only when the wish fits none of them.

| Wish | Manifest | Capabilities |
| --- | --- | --- |
| Colours of the whole app | `themes` (and/or `css`) | none |
| A panel next to the chat | `views[] kind: "pane"` | `ui.pane` |
| A strip above the prompt box | `views[] kind: "band"` | `ui.band` |
| `/<addon>:<cmd>` that fills the prompt box | `commands`, `commandsEntry` | `ui.command`, `prompt.insert` |
| Draw ` ```<lang> ` blocks | `renderers[] {fence}` | `render.fence:<lang>` |
| Draw a tool's result | `renderers[] {tool}` | `render.tool:<tool name>` |
| Read the session's messages and tool calls | in a pane, band or command | `session.read` |
| Status bar text, toasts | | `ui.status`, `ui.toast` |
| Debug lines the owner can read in the Addons dialog | `log(text, level)` | `log` |
| Call an HTTP API | `fetch` from a view | `net:<origin>` (the API must allow CORS for `Origin: null`) |

Where code runs decides when it runs. A pane frame loads lazily: nothing runs until the user first
shows the pane, and a pane tab in the main area runs only while that tab exists. A band and the
hidden command frame load with the session that shows. Status text that must be current before the
pane was ever opened belongs in a band (or, with commands, the command frame). Choose the frame by
that, and tell the user which you chose and why.

Impossible, whatever is approved: send a prompt, read or write files, run shell or git, answer
permissions, read the session from a renderer (renderers get only `ui.ready` and `ui.toast`).
Say so instead of working around it.

Request no capability the addon does not use: the owner approves each one, and every new one in
an update needs approval again.

## 2. Scaffold

```
<addon>/
  addon.json
  claude-ui-addon.js      # SDK, copied, never edited
  claude-ui-addon.d.ts    # SDK types, copied, never edited
  <view>.html, <view>.js  # one pair per view, command handler or renderer
  jsconfig.json           # only for the type check
```

Copy the SDK of the claude-ui version the user runs (`claude-ui --version` prints it):

```sh
v=$(claude-ui --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
for f in claude-ui-addon.js claude-ui-addon.d.ts; do
  curl -fsSL "https://raw.githubusercontent.com/cuonghuunguyen/claude-code-ui/v$v/packages/addon-sdk/$f" -o "$f" ||
  curl -fsSL "https://raw.githubusercontent.com/cuonghuunguyen/claude-code-ui/main/packages/addon-sdk/$f" -o "$f"
done
```

From a claude-ui checkout, copy `packages/addon-sdk/claude-ui-addon.{js,d.ts}` instead. A tag
without the SDK (older than the SDK) falls back to `main`; then tell the user the SDK may be
newer than their claude-ui.

`addon.json`:

```json
{
  "name": "my-addon",
  "version": "0.1.0",
  "description": "One line shown in the Addons dialog",
  "capabilities": ["ui.pane", "session.read"],
  "views": [{ "id": "main", "title": "My pane", "entry": "pane.html", "kind": "pane" }]
}
```

- `name`: `^[a-z0-9]+(-[a-z0-9]+)*$`, at most 64. It namespaces commands (`/my-addon:cmd`) and
  pane ids, and never changes between versions.
- `views`: `{id, title, entry, kind: "pane" | "band", phone?}`; `phone: false` hides it below 640 px.
- `commands`: `{name, description, argumentHint?}`; `commandsEntry`: the `.html` that handles them
  (default: the first view's entry). It runs in a hidden frame per session.
- `renderers`: `{fence: "<lang>", entry}` or `{tool: "<tool name>", entry}`.
- `themes`: `{id, name, light?, dark?}`. A given mode needs all 19 keys as `#rrggbb`: `bg`, `card`,
  `side`, `fg`, `muted`, `border`, `primary`, `pfg`, `kw`, `str`, `fn`, `type`, `num`, `cmt`,
  `varc`, `err`, `warn`, `ok`, `info`. Copy the theme of `examples/addons/example/addon.json` and
  change the values; keep text on `bg`/`card` at a 4.5:1 contrast.
- `css`: one `.css` applied app-wide while enabled; it cannot load external images or `@import`.
- Paths are relative, without `..` or `\`, and must exist. Every view, command and renderer needs
  its capability, or install fails naming it.

Each `.html` loads its script as a module and has no inline script or style:

```html
<!doctype html>
<meta charset="utf-8" />
<link rel="stylesheet" href="pane.css" />
<div id="out"></div>
<script type="module" src="pane.js"></script>
```

`jsconfig.json`:

```json
{ "compilerOptions": { "checkJs": true, "strict": true, "target": "ES2023", "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2023", "DOM", "DOM.Iterable"], "noEmit": true }, "exclude": ["claude-ui-addon.js", "*.test.mjs"] }
```

## 3. Write the views with the SDK

Start every script with `// @ts-check` and import from `./claude-ui-addon.js`.

| SDK | Use for |
| --- | --- |
| `watchSession(fn)` | Pane or band showing session data: `fn(parts)` after `init`, again with all parts on every change. Call at module top level |
| `on("command.run", (m) => …)` | Command handler: `m.name`, `m.args` |
| `on("render", (m) => …)` | Renderer: `"fence" in m` → `m.fence.lang`, `m.fence.source`; else `m.tool.name`, `.input`, `.result` |
| `on("init", (m) => …)`, `on("theme.changed", …)` | `m.addon`, `m.view`, `m.role`, `m.session`, `m.theme`, `m.mode` (`light`/`dark`) |
| `autoHeight()` | Every band and renderer: keeps the frame as tall as the content (band 24-160 px, renderer up to 480) |
| `insertPrompt(text)` | Fill the prompt box (≤ 32 KiB); the user sends it |
| `setStatus(text \| null)`, `toast(text)` | ≤ 80 and ≤ 200 characters |
| `log(text, level?)` | A line in the addon's log (≤ 500 characters, 20 per second); works in renderers too |
| `call(type, payload)` | Any request; rejects with `AddonError` (`code`: `refused`, `bad_request`, `unknown`) |

Parts: narrow by `p.type`. The useful ones: `user_text` and `assistant_text` (`text`),
`tool_call` (`tool`, `input`, `status`), `tool_result` (`toolUseId`, `output`, `isError`),
`turn_result` (`durationMs`, `costUsd`, `usage`), `todo_update` (`items`), `subagent`. The full
union is `Part` in `claude-ui-addon.d.ts`. A part is sent again when it changes (same `id`):
key by `id`, as `watchSession` does.

Rules:
- Build DOM with `textContent`/`createElement`, never `innerHTML` with session or tool data.
- Style through a `.css` file or `element.style`; inline `<style>` and `style=""` attributes are
  blocked by the frame's CSP.
- The SDK sets `color-scheme` from the app's mode; use `light-dark()` or
  `@media (prefers-color-scheme)` in CSS for your own colours.
- Keep a pane usable at 390 px wide: no fixed widths, wrap text.
- Put logic that is more than glue (a parser, sums, formatting) in its own module without DOM or
  bridge calls, and check it with a small `node:assert` script (`node <name>.test.mjs`). A frame
  is hard to debug: catching a parsing bug there costs a page reload and a guess.

## 4. Check

1. Types: `npx -y -p typescript tsc -p jsconfig.json` in the addon folder. Fix every error; never silence one
   with `any` or `@ts-ignore`.
2. Logic: run each `*.test.mjs`.
3. Manifest: every path exists; every view, command and renderer has its capability; no unused
   capability.
4. Run it (needs the user's claude-ui running; ask before touching their install): `/addons` in the
   prompt box › **Local folder** › absolute path of the addon › **Install** › **Approve and
   enable**. A local folder is re-read when the dialog opens: after an edit, reopen the dialog
   (and reload the page for view scripts). A row in **Error** names what failed; a refused call
   shows a toast and is listed under the addon's row.

Report to the user: the folder, what each part does, the capabilities it asks for and why, and
the type-check result.

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.

View all in development →

Comments

Loading comments…

Newsletter

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