How the Claude Code Plugin System Architecture Works
Claude Code discovers and activates plugins at startup by scanning the plugins/ directory for .claude-plugin/plugin.json manifests, then registers commands, agents, skills, hooks, and MCP servers without modifying core code.
The Claude Code plugin system architecture provides a declarative, file-based extension mechanism for the anthropics/claude-code repository. By placing a structured directory with a JSON manifest and optional component folders, third-party developers can inject new slash commands, validation hooks, and autonomous agents into the runtime. The system relies on a small Python loader that walks the plugin tree at initialization and wires components into the internal Agent SDK.
Plugin Directory Structure and Manifest
Each plugin lives in its own folder under plugins/ and exposes capabilities through a .claude-plugin/plugin.json manifest file. This JSON descriptor tells Claude Code what components the plugin provides and where to find them.
| Component | Purpose | Location |
|---|---|---|
| Commands | Slash-command definitions (e.g., /my-cmd) invoked by users |
plugins/<plugin-name>/commands/ (markdown files with front-matter) |
| Agents | Autonomous Claude agents for long-running or multi-step work | plugins/<plugin-name>/agents/ |
| Skills | Re-usable skill files (SKILL.md) that agents import |
plugins/<plugin-name>/skills/ |
| Hooks | Event-driven scripts that run before/after tools, on stop, etc. | plugins/<plugin-name>/hooks/ (bash, python, or JSON schemas) |
| MCP Server | Optional Model-Context-Protocol server exposing tools to Claude | .mcp.json at the plugin root |
| Metadata | Name, version, author, and description for discovery | .claude-plugin/plugin.json |
Component Registration and Loading Flow
When Claude Code starts, it executes a four-stage loading process to integrate plugins into the runtime.
Discovery
The runtime walks the plugins/*/.claude-plugin/plugin.json tree to locate every installed plugin.
Manifest Parsing
Each discovered JSON file is loaded into a Python Plugin object. The manifest declares which component types are present and their relative paths within the plugin folder.
Component Registration
- Commands are parsed from markdown front-matter in
commands/*.mdfiles. - Hooks are collected and wrapped in JSON schemas that declare which events they handle (e.g.,
PreToolUse,Stop). - Agents and skills are registered with the internal Agent SDK for dynamic invocation.
- MCP servers defined in
.mcp.jsonare surfaced to Claude as native tools.
Runtime Execution
When a user triggers an event—such as typing a slash command or invoking a tool—Claude Code looks up the matching component and executes it. Hook scripts communicate back to the runtime via stdout using a JSON protocol; specific exit codes signal whether to allow, block, or modify the operation.
Hook System Deep Dive: The Hookify Example
The hookify plugin demonstrates how event-driven hooks intercept tool calls using a RuleEngine (plugins/hookify/core/rule_engine.py). This architecture allows users to block dangerous operations without modifying Claude Code's core.
Rule Definition and Loading
Users write rules in markdown files stored in .claude/ directories (e.g., .claude/hookify.dangerous-rm.local.md). These files contain YAML front-matter defining match conditions and actions.
The config_loader module parses these files into Rule objects:
# plugins/hookify/hooks/pretooluse.py
from hookify.core.config_loader import load_rules
# Load all rules tagged for the 'bash' event
rules = load_rules(event='bash')
Runtime Evaluation
For every incoming tool call (Bash, Write, etc.), the RuleEngine evaluates each rule. If conditions match, the engine can block the operation or emit a warning.
# plugins/hookify/hooks/pretooluse.py
import json, sys
from hookify.core.config_loader import load_rules
from hookify.core.rule_engine import RuleEngine
def main():
input_data = json.load(sys.stdin) # Tool input from Claude
rules = load_rules(event='bash')
engine = RuleEngine()
result = engine.evaluate_rules(rules, input_data)
if result.get("decision") == "block":
# Protocol: JSON stdout with message, exit code 2
print(json.dumps({"systemMessage": result["reason"]}))
sys.exit(2)
# Exit 0 allows the tool to proceed
sys.exit(0)
if __name__ == "__main__":
main()
Example Rule Configuration
Rules are defined in markdown with front-matter:
---
name: dangerous-rm
enabled: true
event: bash
tool_matcher: Bash
conditions:
- field: command
operator: regex_match
pattern: "rm\\s+-rf"
---
⚠️ **Dangerous command** – `rm -rf` can delete important data. Review before running.
Extending Claude Code: Implementation Patterns
| Extension Point | Implementation Method | Key Source Reference |
|---|---|---|
| Add a new command | Create commands/my-cmd.md with front-matter (name, description, tools) and a markdown body sent to Claude when invoked |
plugins/plugin-dev/README.md |
| Add a hook | Drop a script in hooks/pretooluse.py that reads JSON from stdin and writes a JSON response to stdout; use RuleEngine or custom logic |
plugins/hookify/hooks/pretooluse.py |
| Expose a tool via MCP | Add .mcp.json describing the tool schema (name, description, input_schema) |
plugins/plugin-dev/README.md |
| Add a skill | Write skills/my-skill/SKILL.md following the progressive-disclosure pattern; agents reference it with skill: my-skill |
plugins/plugin-dev/skills/skill-development/SKILL.md |
Minimal Plugin Manifest
Every plugin requires a plugin.json manifest at .claude-plugin/plugin.json:
{
"name": "my-example-plugin",
"description": "Demo plugin showing the manifest format",
"version": "0.1.0",
"author": { "name": "Your Name", "email": "[email protected]" }
}
Simple Slash Command Definition
Create commands/hello.md to register /hello:
---
name: hello
description: Say hello to Claude
tools: []
---
Hello, Claude! 👋
When the user types /hello, Claude renders the markdown body as the response.
Summary
- Discovery mechanism: Claude Code scans
plugins/*/.claude-plugin/plugin.jsonat startup to find extensions. - Component types: Plugins can provide commands (slash commands), hooks (event interceptors), agents (autonomous workers), skills (reusable prompts), and MCP servers (tool schemas).
- Hook protocol: Hook scripts communicate via JSON over stdin/stdout, using exit code
2to block operations and code0to allow them. - Rule engine: The
hookifyplugin demonstrates pluggable validation usingconfig_loader.pyandrule_engine.pyto evaluate markdown-defined rules against tool inputs. - Zero-core changes: The architecture supports new functionality through file placement alone, as implemented in
anthropics/claude-code.
Frequently Asked Questions
What file triggers plugin discovery in Claude Code?
Claude Code looks for .claude-plugin/plugin.json inside each immediate subdirectory of plugins/. This JSON manifest declares the plugin's metadata and available components, enabling the runtime to register the extension without additional configuration.
How do hooks communicate blocking decisions to the runtime?
Hooks write a JSON object to stdout and exit with a specific code. Exit code 0 allows the operation to proceed, while exit code 2 blocks it and displays the systemMessage field from the JSON response to the user. This protocol is implemented in files like plugins/hookify/hooks/pretooluse.py.
Can plugins expose new tools to Claude via MCP?
Yes. By adding a .mcp.json file at the plugin root that defines the tool name, description, and input schema, Claude Code automatically surfaces the tool to the user through the Model-Context-Protocol integration.
Where should skill files be placed in a plugin directory?
Skills belong in plugins/<plugin-name>/skills/ as SKILL.md files following the progressive-disclosure pattern. Agents can then reference these skills using the syntax skill: my-skill to import the capabilities defined in the markdown.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →