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 lifecycle management in the SDK:1. Loading a plugin from GitHub via Conversation (PluginSource)2. Installing plugins to persistent storage (local and GitHub)3. Listing tracked plugins and loading only the enabled ones4. Inspecting the `.installed.json` metadata file and `enabled` flag5. Disabling and re-enabling a plugin without reinstalling it6. Uninstalling plugins from persistent storagePlugins 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 jsonimport osimport tempfilefrom pathlib import Pathfrom pydantic import SecretStrfrom openhands.sdk import LLM, Agent, Conversationfrom openhands.sdk.plugin import ( PluginFetchError, PluginSource, disable_plugin, enable_plugin, 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 TerminalToolscript_dir = Path(__file__).parentlocal_plugin_path = script_dir / "example_plugins" / "code-quality"def print_state(label: str, installed_dir: Path) -> None: """Print tracked, loaded, and persisted plugin state.""" print(f"\n{label}") print("-" * len(label)) installed = list_installed_plugins(installed_dir=installed_dir) print("Tracked plugins:") for info in installed: print(f" - {info.name} (enabled={info.enabled}, source={info.source})") loaded = load_installed_plugins(installed_dir=installed_dir) print(f"Loaded plugins: {[plugin.name for plugin in loaded]}") metadata = json.loads((installed_dir / ".installed.json").read_text()) print("Metadata file:") print(json.dumps(metadata, indent=2))def demo_conversation_with_github_plugin(llm: LLM) -> None: """Demo 1: Load plugin from GitHub via Conversation.""" print("\n" + "=" * 60) print("DEMO 1: Loading plugin from GitHub via Conversation") print("=" * 60) plugins = [ PluginSource( source="github:anthropics/skills", ref="main", ), ] agent = Agent( llm=llm, tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)], ) with tempfile.TemporaryDirectory() as tmpdir: try: conversation = Conversation( agent=agent, workspace=tmpdir, plugins=plugins, ) conversation.send_message( "What's the best way to create a PowerPoint presentation " "programmatically? Check the skill before you answer." ) skills = ( conversation.agent.agent_context.skills if conversation.agent.agent_context else [] ) print(f"✓ Loaded {len(skills)} skill(s) from GitHub plugin") for skill in skills[:5]: print(f" - {skill.name}") if len(skills) > 5: print(f" ... and {len(skills) - 5} more skills") if conversation.resolved_plugins: print("Resolved plugin refs:") for resolved in conversation.resolved_plugins: print(f" - {resolved.source} @ {resolved.resolved_ref}") conversation.run() except PluginFetchError as e: print(f"⚠ Could not fetch from GitHub: {e}") print(" Skipping this demo (network or rate limiting issue)")def demo_install_local_plugin(installed_dir: Path) -> str: """Demo 2: Install a plugin from a local path.""" 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}") return info.namedef demo_install_github_plugin(installed_dir: Path) -> None: """Demo 3: Install a plugin from GitHub to persistent storage.""" print("\n" + "=" * 60) print("DEMO 3: Installing plugin from GitHub") print("=" * 60) try: info = install_plugin( source="github:anthropics/skills", 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}") plugins = load_installed_plugins(installed_dir=installed_dir) for plugin in plugins: if plugin.name != info.name: continue skills = plugin.get_all_skills() print(f" Skills: {len(skills)}") for skill in skills[:5]: desc = skill.description or "(no description)" print(f" - {skill.name}: {desc[:50]}...") if len(skills) > 5: print(f" ... and {len(skills) - 5} more skills") 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 tracked plugins and load the enabled ones.""" print("\n" + "=" * 60) print("DEMO 4: Listing and loading installed plugins") print("=" * 60) print("Tracked plugins:") for info in list_installed_plugins(installed_dir=installed_dir): print(f" - {info.name} v{info.version} (enabled={info.enabled})") 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_enable_disable_plugin(installed_dir: Path, plugin_name: str) -> None: """Demo 5: Disable then re-enable a plugin without reinstalling it.""" print("\n" + "=" * 60) print("DEMO 5: Disabling and re-enabling a plugin") print("=" * 60) print_state("Before disable", installed_dir) assert disable_plugin(plugin_name, installed_dir=installed_dir) is True print_state("After disable", installed_dir) assert plugin_name not in [ plugin.name for plugin in load_installed_plugins(installed_dir=installed_dir) ] metadata = json.loads((installed_dir / ".installed.json").read_text()) assert metadata["plugins"][plugin_name]["enabled"] is False assert enable_plugin(plugin_name, installed_dir=installed_dir) is True print_state("After re-enable", installed_dir) metadata = json.loads((installed_dir / ".installed.json").read_text()) assert metadata["plugins"][plugin_name]["enabled"] is True assert plugin_name in [ plugin.name for plugin in load_installed_plugins(installed_dir=installed_dir) ]def demo_uninstall_plugins(installed_dir: Path) -> None: """Demo 6: Uninstall all tracked plugins.""" print("\n" + "=" * 60) print("DEMO 6: 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)}")if __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 and lifecycle 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-plugins" installed_dir.mkdir() if llm: demo_conversation_with_github_plugin(llm) local_plugin_name = demo_install_local_plugin(installed_dir) demo_install_github_plugin(installed_dir) demo_list_and_load_plugins(installed_dir) demo_enable_disable_plugin(installed_dir, local_plugin_name) 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.
install_plugin(), enable_plugin(), disable_plugin(), and
uninstall_plugin() are exposed from openhands.sdk.plugin, which gives the
CLI a clean SDK surface for /plugin install, /plugin enable,
/plugin disable, and /plugin uninstall.
The ready-to-run example above already demonstrates the full
installed-plugin lifecycle, including toggling the persistent enabled
flag in .installed.json before uninstalling the plugin.Use the same APIs directly when you need a narrower flow: