Files
claudecodeui/examples/plugins/hello-world

Hello World Plugin

A minimal example showing how to build a plugin for Claude Code UI, covering both frontend (iframe) and backend (server subprocess) capabilities.

How plugins work

A plugin's UI runs client-side inside a sandboxed iframe. Plugins can optionally declare a server entry in their manifest — a Node.js script that the host runs as a subprocess. The iframe communicates with its server through a postMessage RPC bridge (the host proxies the calls because the sandboxed iframe has no auth token).

┌─────────────────────────────────────────────────────────┐
│  Host server                                            │
│                                                         │
│  Lifecycle:                                             │
│    git clone / git pull      Install & update           │
│    npm install               Dependency setup           │
│                                                         │
│  Runtime:                                               │
│    GET  /api/plugins                List plugins        │
│    GET  /api/plugins/:name/assets/* Serve static files  │
│    ALL  /api/plugins/:name/rpc/*    Proxy → subprocess  │
│    PUT  /api/plugins/:name/enable   Toggle + start/stop │
│    DELETE /api/plugins/:name        Uninstall + stop    │
│                                                         │
│  Plugin subprocess (server.js):                         │
│    Runs as a child process with restricted env           │
│    Listens on random local port                         │
│    Receives secrets via X-Plugin-Secret-* headers       │
└───────────┬─────────────────────────┬───────────────────┘
            │ serves static files     │ proxies RPC
┌───────────▼─────────────────────────▼───────────────────┐
│  Frontend (browser)                                     │
│                                                         │
│  Plugin iframe ◄──postMessage──► Host app               │
│  (sandboxed)        ccui:context                        │
│                     ccui:request-context                 │
│                     ccui:rpc / ccui:rpc-response         │
└─────────────────────────────────────────────────────────┘

Plugin structure

my-plugin/
  manifest.json   # Required — plugin metadata
  index.html      # Frontend entry point (rendered in iframe)
  server.js       # Optional — backend entry point (runs as subprocess)
  package.json    # Optional — npm dependencies for server.js

All files in the plugin directory are accessible via /api/plugins/:name/assets/. Use relative paths in your HTML (e.g., <link href="styles.css">).

manifest.json

{
  "name": "hello-world",        // Unique id — alphanumeric, hyphens, underscores only
  "displayName": "Hello World", // Shown in the UI
  "version": "1.0.0",
  "description": "Short description shown in settings.",
  "author": "Your Name",
  "icon": "Puzzle",             // Lucide icon name (see available icons below)
  "type": "iframe",             // Rendering method — only "iframe" is supported today
  "slot": "tab",                // Where the plugin appears — only "tab" is supported today
  "entry": "index.html",        // Frontend entry file, relative to plugin directory
  "server": "server.js",        // Optional — backend entry file, runs as Node.js subprocess
  "permissions": []              // Reserved for future use
}

Available icons

Puzzle (default), Box, Database, Globe, Terminal, Wrench, Zap, BarChart3, Folder, MessageSquare, GitBranch

Installation

Manual: Copy your plugin folder into ~/.claude-code-ui/plugins/.

From git: In Settings > Plugins, paste a git URL and click Install. The repo is cloned into the plugins directory.


Backend — Server subprocess

Plugins that need to make authenticated API calls, use npm packages, or run Node.js logic can declare a "server" entry in their manifest. The host manages the full lifecycle:

How it works

  1. When the plugin is enabled, the host spawns node server.js as a child process
  2. The subprocess must print a JSON line to stdout: {"ready": true, "port": 12345}
  3. The host records the port and proxies requests from /api/plugins/:name/rpc/* to it
  4. When the plugin is disabled or uninstalled, the host sends SIGTERM to the process

Restricted environment

The subprocess runs with a minimal env — only PATH, HOME, NODE_ENV, and PLUGIN_NAME. It does not inherit the host's API keys, database URLs, or other secrets from process.env.

Secrets

Per-plugin secrets are stored in ~/.claude-code-ui/plugins.json and injected as HTTP headers on every proxied request:

{
  "hello-world": {
    "enabled": true,
    "secrets": {
      "apiKey": "sk-live-..."
    }
  }
}

The plugin's server receives these as x-plugin-secret-apikey headers — they are per-call, never stored in the subprocess env.

Example server.js

const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json');

  // Read host-injected secrets
  const apiKey = req.headers['x-plugin-secret-apikey'];

  if (req.method === 'GET' && req.url === '/hello') {
    res.end(JSON.stringify({ message: 'Hello!', hasApiKey: Boolean(apiKey) }));
    return;
  }

  res.writeHead(404);
  res.end(JSON.stringify({ error: 'Not found' }));
});

// Listen on a random port and signal readiness
server.listen(0, '127.0.0.1', () => {
  const { port } = server.address();
  console.log(JSON.stringify({ ready: true, port }));
});

Frontend — Context API

The host app sends a ccui:context message to the iframe whenever the theme, project, or session changes:

window.addEventListener('message', (event) => {
  if (event.data?.type !== 'ccui:context') return;

  const { theme, project, session } = event.data;
  // theme   — "dark" | "light"
  // project — { name, path } | null
  // session — { id, title } | null
});

To request the current context on load:

window.parent.postMessage({ type: 'ccui:request-context' }, '*');

Frontend — RPC bridge

The sandboxed iframe cannot make authenticated API calls directly. Instead, it sends RPC requests via postMessage, and the host proxies them to the plugin's server subprocess:

function callBackend(method, path, body) {
  return new Promise((resolve) => {
    const requestId = Math.random().toString(36).slice(2);

    function handler(event) {
      if (event.data?.type === 'ccui:rpc-response' && event.data.requestId === requestId) {
        window.removeEventListener('message', handler);
        resolve(event.data);
      }
    }
    window.addEventListener('message', handler);

    window.parent.postMessage({
      type: 'ccui:rpc',
      requestId,
      method,
      path,
      body: body || undefined,
    }, '*');
  });
}

// Usage
const result = await callBackend('GET', '/hello');
// result = { type: 'ccui:rpc-response', requestId, status: 200, data: { message: 'Hello!' } }

The host receives ccui:rpc, makes an authenticated fetch to /api/plugins/:name/rpc/*, and returns the response as ccui:rpc-response.

Frontend — Mobile

On desktop, each enabled plugin gets its own tab in the tab bar. On mobile, plugins are grouped under a single "More" button in the bottom navigation to save space.

Security

Frontend sandbox

Plugins run in a sandboxed iframe with allow-scripts allow-forms allow-popups. They cannot access the host app's cookies, localStorage, or auth tokens (allow-same-origin is intentionally omitted).

Server subprocess isolation

The subprocess runs as a separate OS process with:

  • Restricted env — no host secrets inherited; only PATH, HOME, NODE_ENV, PLUGIN_NAME
  • Per-call secrets — injected as HTTP headers by the host proxy, never stored in process env
  • Process boundary — a crash in the plugin cannot crash the host
  • Auth stripping — the host removes authorization and cookie headers before proxying

The subprocess runs as the same OS user, so it has the same filesystem/network access. This matches the trust model of VS Code extensions, Grafana backend plugins, and Terraform providers — the user explicitly installs the plugin.

Install-time protections

npm postinstall scripts are blocked during installation (--ignore-scripts). Plugins that need npm packages should ship pre-built or use packages that work without postinstall hooks.

Try it

cp -r examples/plugins/hello-world ~/.claude-code-ui/plugins/

Then open Settings > Plugins — "Hello World" should appear. Enable it, open its tab, and click the RPC buttons to test the server subprocess.