Plugins
Plugins provide a way to package and distribute multiple agent components together. A single plugin can include:
- Skills: Specialized knowledge and workflows
- Hooks: Event handlers for tool lifecycle
- MCP Config: External tool server configurations
- Agents: Specialized agent definitions
- Commands: Slash commands
The plugin format is compatible with the Claude Code plugin structure.
Plugin structure
A plugin follows this directory structure:
- plugin-name
- .plugin
- plugin.json
- skills
- skill-name
- SKILL.md
- skill-name
- hooks
- hooks.json
- agents
- agent-name.md
- commands
- command-name.md
- .mcp.json
- README.md
- .plugin
Note that the plugin metadata, i.e., plugin-name/.plugin/plugin.json, is required.
Plugin manifest
The manifest file plugin-name/.plugin/plugin.json defines plugin metadata:
{
"name": "code-quality",
"version": "1.0.0",
"description": "Code quality tools and workflows",
"author": "faheem-code",
"license": "MIT",
"repository": "https://github.com/example/code-quality-plugin"
}
Skills
Skills are defined in markdown files with YAML frontmatter:
---
name: python-linting
description: Instructions for linting Python code
trigger:
type: keyword
keywords:
- lint
- linting
- code quality
---
# Python Linting Skill
Run ruff to check for issues:
\`\`\`bash
ruff check .
\`\`\`
Hooks
Hooks are defined in hooks/hooks.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "file_editor",
"hooks": [
{
"type": "command",
"command": "echo 'File edited: $FAHEEMCODE_TOOL_NAME'",
"timeout": 5
}
]
}
]
}
}
MCP configuration
MCP servers are configured in .mcp.json:
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
Using plugin components
The ready-to-run example is available here.
Brief explanation on how to use a plugin with an agent.
Loading a Plugin
First, load the desired plugins.
from faheemcode.sdk.plugin import Plugin
# Load a single plugin
plugin = Plugin.load("/path/to/plugin")
# Load all plugins from a directory
plugins = Plugin.load_all("/path/to/plugins")
Accessing Components
You can access the different plugin components to see which ones are available.
# Skills
for skill in plugin.skills:
print(f"Skill: {skill.name}")
# Hooks configuration
if plugin.hooks:
print(f"Hooks configured: {plugin.hooks}")
# MCP servers
if plugin.mcp_config:
servers = plugin.mcp_config.get("mcpServers", {})
print(f"MCP servers: {list(servers.keys())}")
Using with an Agent
You can now feed your agent with your preferred plugin.
# Create agent context with plugin skills
agent_context = AgentContext(
skills=plugin.skills,
)
# Create agent with plugin MCP config
agent = Agent(
llm=llm,
tools=tools,
mcp_config=plugin.mcp_config or {},
agent_context=agent_context,
)
# Create conversation with plugin hooks
conversation = Conversation(
agent=agent,
hook_config=plugin.hooks,
)
Ready-to-run example
The example below demonstrates plugin loading via Conversation and plugin management utilities (install, list, load, enable, disable, and uninstall).
"""Example: Loading and Managing Plugins
This 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 ones
4. Inspecting the `.installed.json` metadata file and `enabled` flag
5. Disabling and re-enabling a plugin without reinstalling it
6. Uninstalling plugins from persistent storage
Plugins 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 monorepos
For full documentation, see: https://docs.faheemcode.ai/sdk/guides/plugins
"""
import json
import os
import tempfile
from pathlib import Path
from pydantic import SecretStr
from faheemcode.sdk import LLM, Agent, Conversation
from faheemcode.sdk.plugin import (
PluginFetchError,
PluginSource,
disable_plugin,
enable_plugin,
install_plugin,
list_installed_plugins,
load_installed_plugins,
uninstall_plugin,
)
from faheemcode.sdk.tool import Tool
from faheemcode.tools.file_editor import FileEditorTool
from faheemcode.tools.terminal import TerminalTool
script_dir = Path(__file__).parent
local_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.name
def 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.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/05_skills_and_plugins/02_loading_plugins/main.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/05_skills_and_plugins/02_loading_plugins/main.py
Registered marketplace plugins
Registered marketplaces let an agent context name one or more plugin catalogs once
and then load plugins by marketplace-qualified names like
incident-bot@specialists. Use auto_load="all" when every plugin in a
marketplace should load at conversation startup, and call
conversation.load_plugin() when you want to add a specific plugin later.
The example below builds local marketplace catalogs so it can run without network access or credentials.
"""Example: Registered Marketplaces and Runtime Plugin Loading
This example demonstrates the registered marketplace flow:
1. Register multiple marketplace catalogs on AgentContext.
2. Auto-load plugins from a marketplace with ``auto_load='all'``.
3. Load an additional plugin at runtime by marketplace-qualified name.
The example builds two temporary local marketplaces so it can run without network
access or external credentials.
"""
import json
import tempfile
from pathlib import Path
from faheemcode.sdk import Agent, AgentContext, Conversation
from faheemcode.sdk.marketplace import MarketplaceRegistration
from faheemcode.sdk.testing import TestLLM
def write_plugin(plugin_dir: Path, plugin_name: str, skill_name: str) -> None:
manifest_dir = plugin_dir / ".plugin"
manifest_dir.mkdir(parents=True, exist_ok=True)
(manifest_dir / "plugin.json").write_text(
json.dumps(
{
"name": plugin_name,
"version": "1.0.0",
"description": f"Example plugin {plugin_name}",
}
)
)
skills_dir = plugin_dir / "skills"
skills_dir.mkdir()
(skills_dir / f"{skill_name}.md").write_text(
f"---\nname: {skill_name}\ndescription: Example skill\n---\n"
f"Use {skill_name} when demonstrating registered marketplace plugins."
)
def write_marketplace(marketplace_dir: Path, plugin_name: str, skill_name: str) -> None:
write_plugin(marketplace_dir / "plugins" / plugin_name, plugin_name, skill_name)
manifest_dir = marketplace_dir / ".plugin"
manifest_dir.mkdir(parents=True, exist_ok=True)
(manifest_dir / "marketplace.json").write_text(
json.dumps(
{
"name": marketplace_dir.name,
"owner": {"name": "Example Team"},
"plugins": [
{
"name": plugin_name,
"source": f"./plugins/{plugin_name}",
"description": f"Example marketplace plugin {plugin_name}",
}
],
}
)
)
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
team_marketplace = tmp_path / "team-marketplace"
specialists_marketplace = tmp_path / "specialists-marketplace"
write_marketplace(team_marketplace, "review-bot", "review-checklist")
write_marketplace(specialists_marketplace, "incident-bot", "incident-brief")
agent = Agent(
llm=TestLLM.from_messages([]),
tools=[],
agent_context=AgentContext(
registered_marketplaces=[
MarketplaceRegistration(
name="team",
source=str(team_marketplace),
auto_load="all",
),
MarketplaceRegistration(
name="specialists",
source=str(specialists_marketplace),
),
]
),
)
conversation = Conversation(
agent=agent,
workspace=str(tmp_path / "workspace"),
)
conversation.load_plugin("incident-bot@specialists")
agent_context = conversation.agent.agent_context
assert agent_context is not None
skill_names = sorted(skill.name for skill in agent_context.skills or [])
resolved_sources = [plugin.source for plugin in conversation.resolved_plugins or []]
print("Registered marketplaces:")
for registration in agent_context.registered_marketplaces:
print(f" - {registration.name}: auto_load={registration.auto_load}")
print("Loaded skills:")
for skill_name in skill_names:
print(f" - {skill_name}")
print("Resolved plugins:")
for source in resolved_sources:
print(f" - {source}")
assert skill_names == ["incident-brief", "review-checklist"]
assert any(
source.endswith("team-marketplace/plugins/review-bot")
for source in resolved_sources
)
assert any(
source.endswith("specialists-marketplace/plugins/incident-bot")
for source in resolved_sources
)
print("EXAMPLE_COST: 0")
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/05_skills_and_plugins/05_registered_marketplace_plugins/main.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/05_skills_and_plugins/05_registered_marketplace_plugins/main.py
Installing plugins to persistent storage
The SDK provides utilities to install plugins to a local directory
(~/.faheem-code/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 faheemcode.sdk.plugin, which gives the
CLI a clean SDK surface for /plugin install, /plugin enable,
/plugin disable, and /plugin uninstall.
Installed plugin lifecycle
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:
from faheemcode.sdk.plugin import (
disable_plugin,
enable_plugin,
install_plugin,
list_installed_plugins,
load_installed_plugins,
uninstall_plugin,
)
info = install_plugin(source="/path/to/plugin")
tracked_plugins = list_installed_plugins()
disable_plugin(info.name)
enabled_plugins = load_installed_plugins()
enable_plugin(info.name)
uninstall_plugin(info.name)
Next steps
- Skills - Learn more about skills and triggers
- Hooks - Understand hook event types
- MCP Integration - Configure external tool servers