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 and Managing PluginsThis example demonstrates plugin loading and management in the SDK:1. Loading plugins via Conversation (PluginSource)2. Installing plugins to persistent storage3. Listing, updating, and uninstalling pluginsPlugins bundle skills, hooks, and MCP config together.Supported plugin sources:- Local path: /path/to/plugin- GitHub shorthand: github:owner/repo- Git URL: https://github.com/owner/repo.git- With ref: branch, tag, or commit SHA- With repo_path: subdirectory for monoreposFor full documentation, see: https://docs.all-hands.dev/sdk/guides/plugins"""import osimport tempfilefrom pathlib import Pathfrom pydantic import SecretStrfrom openhands.sdk import LLM, Agent, Conversationfrom openhands.sdk.plugin import ( PluginFetchError, PluginSource, install_plugin, list_installed_plugins, load_installed_plugins, uninstall_plugin,)from openhands.sdk.tool import Toolfrom openhands.tools.file_editor import FileEditorToolfrom openhands.tools.terminal import TerminalTool# Locate example plugin directoryscript_dir = Path(__file__).parentlocal_plugin_path = script_dir / "example_plugins" / "code-quality"def demo_conversation_with_plugins(llm: LLM) -> None: """Demo 1: Load plugins via Conversation's plugins parameter. This is the recommended way to use plugins - they are loaded lazily when the conversation starts. """ print("\n" + "=" * 60) print("DEMO 1: Loading plugins via Conversation") print("=" * 60) # Define plugins to load plugins = [ PluginSource(source=str(local_plugin_path)), # Examples of other sources: # PluginSource(source="github:owner/repo", ref="v1.0.0"), # PluginSource(source="github:owner/monorepo", repo_path="plugins/my-plugin"), ] agent = Agent( llm=llm, tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)], ) with tempfile.TemporaryDirectory() as tmpdir: conversation = Conversation( agent=agent, workspace=tmpdir, plugins=plugins, ) # The "lint" keyword triggers the python-linting skill conversation.send_message("How do I lint Python code? Brief answer please.") # Verify skills were loaded skills = ( conversation.agent.agent_context.skills if conversation.agent.agent_context else [] ) print(f"✓ Loaded {len(skills)} skill(s) from plugins") conversation.run()def demo_install_local_plugin(installed_dir: Path) -> None: """Demo 2: Install a plugin from a local path. Useful for development or local-only plugins. """ print("\n" + "=" * 60) print("DEMO 2: Installing plugin from local path") print("=" * 60) info = install_plugin(source=str(local_plugin_path), installed_dir=installed_dir) print(f"✓ Installed: {info.name} v{info.version}") print(f" Source: {info.source}") print(f" Path: {info.install_path}")def demo_install_github_plugin(installed_dir: Path) -> None: """Demo 3: Install a plugin from GitHub. Demonstrates the github:owner/repo shorthand with repo_path for monorepos. """ print("\n" + "=" * 60) print("DEMO 3: Installing plugin from GitHub") print("=" * 60) try: # Install from anthropics/skills repository info = install_plugin( source="github:anthropics/skills", repo_path="skills/pptx", ref="main", installed_dir=installed_dir, ) print(f"✓ Installed: {info.name} v{info.version}") print(f" Source: {info.source}") print(f" Resolved ref: {info.resolved_ref}") except PluginFetchError as e: print(f"⚠ Could not fetch from GitHub: {e}") print(" (Network or rate limiting issue)")def demo_list_and_load_plugins(installed_dir: Path) -> None: """Demo 4: List and load installed plugins.""" print("\n" + "=" * 60) print("DEMO 4: List and load installed plugins") print("=" * 60) # List installed plugins print("Installed plugins:") for info in list_installed_plugins(installed_dir=installed_dir): print(f" - {info.name} v{info.version} ({info.source})") # Load plugins as Plugin objects plugins = load_installed_plugins(installed_dir=installed_dir) print(f"\nLoaded {len(plugins)} plugin(s):") for plugin in plugins: skills = plugin.get_all_skills() print(f" - {plugin.name}: {len(skills)} skill(s)")def demo_uninstall_plugins(installed_dir: Path) -> None: """Demo 5: Uninstall plugins.""" print("\n" + "=" * 60) print("DEMO 5: Uninstalling plugins") print("=" * 60) for info in list_installed_plugins(installed_dir=installed_dir): uninstall_plugin(info.name, installed_dir=installed_dir) print(f"✓ Uninstalled: {info.name}") remaining = list_installed_plugins(installed_dir=installed_dir) print(f"\nRemaining plugins: {len(remaining)}")# Main executionif __name__ == "__main__": api_key = os.getenv("LLM_API_KEY") if not api_key: print("Set LLM_API_KEY to run the full example") print("Running install/uninstall demos only...") llm = None else: 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"), ) with tempfile.TemporaryDirectory() as tmpdir: installed_dir = Path(tmpdir) / "installed" installed_dir.mkdir() # Demo 1: Conversation with plugins (requires LLM) if llm: demo_conversation_with_plugins(llm) # Demo 2-5: Plugin management (no LLM required) demo_install_local_plugin(installed_dir) demo_install_github_plugin(installed_dir) demo_list_and_load_plugins(installed_dir) demo_uninstall_plugins(installed_dir) print("\n" + "=" * 60) print("EXAMPLE COMPLETED SUCCESSFULLY") print("=" * 60) if llm: print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}") else: print("EXAMPLE_COST: 0")
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.
The SDK provides utilities to install plugins to a local directory (~/.openhands/plugins/installed/ by default). Installed plugins are tracked in .installed.json, which stores metadata including a persistent enabled flag.Use list_installed_plugins() to see all tracked plugins (enabled and disabled). Use load_installed_plugins() to load only enabled plugins. Toggle plugins on/off with enable_plugin() and disable_plugin() without uninstalling.
from openhands.sdk.plugin import ( disable_plugin, enable_plugin, install_plugin, list_installed_plugins, load_installed_plugins, uninstall_plugin,)# Install from local path or GitHubinstall_plugin(source="/path/to/plugin")install_plugin(source="github:owner/repo", ref="v1.0.0")# List installed plugins (includes enabled + disabled)for info in list_installed_plugins(): status = "enabled" if info.enabled else "disabled" print(f"{info.name} v{info.version} ({status})")# Disable a plugin (won't be loaded until re-enabled)disable_plugin("plugin-name")# Load only enabled plugins for your agentplugins = load_installed_plugins()# Later: re-enable and reloadenable_plugin("plugin-name")plugins = load_installed_plugins()# Uninstalluninstall_plugin("plugin-name")