diff --git a/sdk/guides/plugins.mdx b/sdk/guides/plugins.mdx
index 8f8287d5..65f6acef 100644
--- a/sdk/guides/plugins.mdx
+++ b/sdk/guides/plugins.mdx
@@ -3,10 +3,6 @@ title: Plugins
description: Plugins bundle skills, hooks, MCP servers, agents, and commands into reusable packages that extend agent capabilities.
---
-
-This example is available on GitHub: [examples/05_skills_and_plugins/02_loading_plugins/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/05_skills_and_plugins/02_loading_plugins/main.py)
-
-
Plugins provide a way to package and distribute multiple agent components together. A single plugin can include:
- **Skills**: Specialized knowledge and workflows
@@ -17,203 +13,194 @@ Plugins provide a way to package and distribute multiple agent components togeth
The plugin format is compatible with the [Claude Code plugin structure](https://github.com/anthropics/claude-code/tree/main/plugins).
-## Loading Plugins
+## Recommended: Plugin Loading via Conversation
-```python icon="python" expandable examples/05_skills_and_plugins/02_loading_plugins/main.py
-"""Example: Loading Plugins
+The recommended way to load plugins is via the `plugins` parameter on `Conversation`. This approach:
-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)
+- **Lazy Loading**: Plugins are fetched and loaded on first `send_message()` or `run()` call
+- **Automatic Merging**: Skills, MCP configs, and hooks are merged correctly
+- **Multi-Plugin Support**: Load multiple plugins with proper conflict resolution
+- **Git Support**: Use GitHub shorthand, git URLs, or local paths
+- **Version Pinning**: Pin to specific branches, tags, or commits
+- **Monorepo Support**: Load plugins from subdirectories in monorepos
-Plugins follow the Claude Code plugin structure for compatibility.
-See the example_plugins/ directory for a complete plugin structure.
-"""
+### Basic Usage
-import os
-import sys
-import tempfile
-from pathlib import Path
+```python
+from openhands.sdk import Agent, Conversation
+from openhands.sdk.plugin import PluginSource
-from pydantic import SecretStr
+conversation = Conversation(
+ agent=agent,
+ workspace="./workspace",
+ plugins=[
+ PluginSource(source="github:org/security-plugin", ref="v2.0.0"),
+ PluginSource(source="/local/path/to/plugin"),
+ ],
+)
+```
-from openhands.sdk import LLM, Agent, AgentContext, Conversation
-from openhands.sdk.plugin import Plugin
+### Plugin Source Formats
+
+**GitHub Shorthand**
+```python
+PluginSource(source="github:owner/repo")
+PluginSource(source="github:owner/repo", ref="v1.0.0") # Pin to tag
+PluginSource(source="github:owner/repo", ref="main") # Pin to branch
+```
+
+**Git URL** (works with any git host)
+```python
+PluginSource(source="https://github.com/org/repo.git")
+PluginSource(source="https://gitlab.com/org/repo.git", ref="develop")
+PluginSource(source="git@github.com:org/repo.git") # SSH
+```
+
+**Local Path**
+```python
+PluginSource(source="/absolute/path/to/plugin")
+PluginSource(source="./relative/path/to/plugin")
+```
+
+**Monorepo** (plugin in subdirectory)
+```python
+PluginSource(
+ source="github:org/plugins-monorepo",
+ repo_path="plugins/security",
+)
+```
+
+### Multiple Plugins
+
+When loading multiple plugins, components are merged in order:
+
+- **Skills**: Override by name (last plugin wins)
+- **MCP Config**: Override by server name (last plugin wins)
+- **Hooks**: Concatenate (all hooks run)
+
+```python
+plugins = [
+ PluginSource(source="github:org/base-plugin"),
+ PluginSource(source="github:org/overlay-plugin"), # Overrides base
+]
+```
+
+### Remote Conversations
+
+The same `plugins` parameter works with remote agent servers:
+
+```python
+from openhands.sdk.workspace import RemoteWorkspace
+
+conversation = Conversation(
+ agent=agent,
+ workspace=RemoteWorkspace(host="http://agent-server:8000"),
+ plugins=[
+ PluginSource(source="github:org/plugin", ref="v1.0.0"),
+ ],
+)
+```
+
+Plugins are sent to the server and loaded inside the sandbox.
+
+### Complete Example
+
+
+Full example on GitHub: [examples/05_skills_and_plugins/03_plugin_via_conversation/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/05_skills_and_plugins/03_plugin_via_conversation/main.py)
+
+
+```python
+from openhands.sdk import LLM, Agent, Conversation
+from openhands.sdk.plugin import PluginSource
from openhands.sdk.tool import Tool
-from openhands.tools.file_editor import FileEditorTool
from openhands.tools.terminal import TerminalTool
+# Define plugins to load
+plugins = [
+ PluginSource(source="github:org/code-quality", ref="v1.0.0"),
+]
-# Get the directory containing this script
-script_dir = Path(__file__).parent
-example_plugins_dir = script_dir / "example_plugins"
+# Configure agent
+llm = LLM(usage_id="demo", model="anthropic/claude-sonnet-4-5-20250929", api_key=api_key)
+agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name)])
-# =============================================================================
-# Part 1: Loading a Single Plugin
-# =============================================================================
-print("=" * 80)
-print("Part 1: Loading a Single Plugin")
-print("=" * 80)
+# Create conversation with plugins
+# Note: Plugins are loaded lazily on first send_message() or run() call
+conversation = Conversation(
+ agent=agent,
+ workspace="./workspace",
+ plugins=plugins,
+)
-plugin_path = example_plugins_dir / "code-quality"
-print(f"Loading plugin from: {plugin_path}")
+# This first send_message() triggers plugin loading
+# Skills from plugins are then available to the agent
+conversation.send_message("How do I lint Python code?")
+conversation.run()
+```
-plugin = Plugin.load(plugin_path)
+```bash Running the Example
+export LLM_API_KEY="your-api-key"
+cd agent-sdk
+uv run python examples/05_skills_and_plugins/03_plugin_via_conversation/main.py
+```
-print("\nPlugin loaded successfully!")
-print(f" Name: {plugin.name}")
-print(f" Version: {plugin.version}")
-print(f" Description: {plugin.description}")
+## Manual Plugin Loading
-# 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')}")
+
+For most use cases, the [recommended approach](#recommended-plugin-loading-via-conversation) above is simpler and handles merging automatically.
+
-# =============================================================================
-# Part 2: Exploring Plugin Components
-# =============================================================================
-print("\n" + "=" * 80)
-print("Part 2: Exploring Plugin Components")
-print("=" * 80)
+For advanced use cases where you need fine-grained control over plugin loading, you can use `Plugin.load()` directly:
-# 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}")
+```python
+from openhands.sdk.plugin import Plugin
-# Hooks
-print(f"\nHooks: {'Configured' if plugin.hooks else 'None'}")
-if plugin.hooks:
- for event_type, matchers in plugin.hooks.hooks.items():
- print(f" - {event_type}: {len(matchers)} matcher(s)")
+# Load a single plugin
+plugin = Plugin.load("/path/to/plugin")
-# 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")
- 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),
-)
+# Access plugin components
+print(f"Name: {plugin.name}, Version: {plugin.version}")
+print(f"Skills: {len(plugin.skills)}")
+print(f"Hooks: {'Yes' if plugin.hooks else 'No'}")
+print(f"MCP servers: {list(plugin.mcp_config.get('mcpServers', {}).keys())}")
+
+# Load all plugins from a directory
+plugins = Plugin.load_all("/path/to/plugins")
+```
+
+### Using Components Manually
+
+When loading manually, you wire each component to the agent yourself:
+
+```python
+from openhands.sdk import Agent, AgentContext, Conversation
# Create agent context with plugin skills
agent_context = AgentContext(
skills=plugin.skills,
- load_public_skills=False, # Only use plugin skills for this demo
+ load_public_skills=False,
)
-# Create agent with tools and plugin MCP config
-tools = [
- Tool(name=TerminalTool.name),
- Tool(name=FileEditorTool.name),
-]
+# Create agent with plugin MCP config
agent = Agent(
llm=llm,
tools=tools,
agent_context=agent_context,
- mcp_config=plugin.mcp_config or {}, # Use MCP servers from plugin
+ mcp_config=plugin.mcp_config or {},
)
-# 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}")
+# Create conversation with plugin hooks
+conversation = Conversation(
+ agent=agent,
+ workspace="./workspace",
+ hook_config=plugin.hooks,
+)
```
+
+Full example on GitHub: [examples/05_skills_and_plugins/02_loading_plugins/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/05_skills_and_plugins/02_loading_plugins/main.py)
+
+
```bash Running the Example
export LLM_API_KEY="your-api-key"
cd agent-sdk