Skip to main content

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"
}
FieldRequiredDescription
nameYesPlugin name (alphanumeric, hyphens)
versionYesSemantic version
descriptionYesBrief 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

  1. Create plugin directory
  2. Add path to Claude Code settings
  3. Restart Claude Code
  4. Verify behavior

Platform Testing

PlatformMethod
macOSLocal
LinuxDocker or VM
WindowsActual Windows or VM
WSLWSL2

Distribution

GitHub Release

  1. Create version tag
  2. Write release notes
  3. Provide install.sh script

Contributing Official Plugin

Submit a PR to anthropics/claude-code.

  1. Add plugin to plugins/ directory
  2. Follow existing plugin style
  3. Include test matrix
  4. Submit PR

References