playbook/outfitter-agents/plugins/outfitter/skills/claude-plugins/references/structure.md

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"
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.json file.

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 in hooks/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:

  1. Inline in plugin.json — Add a "hooks" key directly
  2. 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 $file for 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