
Meta Plugin Creator
- 64 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
meta-plugin-creator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- meta-plugin-creator
- AI & Agent Building
- AI-coding skill
Meta Plugin Creator by the numbers
- 64 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,160 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill meta-plugin-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Plugin Creator
Overview
Plugins are shareable packages that bundle skills, agents, commands, hooks, MCP servers, and LSP servers into installable units for Claude Code. A plugin requires only a .claude-plugin/plugin.json manifest at minimum; all other components are optional and auto-discovered from conventional directories at the plugin root.
When to use: Sharing functionality across projects or teams, distributing through marketplaces, versioning reusable agent capabilities, bundling MCP servers with skills.
When NOT to use: Single-project customizations (use .claude/ directory instead), quick experiments before packaging, personal workflows that do not need distribution.
Quick Reference
| Component | Location | Format | Discovery |
|---|---|---|---|
| Manifest | .claude-plugin/plugin.json | JSON | Required |
| Skills | skills/*/SKILL.md | Markdown + YAML | Auto by context |
| Commands | commands/*.md | Markdown + YAML | Auto, namespaced |
| Agents | agents/*.md | Markdown + YAML | Auto, /agents UI |
| Hooks | hooks/hooks.json | JSON | Auto on events |
| MCP servers | .mcp.json | JSON | Auto on enable |
| LSP servers | .lsp.json | JSON | Auto on enable |
| Output styles | Custom path | Per config | Via manifest |
| Plugin env var | ${CLAUDE_PLUGIN_ROOT} | Absolute path | All configs/scripts |
| Project env | ${CLAUDE_PROJECT_DIR} | Absolute path | All configs/scripts |
| CLI test flag | --plugin-dir ./my-plugin | Local dev | Manual load |
| CLI validate | claude plugin validate . | Local dev | Manual check |
| Install scope | `--scope user\ | project\ | local` |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Placing components inside .claude-plugin/ | Only plugin.json goes in .claude-plugin/; components at plugin root |
| Using absolute paths in configs | Use ${CLAUDE_PLUGIN_ROOT} for all plugin file references |
Missing plugin.json manifest | Create .claude-plugin/plugin.json with at least a name field |
Path traversal with ../ | Keep all files inside the plugin directory |
| Non-executable hook scripts | Run chmod +x on all scripts referenced by hooks |
| Forgetting namespacing | Plugin skills are invoked as /plugin-name:skill-name |
| Inline hooks missing event casing | Event names are case-sensitive: PostToolUse, not postToolUse |
| Expecting custom paths to replace defaults | Custom component paths supplement default directories, not replace them |
Delegation
- Plugin component creation: Build skills, agents, and commands following their respective standards
- Pattern discovery: Use
Exploreagent to find existing plugin patterns in the codebase - Code review: Use
Taskagent for plugin structure validation
If the meta-skill-creator skill is available, delegate skill authoring within plugins to it.Otherwise, recommend: npx skills add oakoss/agent-skills --skill meta-skill-creatorReferences
- Plugin manifest schema, directory structure, and component paths
- Skills, agents, commands, hooks, MCP servers, and LSP servers in plugins
- Installation scopes, environment variables, and publishing
Plugin Components
Skills
Skills are auto-discovered from skills/*/SKILL.md and provide context-aware capabilities that the agent loads when relevant.
skills/
└── code-review/
├── SKILL.md
├── references/
└── scripts/Each SKILL.md needs frontmatter with name and description:
---
name: code-review
description: Reviews code for best practices and potential issues. Use when reviewing code, checking PRs, or analyzing code quality.
---
When reviewing code, check for:
1. Code organization and structure
2. Error handling
3. Security concerns
4. Test coverageSkills support the full Agent Skills standard including reference files, scripts, and progressive disclosure.
Commands
Commands are slash commands defined as Markdown files in commands/. They are namespaced by plugin name: a command at commands/deploy.md in a plugin named my-tools is invoked as /my-tools:deploy.
---
description: Deploy to production
argument-hint: [environment]
---
Deploy the application to $ARGUMENTS environment.| Frontmatter Field | Required | Description |
|---|---|---|
description | Yes | Shown in /help and autocomplete |
argument-hint | No | Hint text for autocomplete |
The $ARGUMENTS placeholder captures any text the user provides after the command name.
Agents
Agents are specialized subagents defined as Markdown files in agents/. They appear in the /agents interface and can be invoked automatically based on task context.
---
description: Review code for security vulnerabilities
capabilities: ['security-analysis', 'vulnerability-detection']
tools: Read, Grep, Glob
model: haiku
---
# Security Reviewer
You review code for security issues. Focus on:
- Authentication and authorization flaws
- Input validation gaps
- Injection vulnerabilities
- Sensitive data exposure| Frontmatter Field | Required | Description |
|---|---|---|
description | Yes | What the agent specializes in |
capabilities | No | List of task categories |
tools | No | Allowed tools for the agent |
model | No | Model to use (e.g., haiku) |
Hooks
Hooks are event handlers that run automatically when specific events occur. Define them in hooks/hooks.json or inline in plugin.json.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
}
]
}
]
}
}Available Events
| Event | Trigger |
|---|---|
PreToolUse | Before any tool execution |
PostToolUse | After successful tool execution |
PostToolUseFailure | After tool execution fails |
PermissionRequest | When a permission dialog is shown |
UserPromptSubmit | When user submits a prompt |
Notification | When a notification is sent |
Stop | When the agent attempts to stop |
SubagentStart | When a subagent is started |
SubagentStop | When a subagent attempts to stop |
SessionStart | At the beginning of sessions |
SessionEnd | At the end of sessions |
PreCompact | Before conversation history is compacted |
Hook Types
| Type | Description |
|---|---|
command | Execute shell commands or scripts |
prompt | Evaluate a prompt with an LLM (uses $ARGUMENTS) |
agent | Run an agentic verifier with tools for complex tasks |
Event names are case-sensitive. Scripts must be executable (chmod +x).
MCP Servers
MCP servers connect the plugin to external tools via the Model Context Protocol. Define them in .mcp.json or inline in plugin.json.
{
"mcpServers": {
"plugin-database": {
"command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server",
"args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"],
"env": {
"DB_PATH": "${CLAUDE_PLUGIN_ROOT}/data"
}
},
"plugin-api": {
"command": "npx",
"args": ["@company/mcp-server", "--plugin-mode"],
"cwd": "${CLAUDE_PLUGIN_ROOT}"
}
}
}Plugin MCP servers start automatically when the plugin is enabled and appear as standard tools in the agent toolkit.
LSP Servers
LSP servers provide real-time code intelligence (diagnostics, go-to-definition, find references). Define them in .lsp.json or inline in plugin.json.
{
"go": {
"command": "gopls",
"args": ["serve"],
"extensionToLanguage": {
".go": "go"
}
}
}Required LSP Fields
| Field | Description |
|---|---|
command | LSP binary to execute (must be in PATH) |
extensionToLanguage | Maps file extensions to language identifiers |
Optional LSP Fields
| Field | Description |
|---|---|
args | Command-line arguments |
transport | stdio (default) or socket |
env | Environment variables for the server |
initializationOptions | Options passed during initialization |
settings | Settings via workspace/didChangeConfiguration |
workspaceFolder | Workspace folder path |
startupTimeout | Max startup wait time (ms) |
shutdownTimeout | Max graceful shutdown wait time (ms) |
restartOnCrash | Auto-restart on crash |
maxRestarts | Maximum restart attempts |
loggingConfig | Debug logging configuration |
TypeScript LSP Example
Complete configuration showing initializationOptions and logging:
{
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact"
},
"initializationOptions": {
"preferences": {
"includeInlayParameterNameHints": "all"
}
},
"loggingConfig": {
"args": ["--log-level", "verbose"],
"env": {
"TSS_LOG": "-level verbose -file ${CLAUDE_PLUGIN_LSP_LOG_FILE}"
}
}
}
}The language server binary must be installed separately. The plugin configures how the agent connects to it.
Distribution
Installation Scopes
When installing a plugin, choose a scope that determines availability and visibility:
| Scope | Settings File | Use Case |
|---|---|---|
user | ~/.claude/settings.json | Personal plugins, all projects (default) |
project | .claude/settings.json | Team plugins, version controlled |
local | .claude/settings.local.json | Project-specific, gitignored |
managed | managed-settings.json | Enterprise, read-only (update only) |
Install with a specific scope:
claude plugin install formatter@my-marketplace --scope projectEnvironment Variables
Plugin Path Variables
| Variable | Purpose |
|---|---|
${CLAUDE_PLUGIN_ROOT} | Absolute path to plugin install directory |
${CLAUDE_PROJECT_DIR} | Project root directory |
Always use ${CLAUDE_PLUGIN_ROOT} for paths to files within the plugin. Absolute paths break when the plugin is installed to a different location.
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh"
}
]
}
]
}
}Auto-Update Configuration
Control auto-update behavior for plugins and Claude Code:
| Variable | Effect |
|---|---|
DISABLE_AUTOUPDATER | Disables all auto-updates (plugins and Claude Code) |
FORCE_AUTOUPDATE_PLUGINS | Enables plugin updates even when autoupdater disabled |
# Disable all auto-updates
export DISABLE_AUTOUPDATER=true
# Keep plugin updates, disable Claude Code updates
export DISABLE_AUTOUPDATER=true
export FORCE_AUTOUPDATE_PLUGINS=trueCLI Commands
Plugin Management
claude plugin install <plugin> [--scope user|project|local]
claude plugin uninstall <plugin> [--scope user|project|local]
claude plugin enable <plugin> [--scope user|project|local]
claude plugin disable <plugin> [--scope user|project|local]
claude plugin update <plugin> [--scope user|project|local|managed]
claude plugin validate .Local Development
Test a plugin without installation:
claude --plugin-dir ./my-pluginLoad multiple plugins simultaneously:
claude --plugin-dir ./plugin-one --plugin-dir ./plugin-twoInteractive UI
/plugin # Open plugin managerTabs:
- Discover: Browse available plugins from connected marketplaces
- Installed: Manage installed plugins (enable, disable, uninstall)
- Marketplaces: Add/remove marketplaces
- Errors: View loading errors for troubleshooting
Debugging
claude --debugShows plugin loading details, manifest errors, component registration, and MCP server initialization.
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Plugin not loading | Invalid plugin.json | Validate JSON syntax |
| Commands missing | Wrong directory | Components at root, not in .claude-plugin/ |
| Hooks not firing | Script not executable | chmod +x script.sh |
| MCP server fails | Missing ${CLAUDE_PLUGIN_ROOT} | Use variable for paths |
| LSP not found | Binary not installed | Install language server |
| Skills not appearing | Cache stale | Clear cache, reinstall |
Debug Checklist
1. Run claude --debug and check "loading plugin" messages 2. Verify component directories appear in debug output 3. Check file permissions on scripts 4. Test scripts manually outside Claude 5. Validate JSON syntax in all config files 6. Clear cache if needed: rm -rf ~/.claude/plugins/cache
Marketplace Publishing
Marketplace Structure
A marketplace is a git repository with .claude-plugin/marketplace.json:
{
"name": "company-tools",
"owner": {
"name": "DevTools Team",
"email": "devtools@example.com"
},
"metadata": {
"description": "Internal development tools",
"version": "1.0.0",
"pluginRoot": "./plugins"
},
"plugins": [
{
"name": "code-formatter",
"source": "./plugins/formatter",
"description": "Automatic code formatting",
"version": "2.1.0"
}
]
}Required Marketplace Fields
| Field | Type | Description |
|---|---|---|
name | string | Marketplace identifier (kebab-case) |
owner | object | Maintainer info (name required) |
plugins | array | List of plugin entries |
Plugin Entry Fields
Each entry requires name and source. The source can be:
Relative path (same repository):
{
"name": "my-plugin",
"source": "./plugins/my-plugin"
}GitHub repository:
{
"name": "github-plugin",
"source": {
"source": "github",
"repo": "owner/plugin-repo",
"ref": "v2.0.0"
}
}Git URL:
{
"name": "git-plugin",
"source": {
"source": "url",
"url": "https://gitlab.com/team/plugin.git"
}
}The strict Field
| Value | Behavior |
|---|---|
true (default) | Plugin source must contain its own plugin.json |
false | Marketplace entry defines everything; no plugin.json needed |
When strict: true, fields in the marketplace entry merge with the plugin's own plugin.json.
Marketplace CLI
# Add a marketplace
/plugin marketplace add owner/repo
/plugin marketplace add https://gitlab.com/team/plugins.git
/plugin marketplace add ./local-marketplace
# Update marketplace catalog
/plugin marketplace update
# Install from marketplace
/plugin install my-plugin@marketplace-nameTeam Distribution
Auto-Prompt Installation
Add marketplace to .claude/settings.json so team members are prompted to install:
{
"extraKnownMarketplaces": {
"company-tools": {
"source": {
"source": "github",
"repo": "your-org/claude-plugins"
}
}
},
"enabledPlugins": {
"formatter@company-tools": true,
"deploy-tools@company-tools": true
}
}Private Repository Access
For background auto-updates of private marketplace repos, set authentication tokens:
| Provider | Environment Variables |
|---|---|
| GitHub | GITHUB_TOKEN or GH_TOKEN |
| GitLab | GITLAB_TOKEN or GL_TOKEN |
| Bitbucket | BITBUCKET_TOKEN |
Manual installation and updates use existing git credential helpers.
Official Marketplace Plugins
Code Intelligence (LSP)
pyright-lsp— Python type checkingtypescript-lsp— TypeScript/JavaScriptrust-analyzer-lsp— Rustgopls-lsp— Goclangd-lsp— C/C++
External Integrations (MCP)
github,gitlab— Source controlatlassian,linear,notion— Project managementslack— Communicationvercel,firebase,supabase— Infrastructure
Development Workflows
commit-commands— Git workflowspr-review-toolkit— PR review agentsplugin-dev— Plugin development toolkit
Validation Checklist
Before publishing, verify:
.claude-plugin/plugin.jsonexists with at least anamefield- All component directories are at plugin root, not inside
.claude-plugin/ - All paths are relative, starting with
./ - Scripts are executable (
chmod +x) - All file references use
${CLAUDE_PLUGIN_ROOT} - Version follows semver (
MAJOR.MINOR.PATCH) claude plugin validate .passes with no errors
Plugin Anatomy
Directory Structure
A complete plugin follows this layout. Only .claude-plugin/plugin.json is required; all other directories are optional.
my-plugin/
├── .claude-plugin/ # Metadata directory
│ └── plugin.json # Required: plugin manifest
├── skills/ # Agent Skills (auto-discovered)
│ └── code-review/
│ ├── SKILL.md
│ └── scripts/
├── commands/ # Slash commands (auto-discovered)
│ ├── deploy.md
│ └── status.md
├── agents/ # Subagents (auto-discovered)
│ ├── reviewer.md
│ └── tester.md
├── hooks/ # Event hooks
│ └── hooks.json
├── .mcp.json # MCP server config
├── .lsp.json # LSP server config
├── scripts/ # Utility scripts for hooks
│ └── format.sh
├── LICENSE
└── CHANGELOG.mdOnly plugin.json belongs inside .claude-plugin/. All component directories (skills, commands, agents, hooks) go at the plugin root.
Plugin Manifest Schema
Minimal Manifest
{
"name": "my-plugin"
}The name field is the only required field. It must be kebab-case with no spaces.
Complete Schema
{
"name": "my-plugin",
"version": "1.2.0",
"description": "Brief plugin description",
"author": {
"name": "Author Name",
"email": "author@example.com",
"url": "https://github.com/author"
},
"homepage": "https://docs.example.com/plugin",
"repository": "https://github.com/author/plugin",
"license": "MIT",
"keywords": ["keyword1", "keyword2"],
"commands": ["./custom/commands/special.md"],
"agents": "./custom/agents/",
"skills": "./custom/skills/",
"hooks": "./config/hooks.json",
"mcpServers": "./mcp-config.json",
"lspServers": "./.lsp.json",
"outputStyles": "./styles/"
}Required Fields
| Field | Type | Description | Example |
|---|---|---|---|
name | string | Unique identifier (kebab-case, no spaces) | "deployment-tools" |
Metadata Fields
| Field | Type | Description | Example |
|---|---|---|---|
version | string | Semantic version | "2.1.0" |
description | string | Brief explanation of plugin purpose | "Deployment automation tools" |
author | object | Author information | {"name": "Dev Team", "email": "dev@company.com"} |
homepage | string | Documentation URL | "https://docs.example.com" |
repository | string | Source code URL | "https://github.com/user/plugin" |
license | string | License identifier | "MIT", "Apache-2.0" |
keywords | array | Discovery tags | ["deployment", "ci-cd"] |
Component Path Fields
| Field | Type | Default Location | Description |
|---|---|---|---|
commands | string\ | array | commands/ |
agents | string\ | array | agents/ |
skills | string\ | array | skills/ |
hooks | string\ | object | hooks/hooks.json |
mcpServers | string\ | object | .mcp.json |
lspServers | string\ | object | .lsp.json |
outputStyles | string\ | array | - |
Custom paths supplement default directories. If commands/ exists at the root, it is loaded in addition to any custom command paths.
Path Behavior Rules
All paths must be relative to the plugin root and start with ./. Multiple paths can be specified as arrays:
{
"commands": ["./specialized/deploy.md", "./utilities/batch-process.md"],
"agents": ["./custom-agents/reviewer.md", "./custom-agents/tester.md"]
}Plugins cannot reference files outside their directory. Path traversal (../) does not work because plugins are copied to a cache directory during installation.
Version Management
Follow semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR: Breaking changes (incompatible API changes)
- MINOR: New features (backward-compatible additions)
- PATCH: Bug fixes (backward-compatible fixes)
Start at 1.0.0 for the first stable release. Pre-release versions like 2.0.0-beta.1 are supported for testing.
Complete Plugin Example
A deployment-tools plugin with commands, agents, skills, hooks, and MCP server:
deployment-tools/
├── .claude-plugin/
│ └── plugin.json
├── commands/
│ ├── deploy.md
│ ├── rollback.md
│ └── status.md
├── agents/
│ └── deployment-checker.md
├── skills/
│ └── infrastructure/
│ ├── SKILL.md
│ └── scripts/
│ └── validate-config.py
├── hooks/
│ └── hooks.json
├── .mcp.json
├── scripts/
│ ├── pre-deploy.sh
│ └── notify.py
├── LICENSE
└── CHANGELOG.mdplugin.json
{
"name": "deployment-tools",
"version": "2.1.0",
"description": "Deployment automation for Claude Code",
"author": {
"name": "DevOps Team",
"email": "devops@company.com"
},
"repository": "https://github.com/company/deployment-tools",
"license": "MIT",
"keywords": ["deployment", "ci-cd", "automation"]
}hooks.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/pre-deploy.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/notify.py"
}
]
}
]
}
}.mcp.json
{
"mcpServers": {
"deployment-api": {
"command": "npx",
"args": ["@company/deploy-mcp-server"],
"cwd": "${CLAUDE_PLUGIN_ROOT}",
"env": {
"CONFIG_PATH": "${CLAUDE_PLUGIN_ROOT}/config.json"
}
}
}
}Plugin Caching
When installed, plugins are copied to a cache directory rather than used in-place. This means:
- Files outside the plugin directory are not available after installation
- Symlinks within the plugin directory are followed during copying
- All file references must use
${CLAUDE_PLUGIN_ROOT}to resolve correctly regardless of installation location
Using Symlinks for Shared Dependencies
If you need to reference external files or shared utilities, create symlinks inside the plugin directory. Symlinks are followed during the copy process:
# Inside plugin directory
ln -s /path/to/shared-utils ./shared-utilsThe symlinked content becomes part of the plugin cache, allowing access to shared resources while maintaining plugin isolation.
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.11"
# ///
"""
Validate Claude Code plugin structure against best practices.
Usage:
uv run scripts/validate-plugin.py <path> # Single plugin directory
uv run scripts/validate-plugin.py <glob-pattern> # Multiple plugins
Examples:
uv run scripts/validate-plugin.py ./my-plugin
uv run scripts/validate-plugin.py ./plugins/
uv run scripts/validate-plugin.py "./plugins/*"
"""
import glob
import json
import os
import re
import sys
from pathlib import Path
def resolve_plugin_paths(path_arg: str) -> tuple[list[Path], str | None]:
"""
Resolve a path argument to a list of plugin directories.
Handles:
- Direct plugin directory: ./my-plugin (has .claude-plugin/)
- Parent directory: ./plugins/ (finds all plugin dirs)
- Glob pattern: ./plugins/*
Returns:
Tuple of (list of Path objects, error message if any)
"""
path = Path(path_arg)
# Case 1: Direct plugin directory
if path.is_dir():
# Check if this is a plugin (has .claude-plugin/)
if (path / ".claude-plugin").exists():
return [path], None
# Check if this is a parent directory containing plugins
plugin_dirs = [
d for d in path.iterdir()
if d.is_dir() and (d / ".claude-plugin").exists()
]
if plugin_dirs:
return sorted(plugin_dirs), None
return [], f"No plugins found in: {path} (plugins must have .claude-plugin/ directory)"
# Case 2: Glob pattern
if "*" in path_arg or "?" in path_arg:
matches = glob.glob(path_arg, recursive=True)
plugin_dirs = [
Path(m) for m in matches
if Path(m).is_dir() and (Path(m) / ".claude-plugin").exists()
]
if plugin_dirs:
return sorted(plugin_dirs), None
return [], f"No plugins match pattern: {path_arg}"
# Path doesn't exist
return [], f"Path not found: {path_arg}"
def validate_plugin(plugin_path: Path) -> tuple[list[str], list[str]]:
"""Validate a plugin directory structure."""
errors: list[str] = []
warnings: list[str] = []
# Check for .claude-plugin/plugin.json
manifest_path = plugin_path / ".claude-plugin" / "plugin.json"
if not manifest_path.exists():
errors.append("Missing .claude-plugin/plugin.json")
return errors, warnings
# Parse manifest
try:
manifest = json.loads(manifest_path.read_text())
except json.JSONDecodeError as e:
errors.append(f"Invalid JSON in plugin.json: {e}")
return errors, warnings
except PermissionError:
return [f"Permission denied: {manifest_path}"], []
except OSError as e:
return [f"Cannot read file: {e}"], []
# Required fields
if "name" not in manifest:
errors.append("Missing required field: 'name'")
else:
name = manifest["name"]
# Check kebab-case
if not re.match(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$", name):
warnings.append(f"Name '{name}' should be kebab-case (lowercase, hyphens)")
# Recommended fields
if "version" not in manifest:
warnings.append("Consider adding 'version' field (semver)")
else:
version = manifest["version"]
if not re.match(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$", version):
warnings.append(f"Version '{version}' should follow semver (MAJOR.MINOR.PATCH)")
if "description" not in manifest:
warnings.append("Consider adding 'description' field")
# Check component paths
component_paths = ["commands", "agents", "skills", "hooks", "mcpServers", "lspServers", "outputStyles"]
for comp in component_paths:
if comp in manifest:
value = manifest[comp]
if isinstance(value, str) and not value.startswith("./"):
errors.append(f"Path '{comp}' must be relative, starting with './'")
elif isinstance(value, list):
for comp_path in value:
if isinstance(comp_path, str) and not comp_path.startswith("./"):
errors.append(f"Path in '{comp}' must be relative: {comp_path}")
# Check default directories exist (at least one)
default_dirs = ["commands", "agents", "skills", "hooks"]
found_components = False
for dir_name in default_dirs:
dir_path = plugin_path / dir_name
if dir_path.exists() and dir_path.is_dir():
found_components = True
# Check for content
files = list(dir_path.glob("*"))
if not files:
warnings.append(f"Directory '{dir_name}/' exists but is empty")
# Check for components incorrectly placed in .claude-plugin/
claude_plugin_dir = plugin_path / ".claude-plugin"
for dir_name in default_dirs:
if (claude_plugin_dir / dir_name).exists():
errors.append(f"'{dir_name}/' found inside .claude-plugin/ - move to plugin root")
# Check for MCP/LSP config files
mcp_path = plugin_path / ".mcp.json"
lsp_path = plugin_path / ".lsp.json"
if mcp_path.exists():
try:
mcp_config = json.loads(mcp_path.read_text())
if "mcpServers" in mcp_config:
for server_name, config in mcp_config["mcpServers"].items():
if "command" in config:
cmd = config["command"]
# Check for CLAUDE_PLUGIN_ROOT usage
if "/" in cmd and "${CLAUDE_PLUGIN_ROOT}" not in cmd and not cmd.startswith("npx"):
warnings.append(f"MCP server '{server_name}': use ${{CLAUDE_PLUGIN_ROOT}} for plugin paths")
except json.JSONDecodeError:
errors.append("Invalid JSON in .mcp.json")
if lsp_path.exists():
try:
lsp_config = json.loads(lsp_path.read_text())
for lang, config in lsp_config.items():
if "command" not in config:
errors.append(f"LSP '{lang}': missing required 'command' field")
if "extensionToLanguage" not in config:
errors.append(f"LSP '{lang}': missing required 'extensionToLanguage' field")
except json.JSONDecodeError:
errors.append("Invalid JSON in .lsp.json")
# Check hooks config
hooks_path = plugin_path / "hooks" / "hooks.json"
if hooks_path.exists():
try:
hooks_config = json.loads(hooks_path.read_text())
if "hooks" in hooks_config:
for event, matchers in hooks_config["hooks"].items():
if isinstance(matchers, list):
for matcher in matchers:
if "hooks" in matcher:
for hook in matcher["hooks"]:
if hook.get("type") == "command":
cmd = hook.get("command", "")
if "/" in cmd and "${CLAUDE_PLUGIN_ROOT}" not in cmd:
warnings.append(f"Hook command should use ${{CLAUDE_PLUGIN_ROOT}}: {cmd[:50]}...")
except json.JSONDecodeError:
errors.append("Invalid JSON in hooks/hooks.json")
# Check scripts directory
scripts_path = plugin_path / "scripts"
if scripts_path.exists():
for script in scripts_path.glob("*"):
if script.is_file() and script.suffix in [".sh", ".py", ".js"]:
# Check if executable
if not os.access(script, os.X_OK):
warnings.append(f"Script not executable: {script.name} (run chmod +x)")
if not found_components and "commands" not in manifest and "agents" not in manifest:
warnings.append("No component directories found (commands/, agents/, skills/, hooks/)")
return errors, warnings
def print_result(path: Path, errors: list[str], warnings: list[str], verbose: bool = True) -> None:
"""Print validation results for a single plugin."""
plugin_name = path.name
if errors:
print(f"❌ {plugin_name}: FAILED")
if verbose:
for error in errors:
print(f" ✗ {error}")
elif warnings:
print(f"✓ {plugin_name}: valid (with {len(warnings)} warning(s))")
if verbose:
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(f"✓ {plugin_name}: passed")
def main() -> int:
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: uv run scripts/validate-plugin.py <path>")
print()
print("Accepts:")
print(" - Plugin dir: ./my-plugin (must have .claude-plugin/)")
print(" - Parent dir: ./plugins/ (finds all plugins)")
print(" - Glob pattern: './plugins/*'")
print()
print("Examples:")
print(" uv run scripts/validate-plugin.py ./my-plugin")
print(" uv run scripts/validate-plugin.py ./plugins/")
return 1
path_arg = sys.argv[1]
plugin_paths, error = resolve_plugin_paths(path_arg)
if error:
print(f"❌ Error: {error}")
return 1
if not plugin_paths:
print("❌ No plugins found to validate")
return 1
# Validate all plugins
total_errors = 0
total_warnings = 0
failed_plugins = []
# Single plugin - verbose output
if len(plugin_paths) == 1:
path = plugin_paths[0]
errors, warnings = validate_plugin(path)
if errors:
print("❌ Plugin validation FAILED\n")
print("Errors:")
for error in errors:
print(f" ✗ {error}")
print()
if warnings:
print("Warnings:")
for warning in warnings:
print(f" ⚠ {warning}")
print()
if not errors and not warnings:
print("✓ Plugin validation passed")
elif not errors:
print("✓ Plugin valid (with warnings)")
return 1 if errors else 0
# Multiple plugins - summary output
print(f"Validating {len(plugin_paths)} plugin(s)...\n")
for path in plugin_paths:
errors, warnings = validate_plugin(path)
total_errors += len(errors)
total_warnings += len(warnings)
if errors:
failed_plugins.append(path.name)
print_result(path, errors, warnings, verbose=bool(errors))
# Summary
print()
if failed_plugins:
print(f"❌ {len(failed_plugins)} plugin(s) failed: {', '.join(failed_plugins)}")
else:
print(f"✓ All {len(plugin_paths)} plugin(s) passed")
if total_warnings:
print(f" {total_warnings} total warning(s)")
return 1 if failed_plugins else 0
if __name__ == "__main__":
sys.exit(main())