from openhands.sdk.plugin import Plugin# Load a single pluginplugin = Plugin.load("/path/to/plugin")# Load all plugins from a directoryplugins = Plugin.load_all("/path/to/plugins")
"""Example: Loading PluginsThis 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 osimport sysimport tempfilefrom pathlib import Pathfrom pydantic import SecretStrfrom openhands.sdk import LLM, Agent, AgentContext, Conversationfrom openhands.sdk.plugin import Pluginfrom openhands.sdk.tool import Toolfrom openhands.tools.file_editor import FileEditorToolfrom openhands.tools.terminal import TerminalTool# Get the directory containing this scriptscript_dir = Path(__file__).parentexample_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)# Skillsprint(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}")# Hookshook_config = plugin.hookshas_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 Configprint(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}")# Agentsprint(f"\nAgents ({len(plugin.agents)}):")for agent_def in plugin.agents: print(f" - {agent_def.name}: {agent_def.description[:60]}...")# Commandsprint(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 keyapi_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 LLMmodel = 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 skillsagent_context = AgentContext( skills=plugin.skills, load_public_skills=False, # Only use plugin skills for this demo)# Create agent with tools and plugin MCP configtools = [ 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 demowith 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}")
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.