Skip to main content
Plugins provide a way to package and distribute multiple agent components together. A single plugin can include:
  • Skills: Specialized knowledge and workflows
  • Hooks: Event handlers for tool lifecycle
  • MCP Config: External tool server configurations
  • Agents: Specialized agent definitions
  • Commands: Slash commands
The plugin format is compatible with the Claude Code plugin structure.

Loading Plugins

examples/05_skills_and_plugins/02_loading_plugins/main.py
"""Example: Loading Plugins

This example demonstrates how to load plugins that bundle multiple components:
- Skills (specialized knowledge and workflows)
- Hooks (event handlers for tool lifecycle)
- MCP configuration (external tool servers)
- Agents (specialized agent definitions)
- Commands (slash commands)

Plugins follow the Claude Code plugin structure for compatibility.
See the example_plugins/ directory for a complete plugin structure.
"""

import os
import sys
import tempfile
from pathlib import Path

from pydantic import SecretStr

from openhands.sdk import LLM, Agent, AgentContext, Conversation
from openhands.sdk.plugin import Plugin
from openhands.sdk.tool import Tool
from openhands.tools.file_editor import FileEditorTool
from openhands.tools.terminal import TerminalTool


# Get the directory containing this script
script_dir = Path(__file__).parent
example_plugins_dir = script_dir / "example_plugins"

# =============================================================================
# Part 1: Loading a Single Plugin
# =============================================================================
print("=" * 80)
print("Part 1: Loading a Single Plugin")
print("=" * 80)

plugin_path = example_plugins_dir / "code-quality"
print(f"Loading plugin from: {plugin_path}")

plugin = Plugin.load(plugin_path)

print("\nPlugin loaded successfully!")
print(f"  Name: {plugin.name}")
print(f"  Version: {plugin.version}")
print(f"  Description: {plugin.description}")

# Show manifest details (extra fields are accessible via model_extra)
print("\nManifest details:")
print(f"  Author: {plugin.manifest.author}")
extra = plugin.manifest.model_extra or {}
print(f"  License: {extra.get('license', 'N/A')}")
print(f"  Repository: {extra.get('repository', 'N/A')}")

# =============================================================================
# Part 2: Exploring Plugin Components
# =============================================================================
print("\n" + "=" * 80)
print("Part 2: Exploring Plugin Components")
print("=" * 80)

# Skills
print(f"\nSkills ({len(plugin.skills)}):")
for skill in plugin.skills:
    desc = skill.description or ""
    print(f"  - {skill.name}: {desc[:60]}...")
    if skill.trigger:
        print(f"    Triggers: {skill.trigger}")

# Hooks
hook_config = plugin.hooks
has_hooks = hook_config is not None and not hook_config.is_empty()
print(f"\nHooks: {'Configured' if has_hooks else 'None'}")
if has_hooks:
    assert hook_config is not None
    if hook_config.pre_tool_use:
        print(f"  - PreToolUse: {len(hook_config.pre_tool_use)} matcher(s)")
    if hook_config.post_tool_use:
        print(f"  - PostToolUse: {len(hook_config.post_tool_use)} matcher(s)")
    if hook_config.user_prompt_submit:
        print(f"  - UserPromptSubmit: {len(hook_config.user_prompt_submit)} matcher(s)")
    if hook_config.session_start:
        print(f"  - SessionStart: {len(hook_config.session_start)} matcher(s)")
    if hook_config.session_end:
        print(f"  - SessionEnd: {len(hook_config.session_end)} matcher(s)")
    if hook_config.stop:
        print(f"  - Stop: {len(hook_config.stop)} matcher(s)")

# MCP Config
print(f"\nMCP Config: {'Configured' if plugin.mcp_config else 'None'}")
if plugin.mcp_config is not None:
    servers = plugin.mcp_config.get("mcpServers", {})
    for server_name in servers:
        print(f"  - {server_name}")

# Agents
print(f"\nAgents ({len(plugin.agents)}):")
for agent_def in plugin.agents:
    print(f"  - {agent_def.name}: {agent_def.description[:60]}...")

# Commands
print(f"\nCommands ({len(plugin.commands)}):")
for cmd in plugin.commands:
    print(f"  - /{cmd.name}: {cmd.description[:60]}...")

# =============================================================================
# Part 3: Loading All Plugins from a Directory
# =============================================================================
print("\n" + "=" * 80)
print("Part 3: Loading All Plugins from a Directory")
print("=" * 80)

plugins = Plugin.load_all(example_plugins_dir)
print(f"\nLoaded {len(plugins)} plugin(s) from {example_plugins_dir}")
for p in plugins:
    print(f"  - {p.name} v{p.version}")

# =============================================================================
# Part 4: Using Plugin Components with an Agent
# =============================================================================
print("\n" + "=" * 80)
print("Part 4: Using Plugin Components with an Agent")
print("=" * 80)

# Check for API key
api_key = os.getenv("LLM_API_KEY")
if not api_key:
    print("Skipping agent demo (LLM_API_KEY not set)")
    print("\nTo run the full demo, set the LLM_API_KEY environment variable:")
    print("  export LLM_API_KEY=your-api-key")
    print("EXAMPLE_COST: 0")
    sys.exit(0)

# Configure LLM
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
llm = LLM(
    usage_id="plugin-demo",
    model=model,
    api_key=SecretStr(api_key),
    base_url=os.getenv("LLM_BASE_URL"),
)

# Create agent context with plugin skills
agent_context = AgentContext(
    skills=plugin.skills,
    load_public_skills=False,  # Only use plugin skills for this demo
)

# Create agent with tools and plugin MCP config
tools = [
    Tool(name=TerminalTool.name),
    Tool(name=FileEditorTool.name),
]
agent = Agent(
    llm=llm,
    tools=tools,
    agent_context=agent_context,
    mcp_config=plugin.mcp_config or {},  # Use MCP servers from plugin
)

# Create a temporary directory for the demo
with tempfile.TemporaryDirectory() as tmpdir:
    # Create conversation with plugin hooks
    conversation = Conversation(
        agent=agent,
        workspace=tmpdir,
        hook_config=plugin.hooks,  # Use hooks from plugin
    )

    # Demo 1: Test the skill (triggered by "lint" keyword)
    print("\n--- Demo 1: Skill Triggering ---")
    print("Sending message with 'lint' keyword to trigger skill...")
    conversation.send_message(
        "How do I lint Python code? Just give a brief explanation."
    )
    conversation.run()

    # Demo 2: Test hooks by using file_editor (triggers PostToolUse hook)
    print("\n--- Demo 2: Hook Execution ---")
    print("Creating a file to trigger PostToolUse hook on file_editor...")
    conversation.send_message(
        "Create a file called hello.py with a simple print statement."
    )
    conversation.run()

    # Demo 3: Test MCP by using fetch tool
    print("\n--- Demo 3: MCP Tool Usage ---")
    print("Using fetch MCP tool to retrieve a URL...")
    conversation.send_message(
        "Use the fetch tool to get the content from https://httpbin.org/get "
        "and tell me what the 'origin' field contains."
    )
    conversation.run()

    # Verify hooks executed by checking the hook log file
    print("\n--- Verifying Hook Execution ---")
    hook_log_path = os.path.join(tmpdir, ".hook_log")
    if os.path.exists(hook_log_path):
        print("Hook log file found! Contents:")
        with open(hook_log_path) as f:
            for line in f:
                print(f"  {line.strip()}")
    else:
        print("No hook log file found (hooks may not have executed)")

    print(f"\nTotal cost: ${llm.metrics.accumulated_cost:.4f}")
    print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}")
Running the Example
export LLM_API_KEY="your-api-key"
cd agent-sdk
uv run python examples/05_skills_and_plugins/02_loading_plugins/main.py

Plugin Structure

See the example_plugins directory for a complete working plugin structure.
A plugin follows this directory structure:
plugin-name/
├── .plugin/
│   └── plugin.json          # Plugin metadata (required)
├── skills/                   # Skills directory (optional)
│   └── skill-name/
│       └── SKILL.md         # Skill definition
├── hooks/                    # Hooks directory (optional)
│   └── hooks.json           # Hook definitions
├── agents/                   # Agents directory (optional)
│   └── agent-name.md        # Agent definition
├── commands/                 # Commands directory (optional)
│   └── command-name.md      # Command definition
├── .mcp.json                 # MCP server config (optional)
└── README.md                 # Plugin documentation

Plugin Manifest (plugin.json)

The manifest file defines plugin metadata:
{
  "name": "code-quality",
  "version": "1.0.0",
  "description": "Code quality tools and workflows",
  "author": "openhands",
  "license": "MIT",
  "repository": "https://github.com/example/code-quality-plugin"
}

Skills

Skills are defined in markdown files with YAML frontmatter:
---
name: python-linting
description: Instructions for linting Python code
trigger:
  type: keyword
  keywords:
    - lint
    - linting
    - code quality
---

# Python Linting Skill

Run ruff to check for issues:

\`\`\`bash
ruff check .
\`\`\`

Hooks

Hooks are defined in hooks/hooks.json:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "file_editor",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'File edited: $OPENHANDS_TOOL_NAME'",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

MCP Configuration

MCP servers are configured in .mcp.json:
{
  "mcpServers": {
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"]
    }
  }
}

Using Plugin Components

Loading a Plugin

from openhands.sdk.plugin import Plugin

# Load a single plugin
plugin = Plugin.load("/path/to/plugin")

# Load all plugins from a directory
plugins = Plugin.load_all("/path/to/plugins")

Accessing Components

# Skills
for skill in plugin.skills:
    print(f"Skill: {skill.name}")

# Hooks configuration
if plugin.hooks:
    print(f"Hooks configured: {plugin.hooks}")

# MCP servers
if plugin.mcp_config:
    servers = plugin.mcp_config.get("mcpServers", {})
    print(f"MCP servers: {list(servers.keys())}")

Using with an Agent

# Create agent context with plugin skills
agent_context = AgentContext(
    skills=plugin.skills,
)

# Create agent with plugin MCP config
agent = Agent(
    llm=llm,
    tools=tools,
    mcp_config=plugin.mcp_config or {},
    agent_context=agent_context,
)

# Create conversation with plugin hooks
conversation = Conversation(
    agent=agent,
    hook_config=plugin.hooks,
)

Next Steps

  • Skills - Learn more about skills and triggers
  • Hooks - Understand hook event types
  • MCP Integration - Configure external tool servers