Interpretive guidance for designing Claude Code plugins. Helps you understand plugin architecture, marketplace distribution, and when plugins are the right pattern. Use when creating or reviewing plugins.
Provides interpretive guidance for designing Claude Code plugins. Use when creating or reviewing plugins to understand plugin architecture, marketplace distribution, and when plugins are the right pattern.
/plugin marketplace add racurry/neat-little-package/plugin install box-factory@neat-little-packageThis skill inherits all available tools. When active, it can use any tool Claude has access to.
readme-style.mdThis skill provides interpretive guidance and best practices for creating Claude Code plugins. It helps you understand what the docs mean and how to create excellent plugins.
Plugins are distribution wrappers, not functionality.
Core principles:
.claude-plugin/Design test: If asking "should this be a plugin?", wrong question. Ask "should I package these existing components for sharing?"
Deep dive: Core Understanding - Detailed explanation of packaging model and critical directory structure. Traverse when: Creating first plugin, debugging installation issues, understanding why components won't load. Skip when: Experienced with plugin structure, focusing on specific aspect like MCP bundling.
| If you need to... | Go to section |
|---|---|
| Understand plugin fundamentals | Core Understanding |
| Configure plugin.json | Plugin Manifest |
| Write the plugin README | readme-style.md |
| Bundle MCP servers with plugin | MCP Server Configuration |
| Set up marketplace distribution | Marketplace Distribution |
| Decide if plugin is right pattern | Decision Framework |
| Avoid common mistakes | Common Pitfalls |
| Validate before publishing | Quality Checklist |
Minimal plugin structure:
my-plugin/
├── .claude-plugin/
│ └── plugin.json # {"name": "my-plugin", "version": "1.0.0"}
├── commands/
│ └── my-command.md
├── agents/
│ └── my-agent.md
└── skills/
└── my-skill/
└── SKILL.md
Critical rule: Components (commands/, agents/, skills/, hooks/) MUST be at plugin root, NOT inside .claude-plugin/.
Validation:
claude plugin validate .
Claude Code changes rapidly and is post-training knowledge. Fetch these docs when creating plugins to ensure current syntax:
Key insight: Plugins don't add new capabilities to Claude Code. They package existing extension points (commands, agents, skills, hooks, MCP servers) for distribution.
What this means:
Decision test: If you're asking "should this be a plugin?", you're asking the wrong question. Ask "should I package these existing components for sharing?"
The #1 mistake that causes plugins to install but not work:
✅ Correct:
plugin-name/
├── .claude-plugin/
│ └── plugin.json ← Only metadata here
├── commands/ ← At plugin root
├── agents/ ← At plugin root
├── skills/ ← At plugin root
└── hooks/ ← At plugin root
❌ Wrong (won't work):
plugin-name/
└── .claude-plugin/
├── plugin.json
├── commands/ ← Won't be found!
└── agents/ ← Won't be found!
Official docs say: "All other directories (commands/, agents/, skills/, hooks/) must be at the plugin root, not inside .claude-plugin/."
Skills use subdirectories with a SKILL.md file:
skills/
├── skill-one/
│ ├── SKILL.md ← Skill definition
│ └── helpers.py ← Supporting files (optional)
└── skill-two/
└── SKILL.md
Located at .claude-plugin/plugin.json:
| Field | Type | Purpose |
|---|---|---|
| version | string | Semantic versioning (e.g., "2.1.0") |
| description | string | Brief plugin purpose explanation |
| Field | Type | Purpose |
|---|---|---|
| author | object | Author details: {name, email, url} |
| repository | string | Source code location |
| homepage | string | Documentation URL |
| license | string | License identifier (MIT, Apache-2.0, etc.) |
| keywords | array | Discovery tags for categorization |
Override default locations:
Note: Custom paths supplement default directories, they don't replace them.
Best practice: Always include description and version even though they're optional - improves discoverability and user trust. Do NOT include author, repository, homepage, license, or keywords unless explicitly specified by the user.
Use ${CLAUDE_PLUGIN_ROOT} in paths to reference the plugin's absolute directory.
Specify additional locations for components:
{
"name": "my-plugin",
"commands": ["./extra-commands", "./legacy/commands"],
"agents": "./custom-agents"
}
All paths must be relative to plugin root and start with ./
Define hooks or MCP servers directly in plugin.json instead of separate files:
{
"name": "my-plugin",
"hooks": {
"PreToolUse:Bash": "./hooks/bash-guard.sh"
},
"mcpServers": {
"custom-server": {
"command": "node",
"args": ["./server.js"]
}
}
}
CRITICAL: When configuring MCP servers in plugins, follow these security, transport, and maintainability patterns.
Note: This section covers bundling MCP servers WITH plugins. For adding MCP servers to any project (not plugins), load the mcp-config skill instead.
Deep dive on MCP bundling: This section provides comprehensive guidance on transport selection, security patterns, OAuth flow, and validation. Traverse when: Bundling MCP servers with plugin, configuring external services, handling authentication, troubleshooting MCP issues. Skip when: Plugin has no MCP servers, basic plugin creation only.
For current MCP configuration syntax, fetch:
Claude Code supports three MCP transport types. Choose based on server location:
| Transport | Use Case | Example |
|---|---|---|
| HTTP | Remote/cloud servers (preferred) | https://mcp.notion.com/mcp |
| Stdio | Local processes | npx -y @modelcontextprotocol/server-github |
| SSE | Legacy remote servers (deprecated) | Use HTTP instead |
Key insight: Claude Code supports HTTP transport natively - no proxy needed for remote servers (unlike Claude Desktop which requires mcp-proxy).
For plugins bundling remote services:
{
"cloud-service": {
"type": "http",
"url": "${SERVICE_MCP_URL}",
"headers": {
"Authorization": "Bearer ${SERVICE_TOKEN}"
}
}
}
For plugins bundling local tools:
{
"local-tool": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
}
Decision framework:
type: http with URLcommand with stdio/mcp after plugin installFor MCP servers requiring OAuth (not API keys):
.mcp.json/mcp to authenticate"Plugin config (no credentials needed):
{
"oauth-service": {
"type": "http",
"url": "https://mcp.service.com/mcp"
}
}
README instructions:
## Authentication
After enabling plugin, run `/mcp` in Claude Code to authenticate with Service Name.
Claude knows: Both inline and external MCP configuration are valid per official docs.
User prefers external files for:
CRITICAL: The .mcp.json file alone does nothing. You MUST reference it in plugin.json:
// plugin.json - mcpServers field REQUIRED
{
"name": "my-plugin",
"mcpServers": "./.mcp.json"
}
// .mcp.json (at plugin root) - wrapped format per official docs
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
}
}
Format note: Official docs show wrapped format. Empirically, Claude Code also accepts flat format ({ "server-name": {...} } without mcpServers wrapper). Recommend wrapped to match official docs.
Common mistake: Creating .mcp.json without adding "mcpServers": "./.mcp.json" to plugin.json. The file will be ignored.
Known issue as of December 2024: Environment variable interpolation in the url field may not work in plugin .mcp.json files.
Before using this workaround, verify if still needed:
"url": "${MY_TEST_VAR}api/mcp"export MY_TEST_VAR="https://example.com/"Known interpolation behavior:
| Field | Plugin Interpolation |
|---|---|
url | ❌ May be broken |
args | ✅ Works |
env | ✅ Works |
Workaround if bug still exists: Use mcp-proxy via stdio instead of native HTTP transport:
// BROKEN
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "${SERVER_URL}api/mcp"
}
}
}
// WORKAROUND
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-proxy", "http", "${SERVER_URL}api/mcp"]
}
}
}
When bug is fixed: Remove this workaround and revert to native HTTP transport: "type": "http", "url": "${VAR}...". Monitor Claude Code release notes for environment variable interpolation fixes.
❌ WRONG - Inline configuration:
{
"name": "my-plugin",
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": ""
}
}
}
}
Why external files are better:
✅ CORRECT - Environment variable reference:
{
"server-name": {
"command": "server-binary",
"env": {
"API_KEY": "${API_KEY}",
"AUTH_TOKEN": "${AUTH_TOKEN}",
"DATABASE_URL": "${DATABASE_URL}"
}
}
}
❌ WRONG - Hardcoded empty strings:
{
"server-name": {
"env": {
"API_KEY": "",
"AUTH_TOKEN": "",
"DATABASE_URL": ""
}
}
}
❌ VERY WRONG - Hardcoded actual secrets:
{
"server-name": {
"env": {
"API_KEY": "sk_live_abc123..."
}
}
}
Why environment variables are critical:
${VAR_NAME} is the established MCP patternIn your README, always include:
## MCP Server Setup
This plugin includes the [Server Name] MCP server.
### Prerequisites
- [Tool requirements, e.g., Node.js, Python]
- [Service account or API access]
### Configuration
1. Obtain credentials:
- Visit [credential URL]
- Create [token/API key] with `scope1`, `scope2` permissions
2. Set environment variables:
```bash
export API_KEY="your_key_here"
export AUTH_TOKEN="your_token_here"
**Required documentation elements:**
- Prerequisites (tools, accounts, permissions)
- How to obtain credentials with specific scopes/permissions
- Exact environment variable names to set
- Example export commands
- When the server starts (usually: when plugin enabled)
#### Common MCP Server Types
**GitHub:**
```json
{
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
}
Filesystem:
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
"env": {}
}
}
Database:
{
"database": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "${DATABASE_URL}"
}
}
}
Custom server using plugin root:
{
"custom": {
"command": "${CLAUDE_PLUGIN_ROOT}/servers/custom-server",
"args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"],
"env": {
"CUSTOM_API_KEY": "${CUSTOM_API_KEY}"
}
}
}
Before publishing plugin with MCP servers:
Security:
${ENV_VAR} references (never hardcoded)Structure:
.mcp.json or custom path)./Documentation:
Testing:
Located at .claude-plugin/marketplace.json:
Required fields:
".." since marketplace.json is in .claude-plugin/)Example marketplace:
{
"name": "marketplace-name",
"owner": { "name": "owner-name", "url": "https://..." },
"metadata": {
"pluginRoot": ".."
},
"plugins": [...]
}
Plugin entry fields:
{
"name": "plugin-name",
"source": "./plugins/plugin-name",
"description": "Optional override",
"strict": true
}
Path resolution: Plugin source paths are resolved relative to pluginRoot. Since marketplace.json lives in .claude-plugin/, the ".." tells Claude Code to go up one level to the repository root before resolving ./plugins/... paths.
Local path (development):
"source": "./plugins/my-plugin"
GitHub repository:
"source": {
"source": "github",
"repo": "owner/repo"
}
Git URL:
"source": {
"source": "url",
"url": "https://gitlab.com/user/plugin.git"
}
strict Fieldstrict: true (default): Plugin must have its own plugin.jsonstrict: false: Marketplace entry serves as complete manifestUse strict: false when: Packaging third-party content without modifying it.
Create plugin when:
/plugin install)Don't create plugin when:
Good plugin scope:
Bad plugin scope:
mcp-config skill)The docs show the mechanics. Here's the philosophy:
.claude/ directories for iterationclaude plugin validate . command/plugin marketplace add ./path/to/marketplaceKey insight: Don't start with plugin structure. Build components, then package them.
Key constraint: Claude Code plugins distribute via git clone - you can't exclude files like npm's .npmignore or Python's pyproject.toml exclusions.
Keep tests in the repo but minimize their footprint:
my-plugin/
├── .claude-plugin/plugin.json
├── hooks/
│ └── my-hook.py
├── tests/ # Development tests
│ └── test_my_hook.py # Lightweight, focused
└── README.md # Document that tests/ is for dev
Guidelines:
tests/ directory at plugin roottests/ is for development onlyThe disk space impact is negligible for most plugins:
For Python hook scripts using UV:
# Run tests directly
python -m pytest tests/
# Or test the hook script itself
echo '{"tool_name": "Write", "tool_input": {"file_path": "test.py"}}' | ./hooks/my-hook.py
For bash hooks:
# Test with sample input
echo '{"tool_name": "Bash"}' | ./hooks/my-hook.sh
echo $? # Check exit code
Best practice: Test hooks with realistic stdin JSON matching what Claude Code provides.
Validate plugin structure:
claude plugin validate .
Checks JSON syntax and directory structure.
Add local marketplace for testing:
/plugin marketplace add ./absolute/path/to/marketplace
Install from local marketplace:
/plugin install plugin-name@marketplace-name
Verify installation:
Check /help for newly installed commands and agents.
Problem: Putting commands/agents inside .claude-plugin/
Why it fails: Claude Code only looks at plugin root for components
Solution: Keep .claude-plugin/ for metadata only, put all components at root
Problem: Creating plugin for single command or concept
Why it fails:
Better: Share the markdown file directly, create plugin later if collection grows
Problem: "All my utilities" plugin with unrelated components
Why it fails:
Better: Focused plugins (testing tools, git helpers, docs generators)
Problem: Plugin without README or usage examples
Why it fails:
Solution: Focused README with:
Anti-pattern: Don't over-document. No roadmaps, planned features, or verbose error handling sections.
Problem: Breaking changes in minor/patch versions
Why it fails: Users expect:
1.0.0 → 1.0.1 - Bug fixes only, safe to auto-update1.0.0 → 1.1.0 - New features, backward compatible1.0.0 → 2.0.0 - Breaking changes, needs migrationSolution: Follow semver strictly, document breaking changes
Embedded (monorepo approach):
marketplace/
├── .claude-plugin/marketplace.json
└── plugins/
├── plugin-one/
└── plugin-two/
Pros:
Cons:
External (distributed approach):
{
"plugins": [
{
"name": "plugin-one",
"source": {
"source": "github",
"repo": "org/plugin-one"
}
}
]
}
Pros:
Cons:
Official mechanism: Configure .claude/settings.json at repository level:
{
"extraKnownMarketplaces": ["owner/marketplace-repo"],
"enabledPlugins": ["plugin-name@marketplace-name"]
}
Best practices to consider:
Before publishing:
Structure (from official docs):
plugin.json at .claude-plugin/plugin.json.claude-plugin/)claude plugin validate . passes./Metadata (best practices):
Documentation (best practices):
Design (best practices):
MCP Servers (if bundled):
Basic (what docs show):
{
"name": "my-plugin",
"description": "A plugin",
"version": "1.0.0"
}
Issues:
Excellent (applying best practices):
{
"name": "python-testing-suite",
"version": "1.0.0",
"description": "Comprehensive Python testing tools with pytest integration, coverage reporting, and failure analysis"
}
Improvements: