Plugin Development Guide
Build your own Claude Code plugin.
Plugin Structure
plugins/my-plugin/
├── .claude-plugin/
│ └── plugin.json # Required: Plugin metadata
├── hooks/
│ └── hooks.json # Optional: Hook definitions
├── commands/
│ └── my-command.md # Optional: Slash commands
├── hooks-handlers/
│ └── handler.sh # Optional: Hook handler scripts
└── README.md # Recommended: Documentation
plugin.json
Define plugin metadata.
{
"name": "my-plugin",
"version": "1.0.0",
"description": "My awesome plugin"
}
| Field | Required | Description |
|---|---|---|
name | Yes | Plugin name (alphanumeric, hyphens) |
version | Yes | Semantic version |
description | Yes | Brief description |
Adding Hooks
Writing hooks.json
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/on-stop.sh"
}]
}]
}
}
Handler Script
#!/bin/bash
# hooks-handlers/on-stop.sh
# Use environment variables
echo "Session: $SESSION_ID"
echo "Plugin root: $CLAUDE_PLUGIN_ROOT"
# Perform desired action
# ...
Note: Scripts need execute permission
chmod +x hooks-handlers/on-stop.sh
Adding Slash Commands
commands/my-command.md
---
name: mycommand
description: My custom command
---
# My Command
This command does the following:
1. First action
2. Second action
## Usage
\`\`\`
/mycommand [options]
\`\`\`
When users type /mycommand, this markdown is passed as context.
Cross-Platform Support
OS Detection
# Windows Native (Git Bash)
if [ "$OS" = "Windows_NT" ]; then
# Windows handling
# WSL - Check before Linux!
elif grep -qi microsoft /proc/version 2>/dev/null; then
# WSL handling
# macOS
elif [ "$(uname)" = "Darwin" ]; then
# macOS handling
# Linux
else
# Linux handling
fi
Important: WSL returns Linux from uname, so always check for WSL before Linux.
Calling Windows from WSL
# Pass environment variables
export WSLENV="MY_VAR:OTHER_VAR"
export MY_VAR="value"
# Run PowerShell
powershell.exe -File ./script.ps1
# Path conversion
WIN_PATH=$(wslpath -w "$LINUX_PATH")
Minimize Dependencies
# Work without jq
json_get() {
if command -v jq &> /dev/null; then
echo "$1" | jq -r ".$2"
else
echo "$1" | grep -oE "\"$2\":[[:space:]]*\"[^\"]*\"" | \
sed "s/\"$2\":[[:space:]]*\"\(.*\)\"/\1/"
fi
}
Error Handling
Fail Silently
# Bad: Print error message
if ! command -v notify-send &> /dev/null; then
echo "Error: notify-send not found" >&2
exit 1
fi
# Good: Exit quietly
if ! command -v notify-send &> /dev/null; then
exit 0
fi
Plugin errors degrade user experience. Skip quietly when possible.
Session Independence
Multiple terminals may run simultaneously. Use SESSION_ID.
# Session-specific data
DATA_FILE="$DATA_DIR/data-${SESSION_ID}.txt"
# Cleanup on session end
rm -f "$DATA_DIR/*-${SESSION_ID}.*"
Testing
Local Testing
- Create plugin directory
- Add path to Claude Code settings
- Restart Claude Code
- Verify behavior
Platform Testing
| Platform | Method |
|---|---|
| macOS | Local |
| Linux | Docker or VM |
| Windows | Actual Windows or VM |
| WSL | WSL2 |
Distribution
GitHub Release
- Create version tag
- Write release notes
- Provide install.sh script
Contributing Official Plugin
Submit a PR to anthropics/claude-code.
- Add plugin to
plugins/directory - Follow existing plugin style
- Include test matrix
- Submit PR