# Plugin directories A **plugin** is a directory that bundles SDK extensions — skills, hooks, MCP servers, custom agents, and LSP configuration — behind a single manifest. Pointing the SDK at a plugin directory loads everything the plugin contributes, so you can ship reusable capability packs without writing per-extension wiring in every host application. This guide explains the plugin folder layout, how to load a plugin from a directory, when to use plugin directories vs. registering individual extensions, and how to make plugin sets deterministic. ## When to use plugin directories Use a plugin directory when you want to: * **Distribute a bundle of capabilities** as one unit — e.g., a "TypeScript reviewer" pack with a skill, a `preToolUse` hook that enforces lint, and a custom agent that runs the reviewer. * **Vendor capability packs into a repository** so every clone of the host application loads the same extensions deterministically. * **Develop a plugin locally** before publishing it to a marketplace. * **Override or extend** a marketplace-installed plugin with a local checkout for testing. If you only need to add a single MCP server, a single hook, or a single custom agent, you can register it inline via the SDK config (`mcpServers`, `hooks`, `customAgents`). Plugin directories are most useful once you have three or more related extensions that ship together. ## Plugin folder layout The Copilot CLI scans each plugin directory for a `plugin.json` manifest or a root-level `SKILL.md`. A minimal plugin looks like this: ``` my-plugin/ ├── plugin.json # manifest (required unless using SKILL.md only) ├── SKILL.md # optional: top-level skill ├── hooks.json # optional: hooks config ├── .mcp.json # optional: MCP server config ├── agents/ # optional: custom agents (one .md file per agent) │ └── code-reviewer.md └── skills/ # optional: additional skills └── lint-fix/ └── SKILL.md ``` The manifest may also live at `.github/plugin.json` or `.github/plugin/plugin.json` so plugins can sit inside an existing repository without changing its root layout. Each subsystem (hooks, MCP, LSP, skills, agents) has its own loader and is optional — a plugin only needs the parts it contributes. For the full manifest schema, see the runtime documentation referenced from your CLI's `/plugin` slash command. ## Loading a plugin directory from the SDK Plugin directories are loaded by passing `--plugin-dir ` to the Copilot CLI when the SDK spawns it. Each language exposes this through the runtime connection's extra-args option. The flag can be repeated to load multiple plugins.
Node.js / TypeScript ```typescript import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ args: [ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", ], }), }); await client.start(); } main(); ``` ```typescript import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ args: [ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", ], }), }); await client.start(); ```
Python ```python from copilot import CopilotClient, StdioRuntimeConnection client = CopilotClient( connection=StdioRuntimeConnection( args=( "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", ), ), ) await client.start() ```
Go ```go package main import ( "context" copilot "github.com/github/copilot-sdk/go" ) func main() { ctx := context.Background() client := copilot.NewClient(&copilot.ClientOptions{ Connection: copilot.StdioConnection{ Args: []string{ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", }, }, }) if err := client.Start(ctx); err != nil { return } } ``` ```go client := copilot.NewClient(&copilot.ClientOptions{ Connection: copilot.StdioConnection{ Args: []string{ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", }, }, }) if err := client.Start(ctx); err != nil { return err } ```
.NET ```csharp using GitHub.Copilot; await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(args: new[] { "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", }), }); await client.StartAsync(); ```
Java ```java import com.github.copilot.CopilotClient; import com.github.copilot.rpc.CopilotClientOptions; public class PluginDirectoriesExample { public static void main(String[] args) throws Exception { var options = new CopilotClientOptions() .setCliArgs(new String[] { "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", }); var client = new CopilotClient(options); client.start().get(); } } ``` ```java var options = new CopilotClientOptions() .setCliArgs(new String[] { "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", }); var client = new CopilotClient(options); client.start().get(); ```
Rust ```rust use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), Box> { let _client = Client::start( ClientOptions::new().with_extra_args([ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", ]), ) .await?; Ok(()) } ``` ```rust use github_copilot_sdk::{Client, ClientOptions}; let client = Client::start( ClientOptions::new().with_extra_args([ "--plugin-dir", "./plugins/code-reviewer", "--plugin-dir", "./plugins/lint-fix", ]), ) .await?; ```
> The example above uses an stdio runtime connection — the default when the SDK bundles the CLI. If you connect to an external runtime via a URL (`forUri` / `ForUri`), pass `--plugin-dir` to the long-running CLI server when you start it; the SDK does not forward `--plugin-dir` to runtimes it didn't spawn. ## Per-session plugin directories `--plugin-dir` is a launch argument, so it fixes one plugin set for the CLI process and every session created against it. When sessions need different plugin sets, or when the SDK is connected to a runtime it did not spawn, pass the directories on the session config instead. They travel in the `session.create` and `session.resume` payloads over JSON-RPC rather than as process arguments, so they reach an external runtime the same way the startup option does. ```typescript import { CopilotClient } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient(); await client.start(); const session = await client.createSession({ pluginDirectories: ["./plugins/code-reviewer"], }); } main(); ``` ```typescript import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); await client.start(); const session = await client.createSession({ pluginDirectories: ["./plugins/code-reviewer"], }); ``` Relative paths resolve against `workingDirectory`, or the runtime working directory when that is unset, so absolute paths are recommended. Entries that do not resolve are logged and skipped rather than failing session creation. The option is an explicit opt-in, which means plugin agents and rules load even when `enableConfigDiscovery` is false. Assets loaded this way sit between project sources and personal or home sources in the session-wide precedence order. The equivalent option in each SDK is: | SDK | Session option | |---|---| | Node.js / TypeScript | `pluginDirectories: string[]` | | Python | `plugin_directories=[...]` | | Go | `PluginDirectories: []string{...}` | | .NET | `PluginDirectories = [...]` | | Java | `.setPluginDirectories(List.of(...))` | | Rust | `.with_plugin_directories([...])` | ## Trusted host-bundled plugin directories Applications that ship their own trusted plugins can register them as a client startup option. The SDK sends the complete ordered set after connecting and verifying the protocol, before `start` returns or any session can be created. Paths must be absolute; leaving the option unset or empty makes no RPC call. ```typescript import { CopilotClient } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ builtinPluginDirectories: [ "/opt/my-app/copilot-plugins/core", "/opt/my-app/copilot-plugins/github", ], }); await client.start(); } main(); ``` The equivalent option in each SDK is: | SDK | Startup option | |---|---| | Node.js / TypeScript | `builtinPluginDirectories: string[]` | | Python | `builtin_plugin_directories=[...]` | | Go | `BuiltinPluginDirectories: []string{...}` | | .NET | `BuiltinPluginDirectories = [...]` | | Java | `.setBuiltinPluginDirectories(List.of(Path.of(...)))` | | Rust | `.with_builtin_plugin_directories([...])` | This is a trust boundary for plugins bundled and controlled by the host application. It is distinct from `--plugin-dir`, which is a CLI process launch argument for explicitly loading ordinary plugin directories. The startup option also works when connecting to an existing runtime because it is sent over JSON-RPC rather than forwarded as a process argument. ## What a plugin can contribute Loading a plugin directory makes its extensions visible to every session created by the client. The runtime merges plugin-provided extensions with anything you register inline: | Plugin contributes | Visible to session as | |---|---| | Skills (`SKILL.md`, `skills/*/SKILL.md`) | Items in `session.skills.list()`; injectable by name | | Custom agents (`agents/*.md`) | Dispatchable via the `task(agent_type=...)` tool | | Hooks (`hooks.json`) | Fired alongside hooks registered via the SDK | | MCP servers (`.mcp.json`) | Tools and resources reachable through `session.mcp.*` | | LSP servers (`.lsp.json`) | Initialized via `session.lsp.initialize(...)` | Plugin agents are first-class sub-agents in [fleet mode](./fleet-mode.md): a parent agent can dispatch them by `agent_type`, and the runtime fires the `subagentStart` / `subagentStop` hooks for them like any other sub-agent. ## Plugin-dir vs marketplace plugins The runtime has two ways to install plugins, and both end up looking the same to a session: * **Marketplace / direct-repo plugins** are installed persistently through the CLI's `/plugin` slash command or the underlying `installedPlugins` user setting. They are *ambient* — every session that runs against the same user config sees them, and they participate in plugin discovery rules. * **`--plugin-dir` plugins** are *explicit and ephemeral* — they only apply to the CLI process you launched with that flag. They take precedence over ambient discovery and are de-duplicated against marketplace entries with the same cache path, so the same plugin won't load twice when both surfaces reference it. For SDK-driven applications, `--plugin-dir` is usually the right choice: it keeps the plugin set under your application's control instead of depending on per-machine user state. ## Making plugin sets deterministic When the host machine may have other plugins installed (marketplace or personal), set `COPILOT_PLUGIN_DIR_ONLY=true` in the runtime's environment to suppress automatic plugin discovery. Only the directories you pass via `--plugin-dir` will load.
Node.js / TypeScript ```typescript import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { process.env.COPILOT_PLUGIN_DIR_ONLY = "true"; const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ args: ["--plugin-dir", "./plugins/code-reviewer"], }), }); await client.start(); } main(); ``` ```typescript process.env.COPILOT_PLUGIN_DIR_ONLY = "true"; const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ args: ["--plugin-dir", "./plugins/code-reviewer"], }), }); await client.start(); ```
Use this in CI, in headless server deployments, and anywhere you want a reproducible plugin set that doesn't depend on the host's user configuration. ## Inspecting which plugins loaded Once a session is created, list the active plugins to confirm a directory was picked up correctly:
Node.js / TypeScript ```typescript import { CopilotClient } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient(); await client.start(); const session = await client.createSession({ onPermissionRequest: async () => ({ kind: "approve-once" }), }); const plugins = await session.rpc.plugins.list(); for (const plugin of plugins.plugins) { console.log(`${plugin.name} (${plugin.enabled ? "enabled" : "disabled"})`); } } main(); ``` ```typescript const plugins = await session.rpc.plugins.list(); for (const plugin of plugins.plugins) { console.log(`${plugin.name} (${plugin.enabled ? "enabled" : "disabled"})`); } ```
Plugins loaded via `--plugin-dir` appear in this list with their cache path set to the directory you provided. Marketplace installs are tagged with their registry source. ## Troubleshooting * **"no plugin.json or SKILL.md found in <dir>"** — the directory exists but doesn't qualify as a plugin. Add a `plugin.json` manifest at the root (or under `.github/`), or include a top-level `SKILL.md`. * **Plugin loaded but agents/skills not visible** — make sure the plugin manifest declares the agents/skills it contributes, or use the implicit layout (`agents/*.md`, `skills/*/SKILL.md`). Then call `session.rpc.skills.reload()` to pick up changes without restarting. * **Duplicate hooks firing** — the runtime de-duplicates by `cache_path`, but only when the same directory is referenced both as a marketplace install and a `--plugin-dir`. If two different directories contain the same plugin, both will load. Remove one or use `COPILOT_PLUGIN_DIR_ONLY=true`. * **`--plugin-dir` ignored when connecting to an external runtime** — the SDK only forwards extra args when it spawns the CLI itself. For external runtimes (`forUri`/`ForUri`), pass `--plugin-dir` on the command line that starts the runtime server. ## Related * [Custom Agents](./custom-agents.md): write agents that ship inside a plugin's `agents/` folder. * [Skills](./skills.md): how `SKILL.md` files are loaded, and the skill-tier ordering rules. * [Hooks](./hooks.md): hooks defined by a plugin fire alongside SDK-registered hooks. * [MCP Servers](./mcp.md): plugin-provided MCP servers integrate the same way as inline registrations. * [Fleet Mode](./fleet-mode.md): plugin-provided agents are dispatchable as sub-agents.