12 KiB
Plugin Structure Reference
Complete directory layout and configuration schemas for Claude Code plugins.
Plugin Structure
How you structure plugins depends on how they're distributed.
Single Plugin (Standalone)
Standalone plugins need their own .claude-plugin/plugin.json:
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin manifest (required)
├── commands/
├── agents/
├── hooks/
│ └── hooks.json # Auto-discovered hooks (or inline in plugin.json)
└── README.md
Marketplace with Local Plugins (Consolidated)
When all plugins live in the same repo as the marketplace, use strict: false to consolidate metadata in marketplace.json. Plugins don't need their own manifests:
my-marketplace/
├── .claude-plugin/
│ └── marketplace.json # All metadata here (strict: false)
├── plugin-a/
│ ├── commands/
│ ├── agents/
│ └── README.md
├── plugin-b/
│ ├── skills/
│ └── README.md
└── README.md
Benefits: Single source of truth, no version drift, simpler structure.
Marketplace with External Plugins (Distributed)
When referencing plugins from external repos (GitHub, GitLab, etc.), let each plugin own its manifest:
my-marketplace/
├── .claude-plugin/
│ └── marketplace.json # Points to external repos
└── README.md
# External repos each have:
external-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin owns its manifest
├── commands/
└── README.md
Key principle: External plugins are self-contained. The marketplace.json points to them via source but doesn't define their metadata.
Mixed Approach
Marketplaces can combine both patterns—consolidated for local plugins, distributed for external:
{
"strict": false,
"plugins": [
{"name": "local-plugin", "source": "./local-plugin", "version": "1.0.0"},
{"name": "external-plugin", "source": {"source": "github", "repo": "owner/plugin"}}
]
}
Directory Structure
Minimal Standalone Plugin
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Required: metadata
└── README.md # Required for distribution
Complete Standalone Plugin
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin metadata
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── LICENSE # License file
├── .gitignore # Git ignore patterns
├── commands/ # Slash commands
│ ├── core/ # Core commands
│ │ └── help.md
│ └── advanced/ # Advanced features
│ └── deploy.md
├── agents/ # Custom agents
│ ├── reviewer.md
│ └── analyzer.md
├── skills/ # Reusable skills
│ └── my-skill/
│ └── SKILL.md
├── hooks/ # Event hooks
│ └── hooks.json # Auto-discovered (required format)
├── servers/ # MCP servers
│ └── my-server/
│ ├── server.py
│ └── pyproject.toml
└── scripts/ # Utility scripts
└── setup.sh
plugin.json Schema
Required Fields
| Field | Type | Description | Example |
|---|---|---|---|
name |
string | Unique identifier (kebab-case, no spaces) | "deployment-tools" |
Recommended Metadata Fields
| Field | Type | Description |
|---|---|---|
version |
string | Semantic version (e.g., "1.0.0") |
description |
string | Brief plugin description |
Optional Standard Fields
| Field | Type | Description |
|---|---|---|
author |
object | Creator information |
author.name |
string | Author name |
author.email |
string | Author email |
homepage |
string | Documentation URL |
repository |
string | Source code URL |
license |
string | SPDX identifier (MIT, Apache-2.0) |
keywords |
array | Search tags |
category |
string | Plugin category |
tags |
array | Additional searchability tags |
Component Configuration Fields
| Field | Type | Description |
|---|---|---|
commands |
string|array | Custom paths to command files or directories |
agents |
string|array | Custom paths to agent files |
hooks |
string|object | Hook config path or inline config |
mcpServers |
string|object | MCP config path or inline config |
lspServers |
string|object | LSP config path or inline config |
Behavior Control
| Field | Type | Default | Description |
|---|---|---|---|
strict |
boolean | true |
Set at marketplace level. When false, local plugins don't need .claude-plugin/plugin.json—marketplace defines all metadata. Use false for consolidated local plugins, true (or omit) for external plugins. |
Note: Hooks can be defined inline in plugin.json OR in a separate
hooks/hooks.jsonfile.
Complete Example
{
"name": "enterprise-tools",
"version": "2.1.0",
"description": "Enterprise workflow automation tools",
"author": {
"name": "Enterprise Team",
"email": "team@company.com"
},
"homepage": "https://docs.company.com/plugins",
"repository": "https://github.com/company/enterprise-tools",
"license": "MIT",
"keywords": ["enterprise", "workflow", "automation"],
"category": "productivity",
"commands": [
"./commands/core/",
"./commands/enterprise/"
],
"agents": [
"./agents/security-reviewer.md",
"./agents/compliance-checker.md"
],
"mcpServers": {
"database": {
"command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server",
"args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"],
"env": {
"DB_HOST": "${DATABASE_HOST}",
"DB_PASSWORD": "${DATABASE_PASSWORD}"
}
}
}
}
Note: Hooks can be inlined in plugin.json (add a
"hooks"key) or defined inhooks/hooks.json. See Event Hooks below.
Slash Commands
Command File Structure
Commands are markdown files with YAML frontmatter in commands/.
---
description: "Brief description shown in /help"
---
Command instructions here.
Use {{0}}, {{1}} for parameters.
Parameter Syntax
| Syntax | Description | Example |
|---|---|---|
{{0}} |
First parameter | /cmd value |
{{1}} |
Second parameter | /cmd val1 val2 |
{{0:name}} |
Named (documentation) | {{0:environment}} |
{{...}} |
All remaining | /cmd arg1 arg2 arg3 |
Example Command
---
description: "Deploy to specified environment"
---
Deploy application to {{0:environment}}.
Steps:
1. Validate environment configuration
2. Run pre-deployment checks
3. Deploy application
4. Verify deployment
Custom Agents
Agent File Structure
Agents are markdown files with YAML frontmatter in agents/.
---
description: "What this agent specializes in"
capabilities: ["task1", "task2", "task3"]
allowed-tools: Read, Grep, Glob
---
# Agent Name
Detailed description of the agent's role, expertise, and when Claude should invoke it.
## Capabilities
- Specific task the agent excels at
- Another specialized capability
- When to use this agent vs others
## Context and examples
Provide examples of when this agent should be used.
Agent Frontmatter Fields
| Field | Type | Description |
|---|---|---|
description |
string | Brief explanation of what the agent does |
capabilities |
array | List of tasks the agent can perform (aids discovery) |
allowed-tools |
string | Comma-separated list of allowed tools (optional) |
Tool Restrictions
| Restriction | Tools | Use Case |
|---|---|---|
| Read-only | Read, Grep, Glob |
Analysis only |
| With execution | Read, Grep, Glob, Bash |
Analysis + commands |
| No restriction | (omit field) | Full capabilities |
Event Hooks
Two ways to define hooks in a plugin:
- Inline in plugin.json — Add a
"hooks"key directly - File-based — Auto-discovered from
hooks/hooks.json
Option 1: Inline in plugin.json
{
"name": "my-plugin",
"version": "1.0.0",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
}
]
}
]
}
}
Option 2: Separate hooks.json File
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Can also have hooks inline here
└── hooks/
└── hooks.json # Auto-discovered if present
Hook Types
| Type | When | Use Cases |
|---|---|---|
PreToolUse |
Before tool | Validation, permissions |
PostToolUse |
After tool | Logging, formatting |
UserPromptSubmit |
Before prompt | Input validation |
Stop |
After response | Cleanup, notifications |
SessionStart |
Session begins | Context loading |
SessionEnd |
Session ends | Cleanup |
hooks/hooks.json Format
When using a separate file, it requires a root-level "hooks" wrapper:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"matcher": "Write(*.ts)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 30
}
]
}
]
}
}
Key Points
- Hooks can be inline in plugin.json OR in
hooks/hooks.json - Both formats use a
"hooks"object containing event types - Use
${CLAUDE_PLUGIN_ROOT}for paths relative to plugin - Use
$filefor the affected file path in PostToolUse
Hook Script Interface
Input (stdin):
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/src/file.ts",
"content": "export const foo = 'bar';"
}
}
Output (stdout):
Allow:
{"allowed": true}
Block:
{
"allowed": false,
"message": "Validation failed: reason"
}
Modify:
{
"allowed": true,
"modified_parameters": {
"content": "modified content"
}
}
Example Hook Script
#!/usr/bin/env bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
content=$(echo "$input" | jq -r '.tool_input.content')
# Check for secrets
if echo "$content" | grep -qiE 'api[_-]?key.*=.*[a-zA-Z0-9]{16,}'; then
echo '{"allowed": false, "message": "Potential secret detected"}'
exit 0
fi
echo '{"allowed": true}'
MCP Servers
Server Configuration
{
"mcpServers": {
"server-name": {
"command": "${CLAUDE_PLUGIN_ROOT}/servers/my-server",
"args": ["--flag", "value"],
"env": {
"API_KEY": "${MY_API_KEY}"
}
}
}
}
Variable Substitution
| Variable | Resolves To |
|---|---|
${CLAUDE_PLUGIN_ROOT} |
Plugin installation directory |
${VAR_NAME} |
Environment variable |
Python MCP Server Example
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
async def my_tool(param: str) -> str:
"""Tool description"""
return f"Result: {param}"
if __name__ == "__main__":
mcp.run(transport='stdio')
Platform Considerations
macOS
- Config:
~/Library/Application Support/Claude/ - Logs:
~/Library/Logs/Claude/
Windows
- Config:
%APPDATA%\Claude\ - Use forward slashes or double backslashes
Linux
- Config:
~/.config/claude/ - Check shebang and permissions