plugins-management
Create, publish, delete, and submit plugins for coding agents (Claude Code, OpenCode). Use when user wants to (1) create a new plugin with proper structure, (2) create or configure a plugin marketplace, (3) publish plugins to GitHub/GitLab/npm, (4) delete/uninstall plugins, (5) v
Install
npx skills add https://github.com/CodeAlive-AI/ai-driven-development/tree/main/skills/plugins-management
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install codealive-ai-ai-driven-development@llmmart
git clone https://github.com/CodeAlive-AI/ai-driven-development.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole codealive-ai/ai-driven-development collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Plugins Manager
Manage plugins across coding agents: create, validate, publish, delete, and submit to official directories or npm.
Supported agents:
- Claude Code:
.claude-plugin/plugin.json-based plugins, distributed via marketplaces - OpenCode: TypeScript/JavaScript plugins in
.opencode/plugins/or npm packages listed inopencode.json - Devin CLI / Desktop:
.devin-plugin/plugin.jsonmanifest inside a plugin source (GitHub repo, git URL,git-subdir, or local folder); installs skills (<plugin>:<skill>slash commands),AGENTS.md/rules/,agents/subagents,hooks.json, and.mcp.jsonas one unit. Managed withdevin pluginscommands; plugins also apply to Devin cloud sessions, subject to per-surface limits. The scripts in this skill target Claude/OpenCode manifests — for Devin, author the.devin-plugin/plugin.jsonby hand per the Devin docs.
CRITICAL: Before performing any deletion, uninstall, or removal operation, you MUST use the AskUserQuestion tool to confirm with the user. Never delete/uninstall plugins or remove marketplaces without explicit user confirmation.
Quick Reference
| Task | Command/Script |
|---|---|
| Create plugin | python scripts/init_plugin.py <name> |
| Create marketplace | python scripts/init_marketplace.py <name> |
| Validate plugin | python scripts/validate_plugin.py <path> |
| Validate marketplace | claude plugin validate <path> |
| Prepare submission | python scripts/prepare_submission.py <path> --email X --company-url Y |
| Install plugin | /plugin install <name>@<marketplace> |
| Delete plugin | /plugin uninstall <name>@<marketplace> |
| Test plugin (dev) | claude --plugin-dir ./my-plugin |
| Reload after edits | /reload-plugins |
| Cut release tag | claude plugin tag --push |
| List installed | claude plugin list [--json] [--available] |
| Update plugin | claude plugin update <name>@<marketplace> |
Workflows
1. Create a New Plugin
# Basic plugin with commands
python scripts/init_plugin.py my-plugin --path ./
# Full plugin with all components
python scripts/init_plugin.py my-plugin --path ./ --all
# Specific components
python scripts/init_plugin.py my-plugin --with-agents --with-skills
Flags:
--with-commands(default): Include commands directory--with-agents: Include agents directory--with-skills: Include skills directory--with-hooks: Include hooks configuration--with-mcp: Include MCP server configuration--all: Include all components--author "Name": Set author name
After creation:
- Edit
.claude-plugin/plugin.jsonwith plugin details - Add commands to
commands/*.mdwith YAML frontmatter - Add agents to
agents/*.mdif needed - Update
README.mdwith documentation
2. Create a Marketplace
# Empty marketplace
python scripts/init_marketplace.py my-marketplace --path ./
# With initial plugin
python scripts/init_marketplace.py my-marketplace --with-plugin my-plugin
After creation:
- Edit
.claude-plugin/marketplace.json - Add plugins to
plugins/directory - Push to GitHub:
git push origin main
Users install with:
/plugin marketplace add username/my-marketplace
Marketplace references:
- Required file:
.claude-plugin/marketplace.json - Plugin entries must have
namethat matches each plugin'splugin.jsonname - Use relative paths in
source(e.g.,./plugins/my-plugin), not absolute paths - Use
${CLAUDE_PLUGIN_ROOT}inside hooks and MCP configs referenced by marketplace plugins
3. Validate a Plugin
python scripts/validate_plugin.py ./my-plugin
Validates:
- plugin.json required fields (name, description, version, author)
- Semantic versioning format
- Command/agent frontmatter
- Hooks and MCP configuration
- README.md and LICENSE presence
Also consider:
claude plugin validate <path>for marketplace JSON validation
4. Publish a Plugin
To GitHub:
cd my-marketplace
git init
git add .
git commit -m "Initial release"
git remote add origin https://github.com/user/my-marketplace.git
git push -u origin main
# Tag release
git tag -a v1.0.0 -m "Version 1.0.0"
git push origin v1.0.0
Distribution methods:
- GitHub:
/plugin marketplace add user/repo - GitLab:
/plugin marketplace add https://gitlab.com/user/repo.git - URL:
/plugin marketplace add https://example.com/marketplace.json
5. Delete/Uninstall Plugins
⚠️ ALWAYS confirm with user before deleting/uninstalling. Use AskUserQuestion to ask: "Are you sure you want to uninstall '[plugin-name]'? This action cannot be undone."
# Uninstall from Claude Code
/plugin uninstall plugin-name@marketplace-name
# Remove marketplace (confirm with user first!)
/plugin marketplace remove marketplace-name
To delete source files: First confirm with user via AskUserQuestion, then remove the plugin directory from the marketplace's plugins/ folder and update marketplace.json.
6. Submit to Anthropic's Official Directory
The submission script automatically gathers all required form fields using gh CLI and git.
Prerequisites:
- Plugin pushed to GitHub
ghCLI installed and authenticated- All validation checks pass
Prepare submission:
# Basic - gathers repo URL and SHA automatically
python scripts/prepare_submission.py ./my-plugin
# With required contact info
python scripts/prepare_submission.py ./my-plugin \
--email your@email.com \
--company-url https://yourcompany.com
# Copy SHA to clipboard
python scripts/prepare_submission.py ./my-plugin --copy-sha
# Save to JSON file
python scripts/prepare_submission.py ./my-plugin --output submission.json
# Open form in browser
python scripts/prepare_submission.py ./my-plugin --open-form
Form fields gathered automatically:
| Field | Source |
|---|---|
| Link to Plugin | gh repo view --json url |
| Full SHA | git rev-parse HEAD |
| Plugin Homepage | plugin.json homepage or repo URL |
| Plugin Name | plugin.json name |
| Plugin Description | plugin.json description (50-100 words) |
Fields you must provide:
--email: Primary contact email--company-url: Company/Organization URL
Submission requirements:
- Plugin must be pushed to GitHub
- Working directory should be clean (no uncommitted changes)
- Description should be 50-100 words
- README.md and LICENSE files present
- No secrets/API keys in code
Plugin Structure Reference
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Optional manifest (auto-discovered if absent)
├── skills/ # Agent skills (preferred over commands/)
│ └── */SKILL.md
├── commands/ # Skills as flat .md files
│ └── *.md
├── agents/ # AI subagents
│ └── *.md
├── output-styles/ # Output style definitions (2026)
├── themes/ # Color themes (2026)
├── monitors/ # Background monitors (2026, v2.1.105+)
│ └── monitors.json
├── hooks/
│ └── hooks.json # Event handlers
├── bin/ # Executables added to PATH (2026)
├── settings.json # Default agent / subagentStatusLine (2026)
├── .mcp.json # MCP servers
├── .lsp.json # LSP server config (since v2.0.74)
├── package.json # Auto-installed dependencies (2026)
├── README.md # Documentation
├── CHANGELOG.md
└── LICENSE
For detailed reference: See references/plugin-guide.md
OpenCode Plugins
OpenCode (anomalyco/opencode v1.14.x) plugins are TypeScript/JavaScript modules — fundamentally different from Claude Code plugins.
Quick reference
| Task | Approach |
|---|---|
| Create local plugin | Drop .ts file in .opencode/plugins/ (project) or ~/.config/opencode/plugins/ (global) |
| Author npm plugin | npm init, add keywords: ["opencode-plugin"], depend on @opencode-ai/plugin |
| Install npm plugin | Add package name to opencode.json → "plugin": [...]; restart |
| Distribute | Publish to npm (no central marketplace) |
Minimal plugin
// .opencode/plugins/env-protection.ts
import type { Plugin } from "@opencode-ai/plugin"
export default (async () => ({
tool: {
execute: {
before: async (input, output) => {
if (output.args.filePath?.includes(".env")) {
throw new Error("Reading .env is forbidden")
}
},
},
},
})) satisfies Plugin
Register npm plugins
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"opencode-helicone-session",
"@my-org/custom-plugin"
]
}
OpenCode runs bun install at startup. Cached at ~/.cache/opencode/node_modules/.
What plugins can do
- Add custom tools the AI can call (Zod-validated args)
- Intercept/block tool calls (
tool.execute.beforethrows to block) - Subscribe to ~25 lifecycle events (
session.idle,file.edited,permission.asked, ...) - Register custom slash commands and auth providers
- Transform messages or system prompts during context compaction (experimental)
Critical caveats (v1.14.x)
tool.execute.*hooks do not fire for MCP tool calls — use thepermissionblock inopencode.json- No central marketplace — distribute via npm and aggregators like awesome-opencode
- Plugins run in-process with full SDK access — audit third-party code before installing
See references/opencode-plugins.md for the full OpenCode plugin reference.
Critical Rules (Avoid Silent Failures)
- Keep
skills/,commands/,agents/,hooks/,monitors/,themes/,output-styles/,bin/at the plugin root (never inside.claude-plugin/). - Do not add standard component paths to
plugin.json. Only specify non-standard paths starting with./. - Use
${CLAUDE_PLUGIN_ROOT}(cache path, changes per version) and${CLAUDE_PLUGIN_DATA}(persistent across updates) in hooks and MCP/LSP/monitor config paths. Relative paths break after install. - Ensure hook scripts are executable (
chmod +x scripts/*). - Marketplace
plugins[].namemust match the plugin'splugin.jsonname. - Path traversal limit (2026): plugins cannot reference files outside their directory; use symlinks inside the plugin if needed.
- Versioning (2026): omit
versionto use git SHA (every commit is a new version). Setversionand bump for stable releases. Useclaude plugin tagto cut release tags.
Common Patterns
Command File Format
---
description: What this command does
---
# Command Name
Instructions for Claude when command is invoked.
Agent File Format
---
description: Agent specialty and purpose
---
# Agent Name
Detailed instructions and expertise.
Hooks Configuration
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
]
}
]
}
}
MCP Server Configuration
{
"mcpServers": {
"server-name": {
"command": "node",
"args": ["./servers/server.js"]
}
}
}
Skill File Format
---
name: my-skill
description: What this skill does and when to use it
---
# Skill Title
Instructions for Claude when this skill is invoked.
Marketplace Entry Example
{
"name": "my-plugin",
"source": "./plugins/my-plugin",
"description": "Short description",
"version": "1.0.0",
"author": { "name": "Author Name" },
"category": "productivity",
"keywords": ["tag1", "tag2"],
"strict": true
}
Files (ai-driven-development)
-
assets
-
templates
-
marketplace.json.template 498 B · in bundle
-
plugin.json.template 355 B · in bundle
-
-
-
references
-
opencode-plugins.md 9.4 KB
# OpenCode Plugins Reference Plugin system for [anomalyco/opencode](https://github.com/anomalyco/opencode) (v1.14.x). OpenCode plugins are TypeScript/JavaScript modules that extend the agent with custom tools and lifecycle hooks. They are fundamentally different from Claude Code plugins (`.claude-plugin/`): - **Distribution**: npm packages (or local files), not Anthropic marketplaces - **Format**: TypeScript modules with default-exported async functions, not `plugin.json` manifests - **Components**: custom tools, lifecycle hooks, custom slash commands — defined inline in code - **Loading**: auto-discovered from `.opencode/plugins/` and `~/.config/opencode/plugins/`, plus npm packages listed in `opencode.json` ## Contents - [Plugin Locations](#plugin-locations) - [Authoring a Plugin](#authoring-a-plugin) - [Plugin Manifest (package.json)](#plugin-manifest-packagejson) - [Plugin Capabilities](#plugin-capabilities) - [Custom Tools](#custom-tools) - [Lifecycle Hooks](#lifecycle-hooks) - [Distribution](#distribution) - [Installing Plugins](#installing-plugins) - [Removing Plugins](#removing-plugins) - [Validating Plugins](#validating-plugins) - [Comparison with Claude Code Plugins](#comparison-with-claude-code-plugins) ## Plugin Locations | Scope | Path | |-------|------| | Project | `<project>/.opencode/plugins/*.{ts,js}` | | Global | `~/.config/opencode/plugins/*.{ts,js}` | | npm | `opencode.json` → `"plugin": ["<package-name>", ...]` | OpenCode runs `bun install` at startup for npm plugins listed in `opencode.json`. node_modules cache: `~/.cache/opencode/node_modules/`. For project-local plugins that pull npm dependencies, place a `package.json` in `.opencode/`. OpenCode will install its dependencies via Bun automatically. ## Authoring a Plugin ```typescript // .opencode/plugins/my-plugin.ts import type { Plugin } from "@opencode-ai/plugin" export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree, app, }) => { return { tool: { execute: { before: async (input, output) => { // Block, modify, or log tool calls }, after: async (input, output) => { // Run side effects after tool completes }, }, }, event: async ({ event }) => { // Generic event subscription }, } } export default MyPlugin ``` ### Plugin context | Field | Type | Description | |-------|------|-------------| | `project` | object | Project metadata (root path, name) | | `client` | OpenCodeClient | SDK client (created via `createOpencodeClient()`) | | `$` | shell helper | Bun-style shell — `await $`prettier --write ${file}`` | | `directory` | string | CWD where OpenCode was launched | | `worktree` | string | Git worktree root | | `app` | object | App-level helpers (e.g., logging) | ## Plugin Manifest (package.json) For npm-distributed plugins: ```json { "name": "opencode-my-plugin", "version": "1.0.0", "description": "Adds X to OpenCode", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": ["opencode-plugin"], "license": "MIT", "repository": "https://github.com/me/opencode-my-plugin", "peerDependencies": { "@opencode-ai/plugin": "^1.0.0" }, "devDependencies": { "@opencode-ai/plugin": "^1.14.0", "typescript": "^5.5.0" } } ``` **Conventions:** - Prefix the package name with `opencode-` or scope it under your org - Always include the `opencode-plugin` keyword for discoverability - Use `peerDependencies` for `@opencode-ai/plugin` so the host's version is used ## Plugin Capabilities A single plugin file can expose: 1. **Custom tools** — add new tools the AI can call (with Zod schemas) 2. **Tool interceptors** — `tool.execute.before` / `after` 3. **Event subscribers** — react to ~25 lifecycle events 4. **Custom auth** — wire up new providers 5. **Compaction transforms** — rewrite messages or system prompts during context compaction (`experimental.session.compacting`) ## Custom Tools Custom tools live in `.opencode/tools/` (project) or `~/.config/opencode/tools/` (global), or are returned from a plugin. The filename becomes the tool name. ```typescript // .opencode/tools/database.ts import { tool } from "@opencode-ai/plugin" export default tool({ description: "Query the project database", args: { query: tool.schema.string().describe("SQL query to execute"), }, async execute(args, ctx) { // ctx exposes: agent name, session id, message id, directory, worktree return `Executed: ${args.query}` }, }) ``` Multiple tools per file: export named tools — they get the tool name `<filename>_<exportname>`: ```typescript export const add = tool({ ... }) export const multiply = tool({ ... }) // → tools "math_add" and "math_multiply" if file is math.ts ``` A custom tool with the same name as a built-in (`bash`, `edit`, etc.) **overrides** the built-in. ## Lifecycle Hooks See the dedicated reference at `../../hooks-management/references/opencode-hooks.md` for the full event catalog. Quick summary: - **Tool**: `tool.execute.before`, `tool.execute.after` - **Session**: `session.created`, `session.idle`, `session.compacted`, `session.deleted`, etc. - **Message**: `message.updated`, `message.part.updated`, etc. - **File**: `file.edited`, `file.watcher.updated` - **Permission**: `permission.asked`, `permission.replied` - **Other**: `command.executed`, `todo.updated`, `lsp.client.diagnostics`, `tui.toast.show`, `server.connected` ## Distribution There is **no centralized OpenCode plugin marketplace**. Plugins are distributed via: - **npm** with the `opencode-plugin` keyword - **GitHub repositories** users clone or reference - **Community aggregators** like [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) and [opencode.cafe](https://opencode.cafe) Notable community plugins (npm): - `@opencode-ai/plugin` — official SDK (the dependency, not a plugin itself) - `opencode-helicone-session` — usage tracking - `opencode-wakatime` — coding-time analytics - `opencode-antigravity-auth` — Google Antigravity OAuth bridge - `oh-my-opencode` — comprehensive bundle (agents, hooks, MCPs, skills) ## Installing Plugins ### From npm Add the package name to `opencode.json` and restart: ```json { "$schema": "https://opencode.ai/config.json", "plugin": [ "opencode-helicone-session", "opencode-wakatime", "@my-org/custom-plugin" ] } ``` OpenCode runs `bun install` automatically at next startup. Cached at `~/.cache/opencode/node_modules/`. ### From a local file Drop the `.ts` or `.js` file into `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (global). It will be loaded on next startup. ### Pin versions For npm plugins, pin in `opencode.json`: ```json { "plugin": ["opencode-helicone-session@1.2.3"] } ``` ## Removing Plugins **Always confirm with the user via `AskUserQuestion` before removing.** - **npm**: remove the entry from `opencode.json` `plugin` array. Restart OpenCode. (The package stays in `~/.cache/opencode/node_modules/` until cache clear.) - **Local file**: delete the file from `.opencode/plugins/` or `~/.config/opencode/plugins/`. ## Validating Plugins OpenCode does not ship a plugin validator CLI. Manual checks before publishing: - [ ] Default export is an async function returning a hooks object (or a `Plugin` value) - [ ] `package.json` includes `keywords: ["opencode-plugin"]` - [ ] `peerDependencies` lists `@opencode-ai/plugin` - [ ] `main` (and `types` if TypeScript) point at the bundled output - [ ] No top-level side effects (network, fs writes) on import - [ ] Throws in `tool.execute.before` are intentional (they block tool calls) - [ ] No hardcoded credentials — use env vars and `{env:VAR}` substitution - [ ] README documents required env vars and any opt-in `permission` rules ## Comparison with Claude Code Plugins | Aspect | Claude Code | OpenCode | |--------|-------------|----------| | Manifest | `.claude-plugin/plugin.json` | npm `package.json` (no separate manifest) | | Distribution | Marketplace JSON (`marketplace.json`), `/plugin marketplace add` | npm + GitHub direct linking | | Components | Commands, agents, skills, hooks, MCP — declared as files in dirs | Custom tools, lifecycle hooks — defined in TypeScript code | | Hook config | `hooks/hooks.json` (shell commands) | `tool.execute.before/after` plugin functions | | Install scope | Project (`.claude/`), user, marketplace | Project (`.opencode/plugins/`), user (`~/.config/opencode/plugins/`), npm | | `${CLAUDE_PLUGIN_ROOT}` | Yes | No equivalent — plugins use `directory` / `worktree` from context | | Runtime | Shell commands | Bun (TypeScript) | | Submission | Anthropic Plugin Directory | None — register on awesome-opencode / npm | | Type safety | None (markdown + JSON) | Full TS types via `@opencode-ai/plugin` | ### Equivalences | Claude Code plugin component | OpenCode equivalent | |------------------------------|---------------------| | `commands/*.md` | `command` block in `opencode.json` or `.opencode/commands/*.md` | | `agents/*.md` | `agent` block in `opencode.json` or `.opencode/agents/*.md` | | `skills/*/SKILL.md` | `.opencode/skills/*/SKILL.md` (compatible format) | | `hooks/hooks.json` | `tool.execute.*` and `event` handlers in plugin | | `.mcp.json` | `mcp` block in `opencode.json` | ## Sources - https://opencode.ai/docs/plugins/ - https://opencode.ai/docs/custom-tools/ - https://opencode.ai/docs/ecosystem/ - https://www.npmjs.com/package/@opencode-ai/plugin - https://lushbinary.com/blog/opencode-plugin-development-custom-tools-hooks-guide/ -
plugin-guide.md 16.5 KB
# Claude Code Plugins Complete Reference (2026-04) ## Table of Contents 1. [Plugin Structure](#plugin-structure) 2. [Plugin Manifest](#plugin-manifest) 3. [Plugin Components](#plugin-components) 4. [Background Monitors](#background-monitors) 5. [Themes](#themes) 6. [User Configuration](#user-configuration) 7. [Channels](#channels) 8. [Dependencies and Versioning](#dependencies-and-versioning) 9. [Marketplace Structure](#marketplace-structure) 10. [Marketplace Configuration](#marketplace-configuration) 11. [Source Types](#source-types) 12. [CLI Commands](#cli-commands) 13. [Plugin Cache and Path Traversal](#plugin-cache-and-path-traversal) 14. [Official Submission](#official-submission) --- ## Plugin Structure ``` my-plugin/ ├── .claude-plugin/ │ └── plugin.json # Optional: Plugin manifest. If absent, components are auto-discovered and the directory name becomes the plugin name. ├── skills/ # Optional: Agent Skills (preferred over commands/) │ └── */SKILL.md ├── commands/ # Optional: Skills as flat .md files (legacy / simple commands) │ └── *.md ├── agents/ # Optional: Specialized subagents │ └── *.md ├── output-styles/ # Optional: Output style definitions (2026) │ └── *.md ├── themes/ # Optional: Color themes (2026) │ └── *.json ├── monitors/ # Optional: Background monitors (2026, requires v2.1.105+) │ └── monitors.json ├── hooks/ # Optional: Event handlers │ └── hooks.json ├── bin/ # Optional: Executables added to PATH while plugin is enabled (2026) ├── settings.json # Optional: Default agent / subagentStatusLine config (2026) ├── .mcp.json # Optional: MCP server config ├── .lsp.json # Optional: LSP server config (2026, official LSP support since v2.0.74) ├── scripts/ # Optional: Hook helpers and utilities ├── package.json # Optional: Auto-installed deps when plugin enables (2026) ├── README.md # Recommended ├── CHANGELOG.md # Recommended └── LICENSE # Recommended ``` > Components live at the plugin **root**, not inside `.claude-plugin/`. Only `plugin.json` belongs in `.claude-plugin/`. --- ## Plugin Manifest File: `.claude-plugin/plugin.json` ### Required Fields (Schema) ```json { "name": "plugin-name" } ``` ### Strongly Recommended Metadata ```json { "name": "plugin-name", "description": "Clear explanation of what the plugin does", "version": "1.0.0", "author": { "name": "Author Name" } } ``` ### Optional Fields ```json { "author": { "email": "email@example.com" }, "homepage": "https://github.com/user/plugin", "repository": "https://github.com/user/plugin", "license": "MIT", "keywords": ["tag1", "tag2"] } ``` ### Component Path Fields (Optional) Use these only for non-standard locations. Paths must be relative to the plugin root and start with `./`. **2026 additions: `outputStyles`, `themes`, `lspServers`, `monitors`, `userConfig`, `channels`, `dependencies`.** ```json { "commands": ["./custom/commands/extra.md"], "agents": "./custom/agents/", "hooks": "./hooks/hooks.json", "mcpServers": "./mcp.json", "outputStyles": "./styles/", "themes": "./themes/", "lspServers": "./.lsp.json", "monitors": "./monitors.json", "skills": ["./skills/", "./extras/"] } ``` | Field | Type | Notes | |-------|------|-------| | `skills` | string\|array | Custom directories with `<name>/SKILL.md`. Replaces default `skills/`. To keep default and add more, include both: `"skills": ["./skills/", "./extras/"]`. If a skill path points to the plugin root (e.g. `["./"]`), the frontmatter `name` is used as invocation name. | | `commands` | string\|array | Flat-`.md` skill files | | `agents` | string\|array | Replaces default `agents/` | | `hooks` | string\|array\|object | Path(s) or inline config | | `mcpServers` | string\|array\|object | Path(s) or inline config | | `outputStyles` | string\|array | 2026 | | `themes` | string\|array | 2026 — color theme JSON files | | `lspServers` | string\|array\|object | 2026 — LSP configs | | `monitors` | string\|array | 2026 — background monitors (v2.1.105+) | | `userConfig` | object | 2026 — values prompted at enable time | | `channels` | array | 2026 — Telegram/Slack/Discord-style channel declarations | | `dependencies` | array | 2026 — other plugins required (with optional semver constraints) | ### Version Format Semantic versioning: MAJOR.MINOR.PATCH. Setting `version` in `plugin.json` pins the cache key — bump it on every release. **If you omit `version`, Claude Code falls back to the git commit SHA**, so every commit is treated as a new version (good for internal/team plugins under active development). Resolution order: `plugin.json#version` → marketplace entry `version` → git SHA → `unknown`. --- ## Plugin Components ### Commands (commands/*.md) Markdown files with YAML frontmatter become slash commands. ```markdown --- description: Short description of command --- # Command Title Instructions for Claude when command is invoked. ``` ### Agents (agents/*.md) Specialized agent definitions. ```markdown --- description: Agent purpose and specialty --- # Agent Name Detailed instructions and expertise for this agent. ``` ### Skills (skills/*/SKILL.md) Agent skills with frontmatter. ```markdown --- name: skill-name description: What the skill does and when to use it --- # Skill Documentation ``` ### Hooks (hooks/hooks.json) Event handlers for Claude actions. ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh", "description": "Validate after edits" } ] } ] } } ``` **Hook Events (2026, 28 events):** SessionStart, SessionEnd, InstructionsLoaded, UserPromptSubmit, UserPromptExpansion, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, Stop, StopFailure, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle, ConfigChange, FileChanged, CwdChanged, PreCompact, PostCompact, WorktreeCreate, WorktreeRemove, Elicitation, ElicitationResult, Notification. **Hook handler types:** `command`, `http`, `mcp_tool`, `prompt`, `agent`. ### MCP Servers (.mcp.json) ```json { "mcpServers": { "server-name": { "command": "node", "args": ["./servers/server.js"], "env": { "VAR": "${ENV_VAR}" } } } } ``` ### LSP Servers (.lsp.json) Official since Claude Code v2.0.74. Provides instant diagnostics, go-to-definition, find references, and hover info. ```json { "go": { "command": "gopls", "args": ["serve"], "extensionToLanguage": { ".go": "go" }, "transport": "stdio", "env": { "GOFLAGS": "-mod=vendor" }, "initializationOptions": {}, "settings": {}, "workspaceFolder": ".", "startupTimeout": 30000, "shutdownTimeout": 5000, "restartOnCrash": true, "maxRestarts": 3 } } ``` **Note:** Users must install the language server binary locally. If `Executable not found in $PATH` appears in `/plugin` Errors tab, install the binary (e.g. `npm install -g typescript-language-server typescript`). --- ## Background Monitors **(2026, requires v2.1.105+)** Monitors run a shell command for the lifetime of the session and deliver every stdout line to Claude as a notification. `monitors/monitors.json`: ```json [ { "name": "deploy-status", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/poll-deploy.sh ${user_config.api_endpoint}", "description": "Deployment status changes" }, { "name": "error-log", "command": "tail -F ./logs/error.log", "description": "Application error log", "when": "on-skill-invoke:debug" } ] ``` Required fields: `name`, `command`, `description`. Optional: `when` (`"always"` default, or `"on-skill-invoke:<skill-name>"`). Variable substitutions in `command`: `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}`, `${user_config.*}`, any `${ENV_VAR}`. Disabling a plugin mid-session does NOT stop running monitors — they stop at session end. Plugin monitors run unsandboxed at the same trust level as hooks. --- ## Themes **(2026)** Plugins can ship color themes that appear in `/theme` alongside built-in presets. `themes/dracula.json`: ```json { "name": "Dracula", "base": "dark", "overrides": { "claude": "#bd93f9", "error": "#ff5555", "success": "#50fa7b" } } ``` Plugin themes are read-only; `Ctrl+E` on a plugin theme in `/theme` copies it to `~/.claude/themes/` for editing. --- ## User Configuration **(2026)** Plugins can prompt for values at enable time instead of requiring users to hand-edit `settings.json`. ```json { "userConfig": { "api_endpoint": { "type": "string", "title": "API endpoint", "description": "Your team's API endpoint" }, "api_token": { "type": "string", "title": "API token", "description": "API authentication token", "sensitive": true } } } ``` Field types: `string`, `number`, `boolean`, `directory`, `file`. Sensitive values go to the system keychain (or `~/.claude/.credentials.json`); non-sensitive values persist in `settings.json` under `pluginConfigs[<plugin-id>].options`. **Keychain has ~2KB shared limit with OAuth tokens — keep sensitive values small.** Available as `${user_config.KEY}` substitution in MCP/LSP/hook/monitor configs (and skill/agent content for non-sensitive). Also exported as `CLAUDE_PLUGIN_OPTION_<KEY>` env vars to subprocesses. --- ## Channels **(2026)** Declare message channels that inject content into the conversation (Telegram, Slack, Discord style). Each channel binds to an MCP server provided by the plugin. ```json { "channels": [ { "server": "telegram", "userConfig": { "bot_token": { "type": "string", "title": "Bot token", "sensitive": true }, "owner_id": { "type": "string", "title": "Owner ID" } } } ] } ``` `server` must match a key in the plugin's `mcpServers`. --- ## Dependencies and Versioning **(2026)** Plugins can require other plugins: ```json { "dependencies": [ "helper-lib", { "name": "secrets-vault", "version": "~2.1.0" } ] } ``` Plugins pinned by another plugin's version constraint auto-update to the highest satisfying git tag. Use `claude plugin tag` to cut release tags from the plugin directory. `${CLAUDE_PLUGIN_DATA}` is a persistent directory (`~/.claude/plugins/data/{id}/`) that survives plugin updates. Use it for `node_modules`, Python venvs, caches, and generated files. Pattern: compare bundled `package.json` against a copy in `${CLAUDE_PLUGIN_DATA}` and reinstall when they differ. --- ## Marketplace Structure ``` marketplace/ ├── .claude-plugin/ │ └── marketplace.json # Marketplace catalog ├── plugins/ # Optional: hosted plugins │ └── plugin-name/ └── README.md ``` --- ## Marketplace Configuration File: `.claude-plugin/marketplace.json` ```json { "name": "marketplace-name", "owner": { "name": "Owner Name", "email": "email@example.com" }, "metadata": { "description": "Marketplace description", "version": "1.0.0", "homepage": "https://github.com/user/marketplace", "pluginRoot": "./plugins" }, "plugins": [ { "name": "plugin-name", "source": "./plugins/plugin-name", "description": "Plugin description", "version": "1.0.0", "author": { "name": "Author" }, "category": "productivity", "keywords": ["tag1", "tag2"], "tags": ["tag1", "tag2"], "strict": true } ] } ``` **Note:** Plugin entries accept all `plugin.json` fields as optional metadata, plus marketplace-only fields: `source`, `category`, `tags`, and `strict`. When `strict` is `false`, the marketplace entry can serve as the full manifest if the plugin lacks `plugin.json`. ### Advanced Plugin Entry Override component locations: ```json { "name": "plugin-name", "source": "./plugins/plugin", "commands": ["./commands/core/", "./commands/extra/"], "agents": ["./agents/agent1.md"], "hooks": { "hooks": { "PostToolUse": [...] } }, "mcpServers": { "server": {...} }, "strict": false } ``` --- ## Source Types ### Relative Path ```json { "source": "./plugins/local-plugin" } ``` ### GitHub ```json { "source": { "source": "github", "repo": "owner/repo", "ref": "main" } } ``` ### Git URL ```json { "source": { "source": "url", "url": "https://gitlab.com/team/plugin.git", "ref": "v1.0.0" } } ``` --- ## CLI Commands ### Plugin Management ```bash /plugin # Browse plugins /plugin install <name>@<marketplace> /plugin uninstall <name>@<marketplace> /plugin enable <name>@<marketplace> /plugin disable <name>@<marketplace> /reload-plugins # Reload after editing a plugin claude plugin install <name>@<marketplace> --scope user|project|local claude plugin uninstall <name>@<marketplace> --scope project [--keep-data] claude plugin enable <name>@<marketplace> --scope user claude plugin disable <name>@<marketplace> --scope user claude plugin update <name>@<marketplace> --scope user|project|local|managed claude plugin list [--json] [--available] claude plugin tag [--push] [--dry-run] [--force] # 2026 — cut release git tag claude --plugin-dir ./my-plugin # Test a plugin without installing ``` ### Marketplace Management ```bash /plugin marketplace add <source> /plugin marketplace list /plugin marketplace update <name> /plugin marketplace remove <name> ``` ### Validation ```bash claude plugin validate <path> ``` --- ## Plugin Cache and Path Traversal **(2026)** Marketplace plugins are copied to `~/.claude/plugins/cache/<id>/<version>/` rather than used in place. Each installed version is a separate directory; orphaned versions are auto-removed after **7 days** to allow concurrent sessions to keep running with the older version. **Path traversal limit:** Installed plugins cannot reference files outside their directory. Paths like `../shared-utils` won't work after install. Workaround: create symlinks **inside** your plugin directory pointing at external files; symlinks are preserved in the cache and resolved at runtime. `${CLAUDE_PLUGIN_ROOT}` resolves to the cache install path (changes with every version). `${CLAUDE_PLUGIN_DATA}` resolves to `~/.claude/plugins/data/{id}/` — persistent across updates. `/plugin` interface shows the data directory size and prompts before deletion on uninstall. CLI deletes by default; pass `--keep-data` to preserve. --- ## Official Submission ### Anthropic Plugin Submission Form The official submission form requires the following fields: | Field | Required | Description | Auto-gathered | |-------|----------|-------------|---------------| | Link to Plugin | Yes | GitHub repository URL | `gh repo view --json url` | | Full SHA | Yes | Commit SHA to be reviewed | `git rev-parse HEAD` | | Plugin Homepage | Yes | Documentation/landing page | plugin.json homepage | | Company/Organization URL | Yes | Your company website | `--company-url` flag | | Primary Contact Email | Yes | Email for communication | `--email` flag | | Plugin Name | Yes | Name for Plugin Directory | plugin.json name | | Plugin Description | Yes | 50-100 words | plugin.json description | ### Prepare Submission Command ```bash python scripts/prepare_submission.py ./my-plugin \ --email your@email.com \ --company-url https://yourcompany.com \ --copy-sha ``` ### Prerequisites - Plugin pushed to GitHub - `gh` CLI installed and authenticated (`gh auth login`) - Working directory clean (commit all changes) - Description is 50-100 words ### Requirements - Clear, comprehensive documentation - Well-tested functionality - Security best practices followed - Professional code quality - Responsive maintainer ### Quality Checklist - [ ] plugin.json has all required fields - [ ] README.md is comprehensive - [ ] All commands work correctly - [ ] Agents behave as expected - [ ] Hooks trigger appropriately - [ ] No API keys or secrets in code - [ ] LICENSE file included - [ ] Version follows semver - [ ] Description is 50-100 words - [ ] Plugin pushed to GitHub - [ ] Working directory is clean --- ## Environment Variables **`${CLAUDE_PLUGIN_ROOT}`** resolves to the plugin's installation directory. Use it in hooks, MCP configs, and scripts to avoid path errors after install.
-
-
scripts
-
init_marketplace.py 6 KB
#!/usr/bin/env python3 """ Initialize a new Claude Code plugin marketplace. Usage: python init_marketplace.py <marketplace-name> [--path <output-directory>] [--with-plugin <plugin-name>] """ import argparse import json import os import sys from pathlib import Path def create_marketplace_json(name: str, owner_name: str = "Your Name") -> dict: """Create the marketplace.json manifest.""" return { "name": name, "owner": { "name": owner_name }, "metadata": { "description": f"TODO: Add description for {name} marketplace", "version": "1.0.0" }, "plugins": [] } def create_marketplace_readme(name: str) -> str: """Create README.md content for marketplace.""" return f"""# {name.replace('-', ' ').title()} Marketplace A Claude Code plugin marketplace. ## Installation Add this marketplace to Claude Code: ```bash /plugin marketplace add <github-username>/{name} ``` Or with a full URL: ```bash /plugin marketplace add https://github.com/<username>/{name} ``` ## Available Plugins | Plugin | Description | Version | |--------|-------------|---------| | TODO | Add plugins | 1.0.0 | ## Adding a Plugin To add a new plugin to this marketplace: 1. Create your plugin in the `plugins/` directory 2. Add an entry to `.claude-plugin/marketplace.json` 3. Update this README ## Contributing Contributions welcome! Please open a pull request. ## License MIT License """ def add_plugin_to_marketplace(marketplace_json: dict, plugin_name: str, plugin_path: str) -> dict: """Add a plugin entry to marketplace.json.""" plugin_entry = { "name": plugin_name, "source": plugin_path, "description": f"TODO: Add description for {plugin_name}", "version": "1.0.0" } marketplace_json["plugins"].append(plugin_entry) return marketplace_json def init_marketplace( name: str, output_path: str, owner_name: str = "Your Name", initial_plugins: list = None ) -> Path: """Initialize a new marketplace directory structure.""" marketplace_dir = Path(output_path) / name if marketplace_dir.exists(): raise FileExistsError(f"Directory already exists: {marketplace_dir}") marketplace_dir.mkdir(parents=True) # Create .claude-plugin directory claude_plugin_dir = marketplace_dir / ".claude-plugin" claude_plugin_dir.mkdir() # Create marketplace.json marketplace_json = create_marketplace_json(name, owner_name) # Add initial plugins if provided if initial_plugins: for plugin_name in initial_plugins: plugin_path = f"./plugins/{plugin_name}" marketplace_json = add_plugin_to_marketplace( marketplace_json, plugin_name, plugin_path ) # Create plugin directory plugin_dir = marketplace_dir / "plugins" / plugin_name plugin_dir.mkdir(parents=True) # Create minimal plugin structure (plugin_dir / ".claude-plugin").mkdir() with open(plugin_dir / ".claude-plugin" / "plugin.json", "w") as f: json.dump({ "name": plugin_name, "description": f"TODO: Add description for {plugin_name}", "version": "1.0.0", "author": {"name": owner_name} }, f, indent=2) with open(plugin_dir / "README.md", "w") as f: f.write(f"# {plugin_name}\n\nTODO: Add documentation\n") with open(claude_plugin_dir / "marketplace.json", "w") as f: json.dump(marketplace_json, f, indent=2) # Create plugins directory plugins_dir = marketplace_dir / "plugins" if not plugins_dir.exists(): plugins_dir.mkdir() with open(plugins_dir / ".gitkeep", "w") as f: f.write("") # Create README.md with open(marketplace_dir / "README.md", "w") as f: f.write(create_marketplace_readme(name)) # Create LICENSE with open(marketplace_dir / "LICENSE", "w") as f: f.write("MIT License\n\nCopyright (c) 2024\n\nTODO: Add full license text") return marketplace_dir def main(): parser = argparse.ArgumentParser( description="Initialize a new Claude Code plugin marketplace" ) parser.add_argument( "marketplace_name", help="Name of the marketplace (kebab-case)" ) parser.add_argument( "--path", "-p", default=".", help="Output directory (default: current directory)" ) parser.add_argument( "--owner", "-o", default="Your Name", help="Owner name for marketplace.json" ) parser.add_argument( "--with-plugin", action="append", dest="plugins", help="Create marketplace with initial plugin(s) (can be used multiple times)" ) args = parser.parse_args() try: marketplace_dir = init_marketplace( name=args.marketplace_name, output_path=args.path, owner_name=args.owner, initial_plugins=args.plugins ) print(f"✅ Marketplace '{args.marketplace_name}' initialized at: {marketplace_dir}") print("\nCreated structure:") for root, dirs, files in os.walk(marketplace_dir): level = root.replace(str(marketplace_dir), '').count(os.sep) indent = ' ' * level print(f"{indent}{os.path.basename(root)}/") subindent = ' ' * (level + 1) for file in files: print(f"{subindent}{file}") print("\nNext steps:") print("1. Edit .claude-plugin/marketplace.json with your details") print("2. Add plugins to the plugins/ directory") print("3. Update README.md with plugin list") print("4. Push to GitHub/GitLab for distribution") print("\nUsers can add your marketplace with:") print(f" /plugin marketplace add <username>/{args.marketplace_name}") except FileExistsError as e: print(f"❌ Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() -
init_plugin.py 8.2 KB
#!/usr/bin/env python3 """ Initialize a new Claude Code plugin with proper structure. Usage: python init_plugin.py <plugin-name> [--path <output-directory>] [--with-commands] [--with-agents] [--with-skills] [--with-hooks] [--with-mcp] """ import argparse import json import os import sys from pathlib import Path def create_plugin_json(plugin_name: str, author_name: str = "Your Name") -> dict: """Create the plugin.json manifest.""" return { "name": plugin_name, "description": f"TODO: Add description for {plugin_name}", "version": "1.0.0", "author": { "name": author_name }, "keywords": [] } def create_readme(plugin_name: str) -> str: """Create README.md content.""" return f"""# {plugin_name.replace('-', ' ').title()} ## Description TODO: Describe what this plugin does. ## Installation ### Add the Marketplace ```bash /plugin marketplace add <marketplace-source> ``` ### Install the Plugin ```bash /plugin install {plugin_name}@<marketplace> ``` ## Usage TODO: Describe how to use the plugin. ## Features - Feature 1 - Feature 2 ## License MIT License """ def create_command_template(command_name: str) -> str: """Create a command markdown file.""" return f"""--- description: TODO: Describe what /{command_name} does --- # {command_name.replace('-', ' ').title()} Command TODO: Add instructions for Claude when this command is invoked. """ def create_agent_template(agent_name: str) -> str: """Create an agent markdown file.""" return f"""--- description: TODO: Describe this agent's specialty --- # {agent_name.replace('-', ' ').title()} Agent TODO: Add detailed instructions and expertise for this agent. ## Capabilities - Capability 1 - Capability 2 ## When to Use Use this agent when... """ def create_skill_template(skill_name: str) -> str: """Create a SKILL.md file.""" return f"""--- name: {skill_name} description: TODO: What this skill does. Use when ... --- # {skill_name.replace('-', ' ').title()} ## Capability TODO: Describe what this skill enables. ## Usage TODO: How to use this skill. """ def create_hooks_json() -> dict: """Create hooks.json template.""" return { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh", "description": "TODO: Replace with actual hook" } ] } ] } } def create_mcp_json() -> dict: """Create .mcp.json template.""" return { "mcpServers": { "example-server": { "command": "node", "args": ["./servers/server.js"], "env": {} } } } def init_plugin( plugin_name: str, output_path: str, with_commands: bool = True, with_agents: bool = False, with_skills: bool = False, with_hooks: bool = False, with_mcp: bool = False, author_name: str = "Your Name" ) -> Path: """Initialize a new plugin directory structure.""" # Validate plugin name if not plugin_name or not plugin_name.replace('-', '').replace('_', '').isalnum(): raise ValueError(f"Invalid plugin name: {plugin_name}. Use kebab-case with alphanumeric characters.") # Create plugin directory plugin_dir = Path(output_path) / plugin_name if plugin_dir.exists(): raise FileExistsError(f"Directory already exists: {plugin_dir}") plugin_dir.mkdir(parents=True) # Create .claude-plugin directory and plugin.json claude_plugin_dir = plugin_dir / ".claude-plugin" claude_plugin_dir.mkdir() plugin_json = create_plugin_json(plugin_name, author_name) with open(claude_plugin_dir / "plugin.json", "w") as f: json.dump(plugin_json, f, indent=2) # Create README.md with open(plugin_dir / "README.md", "w") as f: f.write(create_readme(plugin_name)) # Create LICENSE with open(plugin_dir / "LICENSE", "w") as f: f.write("MIT License\n\nCopyright (c) 2024\n\nTODO: Add full license text") # Create optional components if with_commands: commands_dir = plugin_dir / "commands" commands_dir.mkdir() with open(commands_dir / "example.md", "w") as f: f.write(create_command_template("example")) if with_agents: agents_dir = plugin_dir / "agents" agents_dir.mkdir() with open(agents_dir / "example-agent.md", "w") as f: f.write(create_agent_template("example-agent")) if with_skills: skills_dir = plugin_dir / "skills" / "example-skill" skills_dir.mkdir(parents=True) with open(skills_dir / "SKILL.md", "w") as f: f.write(create_skill_template("example-skill")) if with_hooks: hooks_dir = plugin_dir / "hooks" hooks_dir.mkdir() with open(hooks_dir / "hooks.json", "w") as f: json.dump(create_hooks_json(), f, indent=2) if with_mcp: with open(plugin_dir / ".mcp.json", "w") as f: json.dump(create_mcp_json(), f, indent=2) servers_dir = plugin_dir / "servers" servers_dir.mkdir() with open(servers_dir / ".gitkeep", "w") as f: f.write("") return plugin_dir def main(): parser = argparse.ArgumentParser( description="Initialize a new Claude Code plugin" ) parser.add_argument("plugin_name", help="Name of the plugin (kebab-case)") parser.add_argument( "--path", "-p", default=".", help="Output directory (default: current directory)" ) parser.add_argument( "--author", "-a", default="Your Name", help="Author name for plugin.json" ) parser.add_argument( "--with-commands", action="store_true", default=True, help="Include commands directory (default: True)" ) parser.add_argument( "--no-commands", action="store_true", help="Exclude commands directory" ) parser.add_argument( "--with-agents", action="store_true", help="Include agents directory" ) parser.add_argument( "--with-skills", action="store_true", help="Include skills directory" ) parser.add_argument( "--with-hooks", action="store_true", help="Include hooks directory" ) parser.add_argument( "--with-mcp", action="store_true", help="Include MCP server configuration" ) parser.add_argument( "--all", action="store_true", help="Include all optional components" ) args = parser.parse_args() with_commands = not args.no_commands with_agents = args.with_agents or args.all with_skills = args.with_skills or args.all with_hooks = args.with_hooks or args.all with_mcp = args.with_mcp or args.all try: plugin_dir = init_plugin( plugin_name=args.plugin_name, output_path=args.path, with_commands=with_commands, with_agents=with_agents, with_skills=with_skills, with_hooks=with_hooks, with_mcp=with_mcp, author_name=args.author ) print(f"✅ Plugin '{args.plugin_name}' initialized at: {plugin_dir}") print("\nCreated structure:") for root, dirs, files in os.walk(plugin_dir): level = root.replace(str(plugin_dir), '').count(os.sep) indent = ' ' * level print(f"{indent}{os.path.basename(root)}/") subindent = ' ' * (level + 1) for file in files: print(f"{subindent}{file}") print("\nNext steps:") print("1. Edit .claude-plugin/plugin.json with your plugin details") print("2. Update README.md with documentation") print("3. Add your commands, agents, skills, hooks, or MCP servers") print("4. Run validation: claude plugin validate .") except (ValueError, FileExistsError) as e: print(f"❌ Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() -
prepare_submission.py 15.1 KB
#!/usr/bin/env python3 """ Prepare a Claude Code plugin for submission to the Anthropic Plugin Directory. This script gathers all information needed for the submission form and can automatically fetch repository details using `gh` CLI. Usage: python prepare_submission.py <plugin-path> [options] Options: --email EMAIL Primary contact email (required for submission) --company-url URL Company/Organization URL (required for submission) --open-form Open the submission form in browser --json Output as JSON --output FILE Save submission info to file """ import argparse import json import subprocess import sys import urllib.parse from pathlib import Path from typing import Optional # Anthropic Plugin Submission Form URL SUBMISSION_FORM_URL = "https://forms.gle/YourFormID" # Replace with actual form URL when known # Documentation URLs PLUGIN_DOCS_URL = "https://code.claude.com/docs/en/plugins" MARKETPLACE_DOCS_URL = "https://code.claude.com/docs/en/plugin-marketplaces" REFERENCE_DOCS_URL = "https://code.claude.com/docs/en/plugins-reference" def run_command(cmd: list[str], cwd: str = None) -> tuple[bool, str]: """Run a shell command and return (success, output).""" try: result = subprocess.run( cmd, capture_output=True, text=True, cwd=cwd, timeout=30 ) return result.returncode == 0, result.stdout.strip() except (subprocess.TimeoutExpired, FileNotFoundError) as e: return False, str(e) def get_git_info(plugin_path: str) -> dict: """Get git repository information using git and gh CLI.""" git_info = { "repo_url": None, "full_sha": None, "branch": None, "is_clean": False, "has_remote": False, "errors": [] } # Check if it's a git repository success, _ = run_command(["git", "rev-parse", "--git-dir"], cwd=plugin_path) if not success: git_info["errors"].append("Not a git repository") return git_info # Get full SHA success, sha = run_command(["git", "rev-parse", "HEAD"], cwd=plugin_path) if success: git_info["full_sha"] = sha else: git_info["errors"].append("Could not get commit SHA") # Get current branch success, branch = run_command(["git", "branch", "--show-current"], cwd=plugin_path) if success: git_info["branch"] = branch # Check if working directory is clean success, status = run_command(["git", "status", "--porcelain"], cwd=plugin_path) if success: git_info["is_clean"] = len(status) == 0 # Try to get remote URL using gh success, repo_url = run_command(["gh", "repo", "view", "--json", "url", "-q", ".url"], cwd=plugin_path) if success and repo_url: git_info["repo_url"] = repo_url git_info["has_remote"] = True else: # Fallback to git remote success, remote_url = run_command(["git", "remote", "get-url", "origin"], cwd=plugin_path) if success and remote_url: # Convert SSH URL to HTTPS if needed if remote_url.startswith("git@github.com:"): remote_url = remote_url.replace("git@github.com:", "https://github.com/") if remote_url.endswith(".git"): remote_url = remote_url[:-4] git_info["repo_url"] = remote_url git_info["has_remote"] = True else: git_info["errors"].append("No remote repository found. Push to GitHub first.") return git_info def gather_plugin_info(plugin_path: str, email: str = None, company_url: str = None) -> dict: """Gather all information needed for plugin submission.""" plugin_dir = Path(plugin_path).resolve() info = { "plugin_path": str(plugin_dir), "validation": {"passed": True, "issues": [], "warnings": []}, "metadata": {}, "git": {}, "components": {}, "form_fields": {} } # Get git information info["git"] = get_git_info(str(plugin_dir)) # Read plugin.json plugin_json_path = plugin_dir / ".claude-plugin" / "plugin.json" if not plugin_json_path.exists(): info["validation"]["passed"] = False info["validation"]["issues"].append("Missing .claude-plugin/plugin.json") # Set empty form fields for display info["form_fields"] = { "link_to_plugin": info["git"].get("repo_url", ""), "full_sha": info["git"].get("full_sha", ""), "plugin_homepage": "", "company_url": company_url or "", "primary_contact_email": email or "", "plugin_name": "", "plugin_description": "", } return info try: with open(plugin_json_path) as f: plugin_data = json.load(f) info["metadata"] = plugin_data except json.JSONDecodeError as e: info["validation"]["passed"] = False info["validation"]["issues"].append(f"Invalid plugin.json: {e}") # Set empty form fields for display info["form_fields"] = { "link_to_plugin": info["git"].get("repo_url", ""), "full_sha": info["git"].get("full_sha", ""), "plugin_homepage": "", "company_url": company_url or "", "primary_contact_email": email or "", "plugin_name": "", "plugin_description": "", } return info # Check required fields required = ["name", "description", "version", "author"] for field in required: if field not in plugin_data: info["validation"]["passed"] = False info["validation"]["issues"].append(f"Missing required field: {field}") # Check for TODO placeholders description = plugin_data.get("description", "") if "TODO" in description: info["validation"]["passed"] = False info["validation"]["issues"].append("Description contains TODO placeholder") # Check description length (50-100 words recommended) word_count = len(description.split()) if word_count < 50: info["validation"]["warnings"].append(f"Description is only {word_count} words (50-100 recommended)") elif word_count > 100: info["validation"]["warnings"].append(f"Description is {word_count} words (50-100 recommended)") # Gather component info components = info["components"] # Commands commands_dir = plugin_dir / "commands" if commands_dir.exists(): commands = list(commands_dir.glob("*.md")) components["commands"] = [c.stem for c in commands] # Agents agents_dir = plugin_dir / "agents" if agents_dir.exists(): agents = list(agents_dir.glob("*.md")) components["agents"] = [a.stem for a in agents] # Skills skills_dir = plugin_dir / "skills" if skills_dir.exists(): skills = [d.name for d in skills_dir.iterdir() if d.is_dir()] components["skills"] = skills # Hooks hooks_json = plugin_dir / "hooks" / "hooks.json" if hooks_json.exists(): components["hooks"] = True # MCP servers mcp_json = plugin_dir / ".mcp.json" if mcp_json.exists(): try: with open(mcp_json) as f: mcp_data = json.load(f) components["mcp_servers"] = list(mcp_data.get("mcpServers", {}).keys()) except json.JSONDecodeError: pass # Check README readme = plugin_dir / "README.md" if not readme.exists(): info["validation"]["passed"] = False info["validation"]["issues"].append("Missing README.md") elif "TODO" in readme.read_text(): info["validation"]["warnings"].append("README.md contains TODO placeholders") # Check LICENSE license_file = plugin_dir / "LICENSE" if not license_file.exists(): info["validation"]["warnings"].append("Missing LICENSE file (recommended)") # Check git requirements if not info["git"]["has_remote"]: info["validation"]["passed"] = False info["validation"]["issues"].append("Plugin must be pushed to GitHub before submission") if not info["git"]["is_clean"]: info["validation"]["warnings"].append("Working directory has uncommitted changes") # Prepare form fields info["form_fields"] = { "link_to_plugin": info["git"].get("repo_url", ""), "full_sha": info["git"].get("full_sha", ""), "plugin_homepage": plugin_data.get("homepage", "") or info["git"].get("repo_url", ""), "company_url": company_url or "", "primary_contact_email": email or plugin_data.get("author", {}).get("email", ""), "plugin_name": plugin_data.get("name", ""), "plugin_description": description, } # Validate form fields if not info["form_fields"]["link_to_plugin"]: info["validation"]["issues"].append("Link to Plugin is required") if not info["form_fields"]["full_sha"]: info["validation"]["issues"].append("Full SHA is required") if not info["form_fields"]["plugin_homepage"]: info["validation"]["issues"].append("Plugin Homepage is required") if not info["form_fields"]["company_url"]: info["validation"]["warnings"].append("Company/Organization URL not provided (use --company-url)") if not info["form_fields"]["primary_contact_email"]: info["validation"]["warnings"].append("Primary Contact Email not provided (use --email)") return info def print_form_fields(info: dict): """Print form fields in a copy-paste friendly format.""" fields = info["form_fields"] validation = info["validation"] git = info["git"] print("\n" + "=" * 70) print("📋 ANTHROPIC PLUGIN SUBMISSION FORM DATA") print("=" * 70) # Validation status if validation["passed"]: print("\n✅ Validation: PASSED") else: print("\n❌ Validation: FAILED") for issue in validation["issues"]: print(f" ❌ {issue}") if validation["warnings"]: print("\n⚠️ Warnings:") for warning in validation["warnings"]: print(f" ⚠️ {warning}") # Git info print(f"\n📂 Repository Info:") print(f" Branch: {git.get('branch', 'N/A')}") print(f" Clean: {'Yes' if git.get('is_clean') else 'No (has uncommitted changes)'}") # Form fields print("\n" + "-" * 70) print("📝 FORM FIELDS (copy these to the submission form)") print("-" * 70) print(f"\n1. Link to Plugin *") print(f" {fields['link_to_plugin'] or '[REQUIRED - push to GitHub first]'}") print(f"\n2. Full SHA of version you want added *") print(f" {fields['full_sha'] or '[REQUIRED - commit first]'}") print(f"\n3. Plugin Homepage *") print(f" {fields['plugin_homepage'] or '[REQUIRED]'}") print(f"\n4. Company/Organization URL *") print(f" {fields['company_url'] or '[REQUIRED - use --company-url]'}") print(f"\n5. Primary Contact Email *") print(f" {fields['primary_contact_email'] or '[REQUIRED - use --email]'}") print(f"\n6. Plugin Name *") print(f" {fields['plugin_name']}") print(f"\n7. Plugin Description * (50-100 words)") print(f" {fields['plugin_description']}") # Components summary components = info["components"] if components: print("\n" + "-" * 70) print("🧩 Plugin Components (for reference)") print("-" * 70) if components.get("commands"): print(f" Commands: {', '.join(components['commands'])}") if components.get("agents"): print(f" Agents: {', '.join(components['agents'])}") if components.get("skills"): print(f" Skills: {', '.join(components['skills'])}") if components.get("hooks"): print(f" Hooks: Configured") if components.get("mcp_servers"): print(f" MCP Servers: {', '.join(components['mcp_servers'])}") print("\n" + "=" * 70) def copy_to_clipboard(text: str) -> bool: """Try to copy text to clipboard.""" try: # macOS process = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE) process.communicate(text.encode('utf-8')) return process.returncode == 0 except FileNotFoundError: try: # Linux with xclip process = subprocess.Popen(['xclip', '-selection', 'clipboard'], stdin=subprocess.PIPE) process.communicate(text.encode('utf-8')) return process.returncode == 0 except FileNotFoundError: return False def generate_submission_json(info: dict) -> str: """Generate JSON with all submission data.""" return json.dumps({ "form_fields": info["form_fields"], "validation": info["validation"], "git": info["git"], "components": info["components"], "metadata": info["metadata"] }, indent=2) def main(): parser = argparse.ArgumentParser( description="Prepare a Claude Code plugin for submission to Anthropic's Plugin Directory" ) parser.add_argument( "plugin_path", help="Path to the plugin directory" ) parser.add_argument( "--email", "-e", help="Primary contact email (required for submission)" ) parser.add_argument( "--company-url", "-c", help="Company/Organization URL (required for submission)" ) parser.add_argument( "--output", "-o", help="Save submission data to file (JSON format)" ) parser.add_argument( "--json", action="store_true", help="Output as JSON" ) parser.add_argument( "--open-form", action="store_true", help="Open the submission form in browser" ) parser.add_argument( "--copy-sha", action="store_true", help="Copy the full SHA to clipboard" ) args = parser.parse_args() print(f"🔍 Analyzing plugin at: {args.plugin_path}") info = gather_plugin_info( args.plugin_path, email=args.email, company_url=args.company_url ) if args.json: print(generate_submission_json(info)) else: print_form_fields(info) if args.output: with open(args.output, "w") as f: f.write(generate_submission_json(info)) print(f"\n📄 Submission data saved to: {args.output}") if args.copy_sha and info["git"].get("full_sha"): if copy_to_clipboard(info["git"]["full_sha"]): print(f"\n📋 Full SHA copied to clipboard!") else: print(f"\n⚠️ Could not copy to clipboard") if args.open_form: import webbrowser # The actual Google Form URL would go here form_url = "https://docs.google.com/forms/d/e/YOUR_FORM_ID/viewform" print(f"\n🌐 Opening submission form...") print(f" Note: Copy the form fields above and paste into the form") webbrowser.open(form_url) # Exit with error if validation failed if not info["validation"]["passed"]: print("\n⚠️ Please fix validation errors before submitting.") sys.exit(1) print("\n✨ Ready for submission! Copy the form fields above to the submission form.") if __name__ == "__main__": main() -
validate_plugin.py 10.3 KB
#!/usr/bin/env python3 """ Validate a Claude Code plugin structure and manifest. Usage: python validate_plugin.py <plugin-path> """ import argparse import json import re import sys from pathlib import Path from typing import List, Tuple class ValidationResult: def __init__(self): self.errors: List[str] = [] self.warnings: List[str] = [] self.info: List[str] = [] def add_error(self, msg: str): self.errors.append(msg) def add_warning(self, msg: str): self.warnings.append(msg) def add_info(self, msg: str): self.info.append(msg) @property def is_valid(self) -> bool: return len(self.errors) == 0 def print_report(self): if self.info: print("\n📋 Info:") for msg in self.info: print(f" {msg}") if self.warnings: print("\n⚠️ Warnings:") for msg in self.warnings: print(f" {msg}") if self.errors: print("\n❌ Errors:") for msg in self.errors: print(f" {msg}") if self.is_valid: print("\n✅ Plugin is valid!") else: print(f"\n❌ Plugin validation failed with {len(self.errors)} error(s)") def validate_plugin_name(name: str) -> Tuple[bool, str]: """Validate plugin name format (kebab-case).""" if not name: return False, "Plugin name is empty" if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$', name): return False, f"Plugin name '{name}' should be kebab-case (e.g., 'my-plugin')" if '--' in name: return False, f"Plugin name '{name}' should not have consecutive dashes" return True, "" def validate_version(version: str) -> Tuple[bool, str]: """Validate semantic version format.""" if not re.match(r'^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$', version): return False, f"Version '{version}' should follow semver (e.g., '1.0.0')" return True, "" def validate_plugin_json(plugin_dir: Path, result: ValidationResult) -> dict | None: """Validate plugin.json exists and has required fields.""" plugin_json_path = plugin_dir / ".claude-plugin" / "plugin.json" if not plugin_json_path.exists(): result.add_error(f"Missing .claude-plugin/plugin.json") return None try: with open(plugin_json_path) as f: plugin_data = json.load(f) except json.JSONDecodeError as e: result.add_error(f"Invalid JSON in plugin.json: {e}") return None # Required fields required_fields = ["name", "description", "version", "author"] for field in required_fields: if field not in plugin_data: result.add_error(f"Missing required field '{field}' in plugin.json") # Validate name if "name" in plugin_data: valid, msg = validate_plugin_name(plugin_data["name"]) if not valid: result.add_error(msg) # Validate version if "version" in plugin_data: valid, msg = validate_version(plugin_data["version"]) if not valid: result.add_error(msg) # Validate author if "author" in plugin_data: if not isinstance(plugin_data["author"], dict): result.add_error("'author' should be an object with 'name' field") elif "name" not in plugin_data["author"]: result.add_error("'author' object must have 'name' field") # Validate description if "description" in plugin_data: desc = plugin_data["description"] if len(desc) < 10: result.add_warning("Description is very short, consider adding more detail") if "TODO" in desc: result.add_warning("Description contains TODO placeholder") # Optional fields validation if "keywords" in plugin_data: if not isinstance(plugin_data["keywords"], list): result.add_error("'keywords' should be an array") result.add_info(f"Plugin: {plugin_data.get('name', 'unknown')} v{plugin_data.get('version', 'unknown')}") return plugin_data def validate_commands(plugin_dir: Path, result: ValidationResult): """Validate commands directory and files.""" commands_dir = plugin_dir / "commands" if not commands_dir.exists(): return md_files = list(commands_dir.glob("*.md")) if not md_files: result.add_warning("commands/ directory exists but contains no .md files") return result.add_info(f"Found {len(md_files)} command(s)") for md_file in md_files: content = md_file.read_text() # Check for frontmatter if not content.startswith("---"): result.add_warning(f"Command '{md_file.name}' missing YAML frontmatter") continue # Check for description in frontmatter if "description:" not in content.split("---")[1] if len(content.split("---")) > 1 else "": result.add_warning(f"Command '{md_file.name}' missing description in frontmatter") def validate_agents(plugin_dir: Path, result: ValidationResult): """Validate agents directory and files.""" agents_dir = plugin_dir / "agents" if not agents_dir.exists(): return md_files = list(agents_dir.glob("*.md")) if not md_files: result.add_warning("agents/ directory exists but contains no .md files") return result.add_info(f"Found {len(md_files)} agent(s)") for md_file in md_files: content = md_file.read_text() if not content.startswith("---"): result.add_warning(f"Agent '{md_file.name}' missing YAML frontmatter") def validate_skills(plugin_dir: Path, result: ValidationResult): """Validate skills directory structure.""" skills_dir = plugin_dir / "skills" if not skills_dir.exists(): return skill_dirs = [d for d in skills_dir.iterdir() if d.is_dir()] if not skill_dirs: result.add_warning("skills/ directory exists but contains no skill subdirectories") return result.add_info(f"Found {len(skill_dirs)} skill(s)") for skill_dir in skill_dirs: skill_md = skill_dir / "SKILL.md" if not skill_md.exists(): result.add_error(f"Skill '{skill_dir.name}' missing SKILL.md") def validate_hooks(plugin_dir: Path, result: ValidationResult): """Validate hooks configuration.""" hooks_dir = plugin_dir / "hooks" if not hooks_dir.exists(): return hooks_json = hooks_dir / "hooks.json" if not hooks_json.exists(): result.add_warning("hooks/ directory exists but missing hooks.json") return try: with open(hooks_json) as f: hooks_data = json.load(f) result.add_info("Found hooks configuration") events = hooks_data.get("hooks", hooks_data) if not isinstance(events, dict): result.add_error("hooks.json 'hooks' field must be an object") return valid_events = { "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Notification", "Stop", "SubagentStop", "SessionStart", "SessionEnd", "PreCompact", "PrePrompt", } for event in events.keys(): if event not in valid_events: result.add_warning(f"Unknown hook event: {event}") except json.JSONDecodeError as e: result.add_error(f"Invalid JSON in hooks.json: {e}") def validate_mcp(plugin_dir: Path, result: ValidationResult): """Validate MCP server configuration.""" mcp_json = plugin_dir / ".mcp.json" if not mcp_json.exists(): return try: with open(mcp_json) as f: mcp_data = json.load(f) if "mcpServers" not in mcp_data: result.add_error(".mcp.json missing 'mcpServers' key") return servers = mcp_data["mcpServers"] result.add_info(f"Found {len(servers)} MCP server(s)") for name, config in servers.items(): if "command" not in config: result.add_error(f"MCP server '{name}' missing 'command' field") except json.JSONDecodeError as e: result.add_error(f"Invalid JSON in .mcp.json: {e}") def validate_readme(plugin_dir: Path, result: ValidationResult): """Validate README.md exists and has content.""" readme = plugin_dir / "README.md" if not readme.exists(): result.add_error("Missing README.md") return content = readme.read_text() if len(content) < 100: result.add_warning("README.md is very short, consider adding more documentation") if "TODO" in content: result.add_warning("README.md contains TODO placeholders") def validate_license(plugin_dir: Path, result: ValidationResult): """Check for LICENSE file.""" license_file = plugin_dir / "LICENSE" if not license_file.exists(): result.add_warning("Missing LICENSE file (recommended for distribution)") def validate_plugin(plugin_path: str) -> ValidationResult: """Run all validation checks on a plugin.""" result = ValidationResult() plugin_dir = Path(plugin_path).resolve() if not plugin_dir.exists(): result.add_error(f"Plugin directory does not exist: {plugin_dir}") return result if not plugin_dir.is_dir(): result.add_error(f"Path is not a directory: {plugin_dir}") return result # Run all validators validate_plugin_json(plugin_dir, result) validate_commands(plugin_dir, result) validate_agents(plugin_dir, result) validate_skills(plugin_dir, result) validate_hooks(plugin_dir, result) validate_mcp(plugin_dir, result) validate_readme(plugin_dir, result) validate_license(plugin_dir, result) return result def main(): parser = argparse.ArgumentParser( description="Validate a Claude Code plugin" ) parser.add_argument( "plugin_path", help="Path to the plugin directory" ) parser.add_argument( "--quiet", "-q", action="store_true", help="Only show errors" ) args = parser.parse_args() print(f"🔍 Validating plugin at: {args.plugin_path}") result = validate_plugin(args.plugin_path) if not args.quiet or not result.is_valid: result.print_report() sys.exit(0 if result.is_valid else 1) if __name__ == "__main__": main()
-
-
SKILL.md 12.2 KB
--- name: plugins-management description: Create, publish, delete, and submit plugins for coding agents (Claude Code, OpenCode, Devin CLI/Desktop). Use when user wants to (1) create a new plugin with proper structure, (2) create or configure a plugin marketplace, (3) publish plugins to GitHub/GitLab/npm, (4) delete/uninstall plugins, (5) validate plugin structure, or (6) prepare and submit plugins to the official Anthropic directory or the OpenCode ecosystem. --- # Plugins Manager Manage plugins across coding agents: create, validate, publish, delete, and submit to official directories or npm. **Supported agents:** - **Claude Code**: `.claude-plugin/plugin.json`-based plugins, distributed via marketplaces - **OpenCode**: TypeScript/JavaScript plugins in `.opencode/plugins/` or npm packages listed in `opencode.json` - **Devin CLI / Desktop**: `.devin-plugin/plugin.json` manifest inside a plugin source (GitHub repo, git URL, `git-subdir`, or local folder); installs skills (`<plugin>:<skill>` slash commands), `AGENTS.md`/`rules/`, `agents/` subagents, `hooks.json`, and `.mcp.json` as one unit. Managed with `devin plugins` commands; plugins also apply to Devin cloud sessions, subject to per-surface limits. The scripts in this skill target Claude/OpenCode manifests — for Devin, author the `.devin-plugin/plugin.json` by hand per the Devin docs. **CRITICAL**: Before performing any deletion, uninstall, or removal operation, you MUST use the `AskUserQuestion` tool to confirm with the user. Never delete/uninstall plugins or remove marketplaces without explicit user confirmation. ## Quick Reference | Task | Command/Script | |------|----------------| | Create plugin | `python scripts/init_plugin.py <name>` | | Create marketplace | `python scripts/init_marketplace.py <name>` | | Validate plugin | `python scripts/validate_plugin.py <path>` | | Validate marketplace | `claude plugin validate <path>` | | Prepare submission | `python scripts/prepare_submission.py <path> --email X --company-url Y` | | Install plugin | `/plugin install <name>@<marketplace>` | | Delete plugin | `/plugin uninstall <name>@<marketplace>` | | Test plugin (dev) | `claude --plugin-dir ./my-plugin` | | Reload after edits | `/reload-plugins` | | Cut release tag | `claude plugin tag --push` | | List installed | `claude plugin list [--json] [--available]` | | Update plugin | `claude plugin update <name>@<marketplace>` | ## Workflows ### 1. Create a New Plugin ```bash # Basic plugin with commands python scripts/init_plugin.py my-plugin --path ./ # Full plugin with all components python scripts/init_plugin.py my-plugin --path ./ --all # Specific components python scripts/init_plugin.py my-plugin --with-agents --with-skills ``` **Flags:** - `--with-commands` (default): Include commands directory - `--with-agents`: Include agents directory - `--with-skills`: Include skills directory - `--with-hooks`: Include hooks configuration - `--with-mcp`: Include MCP server configuration - `--all`: Include all components - `--author "Name"`: Set author name **After creation:** 1. Edit `.claude-plugin/plugin.json` with plugin details 2. Add commands to `commands/*.md` with YAML frontmatter 3. Add agents to `agents/*.md` if needed 4. Update `README.md` with documentation ### 2. Create a Marketplace ```bash # Empty marketplace python scripts/init_marketplace.py my-marketplace --path ./ # With initial plugin python scripts/init_marketplace.py my-marketplace --with-plugin my-plugin ``` **After creation:** 1. Edit `.claude-plugin/marketplace.json` 2. Add plugins to `plugins/` directory 3. Push to GitHub: `git push origin main` **Users install with:** ```bash /plugin marketplace add username/my-marketplace ``` **Marketplace references:** - Required file: `.claude-plugin/marketplace.json` - Plugin entries must have `name` that matches each plugin's `plugin.json` name - Use relative paths in `source` (e.g., `./plugins/my-plugin`), not absolute paths - Use `${CLAUDE_PLUGIN_ROOT}` inside hooks and MCP configs referenced by marketplace plugins ### 3. Validate a Plugin ```bash python scripts/validate_plugin.py ./my-plugin ``` **Validates:** - plugin.json required fields (name, description, version, author) - Semantic versioning format - Command/agent frontmatter - Hooks and MCP configuration - README.md and LICENSE presence **Also consider:** - `claude plugin validate <path>` for marketplace JSON validation ### 4. Publish a Plugin **To GitHub:** ```bash cd my-marketplace git init git add . git commit -m "Initial release" git remote add origin https://github.com/user/my-marketplace.git git push -u origin main # Tag release git tag -a v1.0.0 -m "Version 1.0.0" git push origin v1.0.0 ``` **Distribution methods:** - GitHub: `/plugin marketplace add user/repo` - GitLab: `/plugin marketplace add https://gitlab.com/user/repo.git` - URL: `/plugin marketplace add https://example.com/marketplace.json` ### 5. Delete/Uninstall Plugins **⚠️ ALWAYS confirm with user before deleting/uninstalling.** Use `AskUserQuestion` to ask: "Are you sure you want to uninstall '[plugin-name]'? This action cannot be undone." ```bash # Uninstall from Claude Code /plugin uninstall plugin-name@marketplace-name # Remove marketplace (confirm with user first!) /plugin marketplace remove marketplace-name ``` **To delete source files:** First confirm with user via `AskUserQuestion`, then remove the plugin directory from the marketplace's `plugins/` folder and update `marketplace.json`. ### 6. Submit to Anthropic's Official Directory The submission script automatically gathers all required form fields using `gh` CLI and git. **Prerequisites:** 1. Plugin pushed to GitHub 2. `gh` CLI installed and authenticated 3. All validation checks pass **Prepare submission:** ```bash # Basic - gathers repo URL and SHA automatically python scripts/prepare_submission.py ./my-plugin # With required contact info python scripts/prepare_submission.py ./my-plugin \ --email your@email.com \ --company-url https://yourcompany.com # Copy SHA to clipboard python scripts/prepare_submission.py ./my-plugin --copy-sha # Save to JSON file python scripts/prepare_submission.py ./my-plugin --output submission.json # Open form in browser python scripts/prepare_submission.py ./my-plugin --open-form ``` **Form fields gathered automatically:** | Field | Source | |-------|--------| | Link to Plugin | `gh repo view --json url` | | Full SHA | `git rev-parse HEAD` | | Plugin Homepage | plugin.json homepage or repo URL | | Plugin Name | plugin.json name | | Plugin Description | plugin.json description (50-100 words) | **Fields you must provide:** - `--email`: Primary contact email - `--company-url`: Company/Organization URL **Submission requirements:** - Plugin must be pushed to GitHub - Working directory should be clean (no uncommitted changes) - Description should be 50-100 words - README.md and LICENSE files present - No secrets/API keys in code ## Plugin Structure Reference ``` my-plugin/ ├── .claude-plugin/ │ └── plugin.json # Optional manifest (auto-discovered if absent) ├── skills/ # Agent skills (preferred over commands/) │ └── */SKILL.md ├── commands/ # Skills as flat .md files │ └── *.md ├── agents/ # AI subagents │ └── *.md ├── output-styles/ # Output style definitions (2026) ├── themes/ # Color themes (2026) ├── monitors/ # Background monitors (2026, v2.1.105+) │ └── monitors.json ├── hooks/ │ └── hooks.json # Event handlers ├── bin/ # Executables added to PATH (2026) ├── settings.json # Default agent / subagentStatusLine (2026) ├── .mcp.json # MCP servers ├── .lsp.json # LSP server config (since v2.0.74) ├── package.json # Auto-installed dependencies (2026) ├── README.md # Documentation ├── CHANGELOG.md └── LICENSE ``` **For detailed reference:** See [references/plugin-guide.md](references/plugin-guide.md) ## OpenCode Plugins OpenCode (anomalyco/opencode v1.14.x) plugins are TypeScript/JavaScript modules — fundamentally different from Claude Code plugins. ### Quick reference | Task | Approach | |------|----------| | Create local plugin | Drop `.ts` file in `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (global) | | Author npm plugin | `npm init`, add `keywords: ["opencode-plugin"]`, depend on `@opencode-ai/plugin` | | Install npm plugin | Add package name to `opencode.json` → `"plugin": [...]`; restart | | Distribute | Publish to npm (no central marketplace) | ### Minimal plugin ```typescript // .opencode/plugins/env-protection.ts import type { Plugin } from "@opencode-ai/plugin" export default (async () => ({ tool: { execute: { before: async (input, output) => { if (output.args.filePath?.includes(".env")) { throw new Error("Reading .env is forbidden") } }, }, }, })) satisfies Plugin ``` ### Register npm plugins ```json { "$schema": "https://opencode.ai/config.json", "plugin": [ "opencode-helicone-session", "@my-org/custom-plugin" ] } ``` OpenCode runs `bun install` at startup. Cached at `~/.cache/opencode/node_modules/`. ### What plugins can do - Add custom tools the AI can call (Zod-validated args) - Intercept/block tool calls (`tool.execute.before` throws to block) - Subscribe to ~25 lifecycle events (`session.idle`, `file.edited`, `permission.asked`, ...) - Register custom slash commands and auth providers - Transform messages or system prompts during context compaction (experimental) ### Critical caveats (v1.14.x) - `tool.execute.*` hooks **do not fire** for MCP tool calls — use the `permission` block in `opencode.json` - No central marketplace — distribute via npm and aggregators like [awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) - Plugins run in-process with full SDK access — audit third-party code before installing See [references/opencode-plugins.md](references/opencode-plugins.md) for the full OpenCode plugin reference. ## Critical Rules (Avoid Silent Failures) - Keep `skills/`, `commands/`, `agents/`, `hooks/`, `monitors/`, `themes/`, `output-styles/`, `bin/` at the plugin root (never inside `.claude-plugin/`). - Do not add standard component paths to `plugin.json`. Only specify non-standard paths starting with `./`. - Use `${CLAUDE_PLUGIN_ROOT}` (cache path, changes per version) and `${CLAUDE_PLUGIN_DATA}` (persistent across updates) in hooks and MCP/LSP/monitor config paths. Relative paths break after install. - Ensure hook scripts are executable (`chmod +x scripts/*`). - Marketplace `plugins[].name` must match the plugin's `plugin.json` `name`. - **Path traversal limit (2026)**: plugins cannot reference files outside their directory; use symlinks inside the plugin if needed. - **Versioning (2026)**: omit `version` to use git SHA (every commit is a new version). Set `version` and bump for stable releases. Use `claude plugin tag` to cut release tags. ## Common Patterns ### Command File Format ```markdown --- description: What this command does --- # Command Name Instructions for Claude when command is invoked. ``` ### Agent File Format ```markdown --- description: Agent specialty and purpose --- # Agent Name Detailed instructions and expertise. ``` ### Hooks Configuration ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" } ] } ] } } ``` ### MCP Server Configuration ```json { "mcpServers": { "server-name": { "command": "node", "args": ["./servers/server.js"] } } } ``` ### Skill File Format ```markdown --- name: my-skill description: What this skill does and when to use it --- # Skill Title Instructions for Claude when this skill is invoked. ``` ### Marketplace Entry Example ```json { "name": "my-plugin", "source": "./plugins/my-plugin", "description": "Short description", "version": "1.0.0", "author": { "name": "Author Name" }, "category": "productivity", "keywords": ["tag1", "tag2"], "strict": true } ```
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.