Claude Skill

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

LLM Mart · 0 points · 3 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download CodeAlive-AI-ai-driven-development-skills_plugins-management-b1fbfa4.zip · 26 KB
Part of codealive-ai/ai-driven-development — 21 skills

Install

skills CLI npx skills add https://github.com/CodeAlive-AI/ai-driven-development/tree/main/skills/plugins-management
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install codealive-ai-ai-driven-development@llmmart
Git 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 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

# 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

# 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:

/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

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:

  1. Plugin pushed to GitHub
  2. gh CLI installed and authenticated
  3. 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.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
  • 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[].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

---
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.

No comments yet.

Reviews (0)

No reviews yet.

Related