{"meta":{"title":"Plugin directories","intro":"Use plugin directories to load skills, hooks, MCP servers, custom agents, and LSP settings from a single manifest.","product":"GitHub Copilot","breadcrumbs":[{"href":"/es/copilot","title":"GitHub Copilot"},{"href":"/es/copilot/how-tos","title":"Procedimientos"},{"href":"/es/copilot/how-tos/copilot-sdk","title":"SDK de Copilot"},{"href":"/es/copilot/how-tos/copilot-sdk/features","title":"Características"},{"href":"/es/copilot/how-tos/copilot-sdk/features/plugin-directories","title":"Plugin Directories"}],"documentType":"article"},"body":"# Plugin directories\n\nUse plugin directories to load skills, hooks, MCP servers, custom agents, and LSP settings from a single manifest.\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\nThis 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.\n\n## When to use plugin directories\n\nUse a plugin directory when you want to:\n\n* **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.\n* **Vendor capability packs into a repository** so every clone of the host application loads the same extensions deterministically.\n* **Develop a plugin locally** before publishing it to a marketplace.\n* **Override or extend** a marketplace-installed plugin with a local checkout for testing.\n\nIf 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.\n\n## Plugin folder layout\n\nThe Copilot CLI scans each plugin directory for a `plugin.json` manifest or a root-level `SKILL.md`. A minimal plugin looks like this:\n\n```text\nmy-plugin/\n├── plugin.json              # manifest (required unless using SKILL.md only)\n├── SKILL.md                 # optional: top-level skill\n├── hooks.json               # optional: hooks config\n├── .mcp.json                # optional: MCP server config\n├── agents/                  # optional: custom agents (one .md file per agent)\n│   └── code-reviewer.md\n└── skills/                  # optional: additional skills\n    └── lint-fix/\n        └── SKILL.md\n```\n\nThe 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.\n\nFor the full manifest schema, see the runtime documentation referenced from your CLI's `/plugin` slash command.\n\n## Loading a plugin directory from the SDK\n\nPlugin directories are loaded by passing `--plugin-dir <path>` 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.\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nimport { CopilotClient, RuntimeConnection } from \"@github/copilot-sdk\";\n\nasync function main() {\n  const client = new CopilotClient({\n    connection: RuntimeConnection.forStdio({\n      args: [\n        \"--plugin-dir\", \"./plugins/code-reviewer\",\n        \"--plugin-dir\", \"./plugins/lint-fix\",\n      ],\n    }),\n  });\n\n  await client.start();\n}\n\nmain();\n```\n\n```typescript\nimport { CopilotClient, RuntimeConnection } from \"@github/copilot-sdk\";\n\nconst client = new CopilotClient({\n  connection: RuntimeConnection.forStdio({\n    args: [\n      \"--plugin-dir\", \"./plugins/code-reviewer\",\n      \"--plugin-dir\", \"./plugins/lint-fix\",\n    ],\n  }),\n});\n\nawait client.start();\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n<!-- docs-validate: wrap-async -->\n\n```python\nfrom copilot import CopilotClient, StdioRuntimeConnection\n\nclient = CopilotClient(\n    connection=StdioRuntimeConnection(\n        args=(\n            \"--plugin-dir\", \"./plugins/code-reviewer\",\n            \"--plugin-dir\", \"./plugins/lint-fix\",\n        ),\n    ),\n)\nawait client.start()\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\npackage main\n\nimport (\n\t\"context\"\n\n\tcopilot \"github.com/github/copilot-sdk/go\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tclient := copilot.NewClient(&copilot.ClientOptions{\n\t\tConnection: copilot.StdioConnection{\n\t\t\tArgs: []string{\n\t\t\t\t\"--plugin-dir\", \"./plugins/code-reviewer\",\n\t\t\t\t\"--plugin-dir\", \"./plugins/lint-fix\",\n\t\t\t},\n\t\t},\n\t})\n\tif err := client.Start(ctx); err != nil {\n\t\treturn\n\t}\n}\n```\n\n```golang\nclient := copilot.NewClient(&copilot.ClientOptions{\n    Connection: copilot.StdioConnection{\n        Args: []string{\n            \"--plugin-dir\", \"./plugins/code-reviewer\",\n            \"--plugin-dir\", \"./plugins/lint-fix\",\n        },\n    },\n})\nif err := client.Start(ctx); err != nil {\n    return err\n}\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nusing GitHub.Copilot;\n\nawait using var client = new CopilotClient(new CopilotClientOptions\n{\n    Connection = RuntimeConnection.ForStdio(args: new[]\n    {\n        \"--plugin-dir\", \"./plugins/code-reviewer\",\n        \"--plugin-dir\", \"./plugins/lint-fix\",\n    }),\n});\n\nawait client.StartAsync();\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n```java\nimport com.github.copilot.CopilotClient;\nimport com.github.copilot.rpc.CopilotClientOptions;\n\npublic class PluginDirectoriesExample {\n    public static void main(String[] args) throws Exception {\n        var options = new CopilotClientOptions()\n            .setCliArgs(new String[] {\n                \"--plugin-dir\", \"./plugins/code-reviewer\",\n                \"--plugin-dir\", \"./plugins/lint-fix\",\n            });\n\n        var client = new CopilotClient(options);\n        client.start().get();\n    }\n}\n```\n\n```java\nvar options = new CopilotClientOptions()\n    .setCliArgs(new String[] {\n        \"--plugin-dir\", \"./plugins/code-reviewer\",\n        \"--plugin-dir\", \"./plugins/lint-fix\",\n    });\n\nvar client = new CopilotClient(options);\nclient.start().get();\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"rust\" data-label=\"Rust\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Rust</div>\n\n```rust\nuse github_copilot_sdk::{Client, ClientOptions};\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n    let _client = Client::start(\n        ClientOptions::new().with_extra_args([\n            \"--plugin-dir\", \"./plugins/code-reviewer\",\n            \"--plugin-dir\", \"./plugins/lint-fix\",\n        ]),\n    )\n    .await?;\n    Ok(())\n}\n```\n\n```rust\nuse github_copilot_sdk::{Client, ClientOptions};\n\nlet client = Client::start(\n    ClientOptions::new().with_extra_args([\n        \"--plugin-dir\", \"./plugins/code-reviewer\",\n        \"--plugin-dir\", \"./plugins/lint-fix\",\n    ]),\n)\n.await?;\n```\n\n</div>\n\n</div>\n\n> 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.\n\n## What a plugin can contribute\n\nLoading 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:\n\n| Plugin contributes                       | Visible to session as                                 |\n| ---------------------------------------- | ----------------------------------------------------- |\n| Skills (`SKILL.md`, `skills/*/SKILL.md`) | Items in `session.skills.list()`; injectable by name  |\n| Custom agents (`agents/*.md`)            | Dispatchable via the `task(agent_type=...)` tool      |\n| Hooks (`hooks.json`)                     | Fired alongside hooks registered via the SDK          |\n| MCP servers (`.mcp.json`)                | Tools and resources reachable through `session.mcp.*` |\n| LSP servers (`.lsp.json`)                | Initialized via `session.lsp.initialize(...)`         |\n\nPlugin agents are first-class sub-agents in [Fleet mode](/es/copilot/how-tos/copilot-sdk/features/fleet-mode): a parent agent can dispatch them by `agent_type`, and the runtime fires the `subagentStart` / `subagentStop` hooks for them like any other sub-agent.\n\n## Plugin-dir vs marketplace plugins\n\nThe runtime has two ways to install plugins, and both end up looking the same to a session:\n\n* **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.\n* **`--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.\n\nFor 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.\n\n## Making plugin sets deterministic\n\nWhen 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.\n\n<details open>\n<summary><strong>Node.js / TypeScript</strong></summary>\n\n<!-- docs-validate: hidden -->\n\n```typescript\nimport { CopilotClient, RuntimeConnection } from \"@github/copilot-sdk\";\n\nasync function main() {\n  process.env.COPILOT_PLUGIN_DIR_ONLY = \"true\";\n  const client = new CopilotClient({\n    connection: RuntimeConnection.forStdio({\n      args: [\"--plugin-dir\", \"./plugins/code-reviewer\"],\n    }),\n  });\n  await client.start();\n}\n\nmain();\n```\n\n<!-- /docs-validate: hidden -->\n\n```typescript\nprocess.env.COPILOT_PLUGIN_DIR_ONLY = \"true\";\n\nconst client = new CopilotClient({\n  connection: RuntimeConnection.forStdio({\n    args: [\"--plugin-dir\", \"./plugins/code-reviewer\"],\n  }),\n});\nawait client.start();\n```\n\n</details>\n\nUse 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.\n\n## Inspecting which plugins loaded\n\nOnce a session is created, list the active plugins to confirm a directory was picked up correctly:\n\n<details open>\n<summary><strong>Node.js / TypeScript</strong></summary>\n\n<!-- docs-validate: hidden -->\n\n```typescript\nimport { CopilotClient } from \"@github/copilot-sdk\";\n\nasync function main() {\n  const client = new CopilotClient();\n  await client.start();\n  const session = await client.createSession({\n    onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n  });\n\n  const plugins = await session.rpc.plugins.list();\n  for (const plugin of plugins.plugins) {\n    console.log(`${plugin.name} (${plugin.enabled ? \"enabled\" : \"disabled\"})`);\n  }\n}\n\nmain();\n```\n\n<!-- /docs-validate: hidden -->\n\n```typescript\nconst plugins = await session.rpc.plugins.list();\nfor (const plugin of plugins.plugins) {\n  console.log(`${plugin.name} (${plugin.enabled ? \"enabled\" : \"disabled\"})`);\n}\n```\n\n</details>\n\nPlugins 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.\n\n## Troubleshooting\n\n* **\"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`.\n* **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.\n* **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`.\n* **`--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.\n\n## Related\n\n* [Agentes personalizados y orquestación de subagentes](/es/copilot/how-tos/copilot-sdk/features/custom-agents): write agents that ship inside a plugin's `agents/` folder.\n* [Aptitudes personalizadas](/es/copilot/how-tos/copilot-sdk/features/skills): how `SKILL.md` files are loaded, and the skill-tier ordering rules.\n* [Trabajar con enlaces](/es/copilot/how-tos/copilot-sdk/features/hooks): hooks defined by a plugin fire alongside SDK-registered hooks.\n* [Using MCP servers with the GitHub Copilot SDK](/es/copilot/how-tos/copilot-sdk/features/mcp): plugin-provided MCP servers integrate the same way as inline registrations.\n* [Fleet mode](/es/copilot/how-tos/copilot-sdk/features/fleet-mode): plugin-provided agents are dispatchable as sub-agents."}