Skip to main content

Plugins and Hooks — Extend and Customize Claude Code

Find, install, and create plugins for Claude Code, and use hooks to control agent behavior

35 minutes
12 min read
Updated March 28, 2026

Plugins and Hooks

Skills are single markdown files. Plugins are full packages — bundling skills, agents, hooks, and MCP servers into one installable unit. If skills are functions, plugins are libraries.

Hooks sit at the other end of the spectrum: low-level interception points that let you run code before or after Claude takes any action. Together, plugins and hooks give you complete control over what Claude can do and how it behaves.


What Are Plugins?

A plugin packages multiple Claude Code extensions into a single installable unit. Where a skill is a single SKILL.md file, a plugin can contain:

  • Skills — reusable workflows and background knowledge
  • Agents — subagent definitions for specialized tasks
  • Hooks — lifecycle interceptors that run before/after Claude actions
  • MCP servers — tool servers that give Claude new capabilities
  • Default settings — preconfigured permissions and preferences

Plugins launched in public beta in October 2025 and reached stable status in early 2026. The ecosystem now includes over 400 plugins spanning development tools, testing frameworks, documentation generators, deployment pipelines, and productivity utilities.


Finding Plugins

Official and Community Sources

SourceURLDescription
Anthropic Officialgithub.com/anthropics/claude-plugins-officialCurated, reviewed plugins maintained by Anthropic
Community Marketplaceclaudemarketplaces.comCommunity-submitted plugins with ratings and reviews
npm Registrynpmjs.comPlugins published as npm packages (search for claude-plugin-*)
GitHub Topicsgithub.com/topics/claude-code-pluginOpen-source plugins tagged on GitHub

Evaluating a Plugin

Before installing a third-party plugin, check these signals:

  • Maintenance status — When was the last commit? Are issues being triaged?
  • Download count — Higher adoption usually means more eyes on the code and fewer bugs
  • Reviews and ratings — Read the negative reviews first; they reveal real limitations
  • Permissions requested — Does the plugin need Bash(*) (full shell access) or just Read, Grep? Prefer the narrowest scope
  • Source code — Skim the hooks and skills. A plugin's plugin.json manifest tells you exactly what it installs

Plugin Categories

The ecosystem organizes around these common categories:

CategoryExamples
Development ToolsLinters, formatters, language-specific helpers, framework scaffolders
TestingTest runners, coverage reporters, snapshot managers, mutation testing
DocumentationAPI doc generators, changelog builders, README scaffolders
DeploymentCI/CD integrations, infrastructure-as-code, container management
ProductivityTime tracking, session logging, notification integrations, project templates
SecuritySecret scanners, dependency auditors, permission guardrails

Installing Plugins

Basic Installation

Add the plugin name to enabledPlugins in your settings file. Plugins can be enabled at the project, user, or enterprise level.

Edit .claude/settings.json in your project root:

JSON
{
"enabledPlugins": [
"claude-plugin-terraform",
"claude-plugin-docker",
"@myorg/claude-plugin-internal"
]
}

Version Pinning

Pin plugin versions to avoid breaking changes:

JSON
{
"enabledPlugins": [
"claude-plugin-terraform@2.1.0",
"claude-plugin-docker@^1.4.0",
"@myorg/claude-plugin-internal@latest"
]
}
  • Exact (@2.1.0) — locks to a specific version
  • Caret (@^1.4.0) — allows patch and minor updates within the major version
  • Latest (@latest) — always uses the newest version (use with caution)

Custom Registries

For private or internal plugins, point to a custom npm registry:

JSON
{
"pluginRegistries": [
"https://npm.internal.mycompany.com"
],
"enabledPlugins": [
"@myorg/claude-plugin-internal"
]
}

Activating Changes

After editing your settings, reload plugins without restarting Claude Code:

Bash
/reload-plugins

Claude confirms which plugins were loaded, skipped, or failed.


Plugin Structure

Every plugin is a directory with a plugin.json manifest at the root. Here is the standard layout:

    • plugin.json
    • settings.json
        • SKILL.md
        • SKILL.md
      • pre-commit-check.sh
      • post-edit-format.sh
    • .mcp.json

The plugin.json Manifest

The manifest declares the plugin's identity and components:

JSON
{
"name": "my-plugin",
"description": "Linting, testing, and code review automation",
"version": "1.2.0",
"author": "Your Name",
"license": "MIT",
"components": {
"skills": ["skills/lint-check", "skills/test-runner"],
"agents": ["agents/code-reviewer.md"],
"hooks": {
"PreToolUse": [
{
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-check.sh",
"if": "Tool(Bash) && args.command matches 'git commit'"
}
],
"PostToolUse": [
{
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-format.sh",
"if": "Tool(Edit)"
}
]
},
"mcpServers": ".mcp.json"
}
}

Key Manifest Fields

FieldDescription
nameUnique identifier. Must match the directory name and the enabledPlugins entry.
descriptionShort summary shown in plugin listings and when Claude loads the plugin.
versionSemver version string. Used for version pinning and update detection.
components.skillsArray of paths to skill directories relative to the plugin root.
components.agentsArray of paths to agent definition files.
components.hooksHook definitions keyed by lifecycle event. Same format as settings.json hooks.
components.mcpServersPath to an .mcp.json file defining MCP server configurations.

Path Variables

Two built-in variables resolve paths inside plugin code:

VariableResolves ToUse Case
${CLAUDE_PLUGIN_ROOT}The plugin installation directoryReference scripts, templates, and skill files
${CLAUDE_PLUGIN_DATA}A persistent data directory that survives plugin updatesStore caches, logs, or state files

Use ${CLAUDE_PLUGIN_ROOT} in hook commands and skill paths so they work regardless of where the plugin is installed. Use ${CLAUDE_PLUGIN_DATA} for anything that should persist across version upgrades.

Plugin Default Settings

A plugin can ship a settings.json file that provides default permissions and preferences:

JSON
{
"permissions": {
"allow": [
"Bash(npx eslint *)",
"Bash(npx prettier *)",
"Read",
"Grep"
]
}
}

Users can override these defaults in their own settings.


Hooks Deep Dive

Hooks are the lowest-level extension mechanism in Claude Code. They intercept specific points in Claude's lifecycle and run your code — a shell command or an HTTP request — before or after the action proceeds.

Unlike skills (which Claude reads and follows) and MCP servers (which expose tools), hooks run your code outside Claude's reasoning. The hook script receives structured JSON, does its work, and returns a JSON response telling Claude what to do.

Hook Types

HookWhen It FiresCommon Use
PreToolUseBefore Claude executes a toolBlock dangerous commands, validate inputs
PostToolUseAfter a tool completesAuto-format code, log tool usage
StopWhen Claude finishes its responseSend notifications, trigger CI
SubagentStopWhen a subagent finishesValidate subagent output
SessionStartWhen a new session beginsLoad environment, check prerequisites
SessionEndWhen a session closesClean up temp files, save logs
UserPromptSubmitAfter user submits a promptEnrich prompts with context, redirect
PreCompactBefore context compactionSave important state before context shrinks
PostCompactAfter context compactionInject critical info back into context
NotificationWhen Claude generates a notificationRoute notifications to Slack, email
StopFailureWhen Claude hits a stop-condition failureRetry logic, escalation alerts
CwdChangedWhen the working directory changesReload environment, update prompts
FileChangedWhen a watched file changesTrigger rebuilds, run tests
InstructionsLoadedAfter CLAUDE.md and skills are loadedInject dynamic instructions
TaskCreatedWhen a new task is createdLog tasks, assign tracking IDs

Shell Command Hooks

The most common hook type. Claude runs your command, pipes JSON to stdin, and reads JSON from stdout.

Configure hooks in settings.json or in a plugin's plugin.json:

JSON
{
"hooks": {
"PreToolUse": [
{
"command": "/path/to/my-hook.sh"
}
]
}
}

The hook script receives a JSON payload on stdin with context about the event. It must return a JSON response on stdout.

Input format (piped to stdin):

JSON
{
"hook": "PreToolUse",
"tool": "Bash",
"args": {
"command": "git push --force origin main"
},
"session_id": "abc123",
"cwd": "/Users/you/project"
}

Output format (printed to stdout):

JSON
{
"action": "block",
"message": "Force-pushing to main is not allowed. Use a feature branch."
}

The action field controls what happens next:

ActionEffect
proceedAllow the action to continue (default if no output)
blockPrevent the action. Claude sees the message and adjusts.
modifyReplace the original args with new values (PreToolUse only).
injectAdd content to Claude's context (PostToolUse, SessionStart, InstructionsLoaded).

HTTP Hooks

Instead of running a local command, send a POST request to a URL endpoint. This is useful for cloud functions, logging services, or centralized team servers.

JSON
{
"hooks": {
"Stop": [
{
"url": "https://hooks.myteam.com/claude-session-complete",
"headers": {
"Authorization": "Bearer ${HOOKS_API_KEY}"
}
}
]
}
}

The POST body is the same JSON payload a shell hook receives on stdin. The response body follows the same action format.

Conditional Hooks

Use the if field to restrict when a hook fires. The syntax mirrors Claude Code's permission rule format:

JSON
{
"hooks": {
"PreToolUse": [
{
"command": "./hooks/block-force-push.sh",
"if": "Tool(Bash) && args.command matches 'git push.*--force'"
},
{
"command": "./hooks/lint-on-edit.sh",
"if": "Tool(Edit) && args.file_path matches '\\.(ts|tsx|js|jsx)$'"
}
],
"PostToolUse": [
{
"command": "./hooks/format-python.sh",
"if": "Tool(Edit) && args.file_path matches '\\.py$'"
}
]
}
}

Without an if field, the hook fires on every occurrence of that event. The conditional syntax supports:

  • Tool(ToolName) — match a specific tool
  • args.field matches 'regex' — match against tool arguments
  • && / || — combine conditions
  • ! — negate a condition

Practical Hook Examples

Block Dangerous Git Commands

Prevent force-pushes and hard resets on protected branches:

Bash
#!/bin/bash
# hooks/block-dangerous-git.sh
# Read JSON from stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.args.command // ""')
# Check for dangerous patterns
if echo "$COMMAND" | grep -qE 'git (push.*--force|reset --hard|clean -fd)'; then
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
echo '{"action": "block", "message": "Blocked: destructive git operation on protected branch '"$BRANCH"'. Switch to a feature branch first."}'
exit 0
fi
fi
# Allow everything else
echo '{"action": "proceed"}'

Settings configuration:

JSON
{
"hooks": {
"PreToolUse": [
{
"command": "./.claude/hooks/block-dangerous-git.sh",
"if": "Tool(Bash) && args.command matches '^git '"
}
]
}
}

Auto-Format Code After Edits

Run Prettier on any TypeScript or JavaScript file Claude edits:

Bash
#!/bin/bash
# hooks/auto-format.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.args.file_path // ""')
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
npx prettier --write "$FILE_PATH" 2>/dev/null
fi
echo '{"action": "proceed"}'
JSON
{
"hooks": {
"PostToolUse": [
{
"command": "./.claude/hooks/auto-format.sh",
"if": "Tool(Edit) && args.file_path matches '\\.(ts|tsx|js|jsx|css)$'"
}
]
}
}

Notify Slack When a Session Completes

Send a summary to a Slack channel when Claude finishes working:

Bash
#!/bin/bash
# hooks/notify-slack.sh
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"')
PROJECT=$(basename "$CWD")
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"Claude Code session complete in *${PROJECT}* (session: ${SESSION_ID})\"
}" > /dev/null 2>&1
echo '{"action": "proceed"}'
JSON
{
"hooks": {
"Stop": [
{
"command": "./.claude/hooks/notify-slack.sh"
}
]
}
}

Inject Context on Session Start

Automatically load project-specific context when a session begins:

Bash
#!/bin/bash
# hooks/load-context.sh
INPUT=$(cat)
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
CONTEXT=""
# Add recent git activity
if [ -d "$CWD/.git" ]; then
RECENT=$(cd "$CWD" && git log --oneline -5 2>/dev/null)
CONTEXT="Recent commits:\n$RECENT\n\n"
fi
# Add open issues count
if command -v gh &> /dev/null; then
ISSUES=$(cd "$CWD" && gh issue list --limit 5 --json title,number 2>/dev/null)
if [ -n "$ISSUES" ]; then
CONTEXT="${CONTEXT}Open issues:\n$ISSUES"
fi
fi
if [ -n "$CONTEXT" ]; then
echo "{\"action\": \"inject\", \"message\": \"$CONTEXT\"}"
else
echo '{"action": "proceed"}'
fi

Creating a Simple Plugin

Walk through building a plugin from scratch that validates commit messages and enforces conventional commit format.

  1. 1

    Create the plugin directory

    Bash
    mkdir -p my-commit-plugin/{skills/commit-helper,hooks}

    After this step your directory looks like:

    Bash
    my-commit-plugin/
    ├── skills/
    │ └── commit-helper/
    └── hooks/
  2. 2

    Write the plugin manifest

    Create my-commit-plugin/plugin.json:

    JSON
    {
    "name": "my-commit-plugin",
    "description": "Enforce conventional commits and provide commit message assistance",
    "version": "0.1.0",
    "author": "Your Name",
    "components": {
    "skills": ["skills/commit-helper"],
    "hooks": {
    "PreToolUse": [
    {
    "command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate-commit.sh",
    "if": "Tool(Bash) && args.command matches 'git commit'"
    }
    ]
    }
    }
    }
  3. 3

    Add a skill

    Create my-commit-plugin/skills/commit-helper/SKILL.md:

    YAML
    ---
    name: commit-helper
    description: "Write a conventional commit message for staged changes. Use when committing code."
    ---
    Write a commit message for the current staged changes:
    1. Run `git diff --cached` to see staged changes
    2. Categorize the change: feat, fix, refactor, docs, test, chore, style, perf
    3. Identify the scope (affected module or component)
    4. Write a subject line: imperative mood, lowercase, no period, under 72 chars
    5. If the change is non-trivial, add a body explaining *why*
    Format: `type(scope): subject`
    IMPORTANT: Always use conventional commit format.
    NEVER: Write vague messages like "update code" or "fix stuff".
  4. 4

    Add a hook

    Create my-commit-plugin/hooks/validate-commit.sh:

    Bash
    #!/bin/bash
    # Validate that git commit commands use conventional commit format
    INPUT=$(cat)
    COMMAND=$(echo "$INPUT" | jq -r '.args.command // ""')
    # Extract the commit message from the command
    MSG=$(echo "$COMMAND" | grep -oP '(?<=-m\s["\x27])[^"\x27]+')
    if [ -z "$MSG" ]; then
    # No -m flag found — could be opening an editor, allow it
    echo '{"action": "proceed"}'
    exit 0
    fi
    # Check conventional commit format: type(scope): description
    if echo "$MSG" | grep -qP '^(feat|fix|refactor|docs|test|chore|style|perf|ci|build)(\(.+\))?: .+'; then
    echo '{"action": "proceed"}'
    else
    echo '{"action": "block", "message": "Commit message must follow conventional commit format: type(scope): description. Valid types: feat, fix, refactor, docs, test, chore, style, perf, ci, build."}'
    fi

    Make the hook executable:

    Bash
    chmod +x my-commit-plugin/hooks/validate-commit.sh
  5. 5

    Test locally

    Enable the plugin by pointing to its local path in your project settings:

    JSON
    {
    "enabledPlugins": [
    "./my-commit-plugin"
    ]
    }

    Reload and test:

    Bash
    /reload-plugins

    Try a non-conventional commit to verify the hook blocks it:

    Bash
    git add .
    git commit -m "updated stuff"
    # Hook should block this with a message about conventional commit format

    Then try a valid one:

    Bash
    git commit -m "feat(auth): add OAuth2 refresh token handling"
    # Hook allows this through
  6. 6

    Share with your team

    Commit the plugin to your repository or publish it:

    Bash
    # Option A: commit to the project repo
    cp -r my-commit-plugin .claude/plugins/
    git add .claude/plugins/my-commit-plugin
    git commit -m "feat: add conventional commit plugin"
    # Option B: publish to npm
    cd my-commit-plugin
    npm init -y
    npm publish --access public

    Team members enable it by adding the plugin name to their enabledPlugins.


Popular Plugins to Try

These well-established plugins cover common workflows. Install any of them by adding the name to enabledPlugins in your settings:

PluginWhat It Does
claude-plugin-git-guardianHooks that block force-pushes, secret commits, and large binary files. Configurable branch protection rules.
claude-plugin-test-runnerSkills for running and analyzing tests across Jest, Vitest, pytest, and Go. Auto-runs relevant tests after code changes.
claude-plugin-docker-devSkills and MCP server for managing Docker containers, composing services, and debugging container issues.
claude-plugin-changelogGenerates changelogs from conventional commits. Hooks into the Stop event to suggest changelog updates after feature work.
claude-plugin-session-loggerLogs every session to a local SQLite database. Skills to query past sessions, find patterns, and generate usage reports.
claude-plugin-pr-reviewerAgent definitions for multi-pass code review. Checks security, performance, testing coverage, and style in parallel.
claude-plugin-db-migrateSkills for generating and reviewing database migrations. Supports Prisma, Drizzle, Knex, and raw SQL.
claude-plugin-i18nDetects hardcoded strings during edits and suggests internationalization. Hooks auto-extract strings to locale files.

Install example:

JSON
{
"enabledPlugins": [
"claude-plugin-git-guardian",
"claude-plugin-test-runner@^2.0.0"
]
}

Hooks vs. Skills vs. MCP Servers

These three extension mechanisms serve different purposes. Choosing the right one matters:

MechanismHow It WorksBest For
SkillsClaude reads instructions and follows themWorkflows, conventions, background knowledge
HooksYour code runs before/after Claude actionsGuardrails, formatting, logging, notifications
MCP ServersExternal tools Claude can callDatabase access, API integrations, external services

Use a skill when you want Claude to understand and follow a process. Skills guide behavior through instructions.

Use a hook when you need deterministic enforcement — blocking bad commands, running formatters, sending notifications. Hooks don't rely on Claude choosing to follow advice; they run unconditionally.

Use an MCP server when Claude needs to interact with an external system — query a database, call an API, search the web. MCP servers give Claude new tools.

A well-designed plugin often combines all three: a skill teaches Claude how to work with a framework, hooks enforce safety constraints, and an MCP server connects to the framework's CLI or API.


Troubleshooting

Plugin Not Loading

  1. Verify the plugin name in enabledPlugins matches the name field in plugin.json
  2. Run /reload-plugins and check the output for errors
  3. Ensure plugin.json is valid JSON (use jq . plugin.json to check)
  4. For local plugins, confirm the path is correct and accessible

Hook Not Firing

  1. Check that the if condition matches the actual tool and arguments. Test with a simple hook that always logs to a file first
  2. Verify the hook script is executable (chmod +x)
  3. Make sure the script outputs valid JSON to stdout. Any non-JSON output (warnings, debug prints to stdout) breaks parsing
  4. Test the script manually: echo '{"hook":"PreToolUse","tool":"Bash","args":{"command":"git commit"}}' | ./hooks/my-hook.sh

Hook Blocks Everything

If a hook returns invalid JSON or exits with a non-zero code, Claude may treat the action as blocked. Add error handling to your scripts:

Bash
#!/bin/bash
INPUT=$(cat)
# Wrap logic in a function for clean error handling
run_hook() {
# ... your logic here ...
echo '{"action": "proceed"}'
}
# Default to proceed if anything fails
run_hook || echo '{"action": "proceed"}'

Skills From Plugin Not Appearing

Plugin skills use namespaced names: plugin-name:skill-name. Try invoking with the full name:

Bash
/my-commit-plugin:commit-helper

Next Steps

  1. Start with hooks — add a single PreToolUse hook to enforce a team convention
  2. Browse the official directory — find a plugin that matches a workflow you do manually
  3. Bundle your skills into a plugin — if you have multiple related skills, packaging them makes sharing easier

Related Topics:

Share this article