Plugins and Hooks — Extend and Customize Claude Code
Find, install, and create plugins for Claude Code, and use hooks to control agent behavior
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.
Prerequisites
This article assumes familiarity with skills and MCP servers. If you haven't worked with those yet, start there.
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.
Pro Tip
Think of a plugin as a "feature pack" for Claude Code. Installing a Terraform plugin, for example, might give you skills for writing HCL, hooks that lint plans before apply, an MCP server connected to your cloud provider, and agent definitions for infrastructure review.
Finding Plugins
Official and Community Sources
| Source | URL | Description |
|---|---|---|
| Anthropic Official | github.com/anthropics/claude-plugins-official | Curated, reviewed plugins maintained by Anthropic |
| Community Marketplace | claudemarketplaces.com | Community-submitted plugins with ratings and reviews |
| npm Registry | npmjs.com | Plugins published as npm packages (search for claude-plugin-*) |
| GitHub Topics | github.com/topics/claude-code-plugin | Open-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 justRead, Grep? Prefer the narrowest scope - Source code — Skim the hooks and skills. A plugin's
plugin.jsonmanifest tells you exactly what it installs
Warning
Plugins can include hooks that run shell commands automatically. Only install plugins from sources you trust. Review the hooks/ directory before enabling any plugin that requests broad tool permissions.
Plugin Categories
The ecosystem organizes around these common categories:
| Category | Examples |
|---|---|
| Development Tools | Linters, formatters, language-specific helpers, framework scaffolders |
| Testing | Test runners, coverage reporters, snapshot managers, mutation testing |
| Documentation | API doc generators, changelog builders, README scaffolders |
| Deployment | CI/CD integrations, infrastructure-as-code, container management |
| Productivity | Time tracking, session logging, notification integrations, project templates |
| Security | Secret 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:
{ "enabledPlugins": [ "claude-plugin-terraform", "claude-plugin-docker", "@myorg/claude-plugin-internal" ]}Version Pinning
Pin plugin versions to avoid breaking changes:
{ "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:
{ "pluginRegistries": [ "https://npm.internal.mycompany.com" ], "enabledPlugins": [ "@myorg/claude-plugin-internal" ]}Activating Changes
After editing your settings, reload plugins without restarting Claude Code:
/reload-pluginsClaude 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:
{ "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
| Field | Description |
|---|---|
name | Unique identifier. Must match the directory name and the enabledPlugins entry. |
description | Short summary shown in plugin listings and when Claude loads the plugin. |
version | Semver version string. Used for version pinning and update detection. |
components.skills | Array of paths to skill directories relative to the plugin root. |
components.agents | Array of paths to agent definition files. |
components.hooks | Hook definitions keyed by lifecycle event. Same format as settings.json hooks. |
components.mcpServers | Path to an .mcp.json file defining MCP server configurations. |
Path Variables
Two built-in variables resolve paths inside plugin code:
| Variable | Resolves To | Use Case |
|---|---|---|
${CLAUDE_PLUGIN_ROOT} | The plugin installation directory | Reference scripts, templates, and skill files |
${CLAUDE_PLUGIN_DATA} | A persistent data directory that survives plugin updates | Store 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:
{ "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
| Hook | When It Fires | Common Use |
|---|---|---|
PreToolUse | Before Claude executes a tool | Block dangerous commands, validate inputs |
PostToolUse | After a tool completes | Auto-format code, log tool usage |
Stop | When Claude finishes its response | Send notifications, trigger CI |
SubagentStop | When a subagent finishes | Validate subagent output |
SessionStart | When a new session begins | Load environment, check prerequisites |
SessionEnd | When a session closes | Clean up temp files, save logs |
UserPromptSubmit | After user submits a prompt | Enrich prompts with context, redirect |
PreCompact | Before context compaction | Save important state before context shrinks |
PostCompact | After context compaction | Inject critical info back into context |
Notification | When Claude generates a notification | Route notifications to Slack, email |
StopFailure | When Claude hits a stop-condition failure | Retry logic, escalation alerts |
CwdChanged | When the working directory changes | Reload environment, update prompts |
FileChanged | When a watched file changes | Trigger rebuilds, run tests |
InstructionsLoaded | After CLAUDE.md and skills are loaded | Inject dynamic instructions |
TaskCreated | When a new task is created | Log 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:
{ "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):
{ "hook": "PreToolUse", "tool": "Bash", "args": { "command": "git push --force origin main" }, "session_id": "abc123", "cwd": "/Users/you/project"}Output format (printed to stdout):
{ "action": "block", "message": "Force-pushing to main is not allowed. Use a feature branch."}The action field controls what happens next:
| Action | Effect |
|---|---|
proceed | Allow the action to continue (default if no output) |
block | Prevent the action. Claude sees the message and adjusts. |
modify | Replace the original args with new values (PreToolUse only). |
inject | Add 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.
{ "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:
{ "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 toolargs.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:
#!/bin/bash# hooks/block-dangerous-git.sh # Read JSON from stdinINPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.args.command // ""') # Check for dangerous patternsif 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 fifi # Allow everything elseecho '{"action": "proceed"}'Settings configuration:
{ "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:
#!/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/nullfi echo '{"action": "proceed"}'{ "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:
#!/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"}'{ "hooks": { "Stop": [ { "command": "./.claude/hooks/notify-slack.sh" } ] }}Inject Context on Session Start
Automatically load project-specific context when a session begins:
#!/bin/bash# hooks/load-context.sh INPUT=$(cat)CWD=$(echo "$INPUT" | jq -r '.cwd // "."') CONTEXT="" # Add recent git activityif [ -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 countif 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" fifi if [ -n "$CONTEXT" ]; then echo "{\"action\": \"inject\", \"message\": \"$CONTEXT\"}"else echo '{"action": "proceed"}'fiCreating a Simple Plugin
Walk through building a plugin from scratch that validates commit messages and enforces conventional commit format.
- 1
Create the plugin directory
Bashmkdir -p my-commit-plugin/{skills/commit-helper,hooks}After this step your directory looks like:
Bashmy-commit-plugin/├── skills/│ └── commit-helper/└── hooks/ - 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
Add a skill
Create
my-commit-plugin/skills/commit-helper/SKILL.md:YAML---name: commit-helperdescription: "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 changes2. Categorize the change: feat, fix, refactor, docs, test, chore, style, perf3. Identify the scope (affected module or component)4. Write a subject line: imperative mood, lowercase, no period, under 72 chars5. 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
Add a hook
Create
my-commit-plugin/hooks/validate-commit.sh:Bash#!/bin/bash# Validate that git commit commands use conventional commit formatINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.args.command // ""')# Extract the commit message from the commandMSG=$(echo "$COMMAND" | grep -oP '(?<=-m\s["\x27])[^"\x27]+')if [ -z "$MSG" ]; then# No -m flag found — could be opening an editor, allow itecho '{"action": "proceed"}'exit 0fi# Check conventional commit format: type(scope): descriptionif echo "$MSG" | grep -qP '^(feat|fix|refactor|docs|test|chore|style|perf|ci|build)(\(.+\))?: .+'; thenecho '{"action": "proceed"}'elseecho '{"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."}'fiMake the hook executable:
Bashchmod +x my-commit-plugin/hooks/validate-commit.sh - 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-pluginsTry a non-conventional commit to verify the hook blocks it:
Bashgit add .git commit -m "updated stuff"# Hook should block this with a message about conventional commit formatThen try a valid one:
Bashgit commit -m "feat(auth): add OAuth2 refresh token handling"# Hook allows this through - 6
Share with your team
Commit the plugin to your repository or publish it:
Bash# Option A: commit to the project repocp -r my-commit-plugin .claude/plugins/git add .claude/plugins/my-commit-plugingit commit -m "feat: add conventional commit plugin"# Option B: publish to npmcd my-commit-pluginnpm init -ynpm publish --access publicTeam 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:
| Plugin | What It Does |
|---|---|
claude-plugin-git-guardian | Hooks that block force-pushes, secret commits, and large binary files. Configurable branch protection rules. |
claude-plugin-test-runner | Skills for running and analyzing tests across Jest, Vitest, pytest, and Go. Auto-runs relevant tests after code changes. |
claude-plugin-docker-dev | Skills and MCP server for managing Docker containers, composing services, and debugging container issues. |
claude-plugin-changelog | Generates changelogs from conventional commits. Hooks into the Stop event to suggest changelog updates after feature work. |
claude-plugin-session-logger | Logs every session to a local SQLite database. Skills to query past sessions, find patterns, and generate usage reports. |
claude-plugin-pr-reviewer | Agent definitions for multi-pass code review. Checks security, performance, testing coverage, and style in parallel. |
claude-plugin-db-migrate | Skills for generating and reviewing database migrations. Supports Prisma, Drizzle, Knex, and raw SQL. |
claude-plugin-i18n | Detects hardcoded strings during edits and suggests internationalization. Hooks auto-extract strings to locale files. |
Install example:
{ "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:
| Mechanism | How It Works | Best For |
|---|---|---|
| Skills | Claude reads instructions and follows them | Workflows, conventions, background knowledge |
| Hooks | Your code runs before/after Claude actions | Guardrails, formatting, logging, notifications |
| MCP Servers | External tools Claude can call | Database 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
- Verify the plugin name in
enabledPluginsmatches thenamefield inplugin.json - Run
/reload-pluginsand check the output for errors - Ensure
plugin.jsonis valid JSON (usejq . plugin.jsonto check) - For local plugins, confirm the path is correct and accessible
Hook Not Firing
- Check that the
ifcondition matches the actual tool and arguments. Test with a simple hook that always logs to a file first - Verify the hook script is executable (
chmod +x) - Make sure the script outputs valid JSON to stdout. Any non-JSON output (warnings, debug prints to stdout) breaks parsing
- 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:
#!/bin/bashINPUT=$(cat) # Wrap logic in a function for clean error handlingrun_hook() { # ... your logic here ... echo '{"action": "proceed"}'} # Default to proceed if anything failsrun_hook || echo '{"action": "proceed"}'Skills From Plugin Not Appearing
Plugin skills use namespaced names: plugin-name:skill-name. Try invoking with the full name:
/my-commit-plugin:commit-helperNext Steps
- Start with hooks — add a single
PreToolUsehook to enforce a team convention - Browse the official directory — find a plugin that matches a workflow you do manually
- Bundle your skills into a plugin — if you have multiple related skills, packaging them makes sharing easier
Related Topics:
- Claude Code Skills — The foundation plugins build on
- Essential MCP Servers — Extend Claude with external tool servers
- Building Agents — Create specialized agent definitions to include in plugins
- Best Practices — Workflows that pair well with plugins and hooks
Success
Plugins and hooks shift Claude Code from "helpful assistant" to "enforceable workflow." Skills suggest behavior; hooks guarantee it. Start with one guardrail hook, see it catch a mistake, and you'll never run without hooks again.