Create a new Claudeck Plugin
You are scaffolding a new tab-sdk plugin called "$ARGUMENTS".
Parse the arguments: the first word is the plugin name (kebab-case), the rest is the description of what the plugin should do.
Plugin Locations
Claudeck supports two plugin directories:
| Directory | URL Path | Use When |
|---|---|---|
plugins/<name>/ (project) |
/plugins/<name>/ |
Building a built-in plugin that ships with Claudeck |
~/.claudeck/plugins/<name>/ (user) |
/user-plugins/<name>/ |
Creating a personal/user-installed plugin that persists across updates |
Default: Create user plugins in ~/.claudeck/plugins/<name>/ unless the user explicitly asks for a built-in plugin. User plugins are the recommended approach since they persist across npm upgrades and don't require modifying the Claudeck source code.
To resolve the user plugins directory, use ~/.claudeck/plugins/ (or $CLAUDECK_HOME/plugins/ if the env var is set).
Rules
- The plugin name MUST be kebab-case (e.g.
my-plugin,code-metrics) - All files go in the chosen plugin directory — NO other files need to be modified (no main.js, no style.css, no index.html)
- The plugin is auto-discovered by the server at runtime via
GET /api/plugins - Create at minimum:
client.jsand optionallyclient.css - If the plugin needs server-side API routes, also create
server.js(for user plugins, requiresCLAUDECK_USER_SERVER_PLUGINS=truein~/.claudeck/.env) - If the plugin needs persistent config, also create
config.json(auto-copied to~/.claudeck/config/on first run)
Client JS file template
Create client.js in the plugin directory following this structure:
// <Title> — Tab SDK plugin
// <Description of what the plugin does>
import { registerTab } from '/js/ui/tab-sdk.js';
registerTab({
id: '<name>',
title: '<Title>',
icon: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">...</svg>',
lazy: true,
init(ctx) {
// ── Build DOM ──
const root = document.createElement('div');
root.className = '<name>-tab';
root.style.cssText = 'display:flex;flex-direction:column;flex:1;overflow:hidden;';
// Build the UI innerHTML here...
// ── Context API (ctx) ──
// ctx.pluginId — Plugin's ID string
// ctx.getProjectPath() — Current project path
// ctx.getSessionId() — Current session ID
// ctx.getTheme() — Current theme: 'dark' or 'light'
// ctx.on('projectChanged', fn) — Fires when user switches project (returns unsub fn)
// ctx.on('ws:message', fn) — Live WebSocket stream messages (returns unsub fn)
// ctx.onState('sessionId', fn) — Session switch (returns unsub fn)
// ctx.off(event, fn) — Remove an event listener
// ctx.api — Full API module for fetch calls
// ctx.storage.get(key) — Read from plugin-scoped localStorage
// ctx.storage.set(key, value) — Write to plugin-scoped localStorage
// ctx.storage.remove(key) — Remove from plugin-scoped localStorage
// ctx.toast(msg, opts) — Show notification ({duration, type: 'info'|'success'|'error'})
// ctx.showBadge(count) — Show badge on tab button
// ctx.clearBadge() — Hide badge
// ctx.setTitle(text) — Update tab button label
// ctx.dispose() — Unsubscribe all listeners (auto-called on destroy)
// ── Project-scoped data ──
// If your plugin loads data scoped to a project, follow this pattern:
//
// function loadData() {
// const project = ctx.getProjectPath();
// if (!project) return;
// fetch(`/api/plugins/<name>/?project=${encodeURIComponent(project)}`)
// .then(r => r.json()).then(data => { /* render */ });
// }
// ctx.on('projectChanged', loadData); // reload on project switch
// loadData(); // initial load
//
// IMPORTANT: ALWAYS use ctx.getProjectPath() to read the project path.
// NEVER use document.getElementById('project-select') directly.
// IMPORTANT: ALWAYS use ctx.on('projectChanged', fn) to react to project changes.
// NEVER add your own change listener to the project select element.
return root; // must return an HTMLElement
},
// onActivate(ctx) { /* tab became visible — ctx is passed */ },
// onDeactivate(ctx) { /* tab was hidden — ctx is passed */ },
// onDestroy(ctx) { /* cleanup — ctx is passed; all subscriptions auto-disposed */ },
});
IMPORTANT: Use absolute import paths (e.g. '/js/ui/tab-sdk.js', '/js/core/api.js') since plugins are served from /plugins/<name>/client.js (built-in) or /user-plugins/<name>/client.js (user). Absolute paths work identically for both locations.
Client CSS file template
Create client.css in the plugin directory with styles scoped to .<name>-tab to avoid conflicts:
/* <Title> — Tab SDK plugin styles */
.<name>-tab {
font-family: var(--font-mono);
color: var(--text);
}
Use these CSS custom properties from the app theme:
- Colors:
var(--text),var(--text-secondary),var(--text-dim),var(--accent),var(--success),var(--warning),var(--error) - Backgrounds:
var(--bg),var(--bg-secondary),var(--bg-tertiary),var(--bg-deep) - Layout:
var(--border),var(--radius),var(--radius-lg),var(--font-mono)
Server JS file template (optional)
Create server.js in the plugin directory if your plugin needs API endpoints. The router is auto-mounted at /api/plugins/<name>/:
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
res.json({ status: "ok" });
});
export default router;
Note for user plugins: Server-side routes require CLAUDECK_USER_SERVER_PLUGINS=true in ~/.claudeck/.env. This is disabled by default for security.
To use config files, import configPath. The import path depends on the plugin location:
- Built-in plugins (
plugins/<name>/):import { configPath } from "../../server/paths.js"; - User plugins (
~/.claudeck/plugins/<name>/): Use the server's config path directly via the route context, or read/write files usingos.homedir():
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
const cfgDir = process.env.CLAUDECK_HOME || join(homedir(), ".claudeck", "config");
const cfgFile = join(cfgDir, "<name>-config.json");
Config JSON file template (optional)
Create config.json in the plugin directory with default values. It will be auto-copied to ~/.claudeck/config/ on first run:
{
"enabled": false,
"settings": {}
}
Available imports
From /js/core/:
store.js—getState(key),setState(key, val),on(key, fn)(returns unsub),off(key, fn)events.js—on(event, fn)(returns unsub),off(event, fn),emit(event, data)dom.js—$(cached DOM query map)constants.js—CHAT_IDS,BOT_CHAT_IDapi.js— all fetch helpers (fetchSessions,fetchFileTree,execCommand, etc.)utils.js—escapeHtml(),getToolDetail(),formatBytes(), etc.
From /js/ui/:
tab-sdk.js—registerTab(),unregisterTab(),getRegisteredTabs()commands.js—registerCommand()for slash commandsformatting.js—renderMarkdown(),highlightCodeBlocks()parallel.js—getPane(),panesright-panel.js—openRightPanel(),toggleRightPanel()
What to build
Now implement the plugin based on the user's description. Build a fully functional plugin — not just a skeleton. Include:
- Complete DOM structure with proper layout
- Event handlers and interactivity
- Real-time updates via
ctx.on('ws:message', ...)if relevant - Session awareness via
ctx.onState('sessionId', ...)if relevant - Project awareness via
ctx.getProjectPath()andctx.on('projectChanged', ...)if the plugin loads project-scoped data - Proper CSS styling matching the app's dark terminal aesthetic
- Badge counts via
ctx.showBadge()where meaningful - Persistent data via
ctx.storage.get/setfor client-side state - Toast notifications via
ctx.toast()for user feedback
CRITICAL: Never access the project select DOM element directly. Always use ctx.getProjectPath() and ctx.on('projectChanged', fn) from the tab SDK context.
After creating the files
- Tell the user the plugin is ready
- Remind them to reload the browser — the plugin will be auto-discovered
- Show the file paths that were created
- If it's a user plugin with
server.js, remind them to setCLAUDECK_USER_SERVER_PLUGINS=truein~/.claudeck/.envand restart the server - Mention which directory was used (built-in vs user) and why
Source: hamedafarag/claudeck — distributed by TomeVault.