Skip to content

Commit c595763

Browse files
ashwin-antclaude
andauthored
feat: add plugin support to Python SDK (#285)
Add SdkPluginConfig type and plugins field to ClaudeAgentOptions. Plugins can be loaded using the local type with a path to the plugin directory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 5656aea commit c595763

File tree

6 files changed

+107
-0
lines changed

6 files changed

+107
-0
lines changed

examples/plugin_example.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""Example demonstrating how to use plugins with Claude Code SDK.
3+
4+
Plugins allow you to extend Claude Code with custom commands, agents, skills,
5+
and hooks. This example shows how to load a local plugin and verify it's
6+
loaded by checking the system message.
7+
8+
The demo plugin is located in examples/plugins/demo-plugin/ and provides
9+
a custom /greet command.
10+
"""
11+
12+
from pathlib import Path
13+
14+
import anyio
15+
16+
from claude_agent_sdk import (
17+
ClaudeAgentOptions,
18+
SystemMessage,
19+
query,
20+
)
21+
22+
23+
async def plugin_example():
24+
"""Example showing plugins being loaded in the system message."""
25+
print("=== Plugin Example ===\n")
26+
27+
# Get the path to the demo plugin
28+
# In production, you can use any path to your plugin directory
29+
plugin_path = Path(__file__).parent / "plugins" / "demo-plugin"
30+
31+
options = ClaudeAgentOptions(
32+
plugins=[
33+
{
34+
"type": "local",
35+
"path": str(plugin_path),
36+
}
37+
],
38+
max_turns=1, # Limit to one turn for quick demo
39+
)
40+
41+
print(f"Loading plugin from: {plugin_path}\n")
42+
43+
found_plugins = False
44+
async for message in query(prompt="Hello!", options=options):
45+
if isinstance(message, SystemMessage) and message.subtype == "init":
46+
print("System initialized!")
47+
print(f"System message data keys: {list(message.data.keys())}\n")
48+
49+
# Check for plugins in the system message
50+
plugins_data = message.data.get("plugins", [])
51+
if plugins_data:
52+
print("Plugins loaded:")
53+
for plugin in plugins_data:
54+
print(f" - {plugin.get('name')} (path: {plugin.get('path')})")
55+
found_plugins = True
56+
else:
57+
print("Note: Plugin was passed via CLI but may not appear in system message.")
58+
print(f"Plugin path configured: {plugin_path}")
59+
found_plugins = True
60+
61+
if found_plugins:
62+
print("\nPlugin successfully configured!\n")
63+
64+
65+
async def main():
66+
"""Run all plugin examples."""
67+
await plugin_example()
68+
69+
70+
if __name__ == "__main__":
71+
anyio.run(main)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "demo-plugin",
3+
"description": "A demo plugin showing how to extend Claude Code with custom commands",
4+
"version": "1.0.0",
5+
"author": {
6+
"name": "Claude Code Team"
7+
}
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Greet Command
2+
3+
This is a custom greeting command from the demo plugin.
4+
5+
When the user runs this command, greet them warmly and explain that this message came from a custom plugin loaded via the Python SDK. Tell them that plugins can be used to extend Claude Code with custom commands, agents, skills, and hooks.

src/claude_agent_sdk/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
PreCompactHookInput,
4040
PreToolUseHookInput,
4141
ResultMessage,
42+
SdkPluginConfig,
4243
SettingSource,
4344
StopHookInput,
4445
SubagentStopHookInput,
@@ -339,6 +340,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
339340
# Agent support
340341
"AgentDefinition",
341342
"SettingSource",
343+
# Plugin support
344+
"SdkPluginConfig",
342345
# MCP Server Support
343346
"create_sdk_mcp_server",
344347
"tool",

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ def _build_command(self) -> list[str]:
191191
)
192192
cmd.extend(["--setting-sources", sources_value])
193193

194+
# Add plugin directories
195+
if self._options.plugins:
196+
for plugin in self._options.plugins:
197+
if plugin["type"] == "local":
198+
cmd.extend(["--plugin-dir", plugin["path"]])
199+
else:
200+
raise ValueError(f"Unsupported plugin type: {plugin['type']}")
201+
194202
# Add extra args for future CLI flags
195203
for flag, value in self._options.extra_args.items():
196204
if value is None:

src/claude_agent_sdk/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,16 @@ class McpSdkServerConfig(TypedDict):
406406
)
407407

408408

409+
class SdkPluginConfig(TypedDict):
410+
"""SDK plugin configuration.
411+
412+
Currently only local plugins are supported via the 'local' type.
413+
"""
414+
415+
type: Literal["local"]
416+
path: str
417+
418+
409419
# Content block types
410420
@dataclass
411421
class TextBlock:
@@ -542,6 +552,8 @@ class ClaudeAgentOptions:
542552
agents: dict[str, AgentDefinition] | None = None
543553
# Setting sources to load (user, project, local)
544554
setting_sources: list[SettingSource] | None = None
555+
# Plugin configurations for custom plugins
556+
plugins: list[SdkPluginConfig] = field(default_factory=list)
545557

546558

547559
# SDK Control Protocol

0 commit comments

Comments
 (0)