Skip to main content
Agent Plugins is a portable packaging format for agent extensions. A package published in this format carries its skills and MCP server configuration in fixed, client-neutral locations, so the same directory loads in OpenHands and in any other compatible client without being rewritten. OpenHands supports version 1.0.0 of the specification. This page covers what the format looks like, which parts of a package OpenHands honours, and how a package behaves when something in it is invalid.
Agent Plugins support ships in openhands-sdk 1.50.0 and later. Earlier versions load a package’s root plugin.json with the OpenHands plugin format instead, which expects its manifest at .plugin/plugin.json and therefore names the plugin after its directory.

How OpenHands Picks the Format

OpenHands supports two plugin layouts and chooses between them by looking for a manifest at the package root: A root plugin.json takes precedence. Its presence alone decides the format: a package whose manifest is present but invalid is rejected as an Agent Plugins package rather than falling back to the other layout.

Package Layout

Everything except plugin.json is optional, and a missing directory is a valid absence rather than an error. Every path a package refers to must stay inside the package directory; paths that resolve outside it are refused.

Manifest

plugin.json sits at the package root and must declare both $schema and name:
The $schema value must be exactly the 1.0.0 plugin schema identifier above. OpenHands validates the manifest against a vendored copy of that schema and never fetches it over the network while loading. Unknown top-level fields are reported in the logs and ignored. Any other manifest violation — unparseable JSON, a missing or unrecognized $schema, a missing name, a field of the wrong type — stops the package from loading before component discovery begins.

Supported Components

Component types that the specification does not define, and extension namespaces belonging to other clients, are ignored without inspecting their contents.

Skills

Each subdirectory of skills/ holds one skill, defined by a SKILL.md file:
Discovery is not recursive: skills/<name>/SKILL.md defines a skill, while deeper Markdown files are that skill’s own resources. A package with no skills/ directory but a SKILL.md at its root loads as a single-skill package. See Skills for how to write one.

MCP Servers

MCP servers are declared in mcp.json at the package root — no leading dot, unlike the OpenHands format’s .mcp.json:
OpenHands supports the stdio and streamable-http transports. An sse entry is reported and skipped: the transport is optional in the specification and deprecated by MCP itself, so OpenHands declines it rather than silently substituting another transport. Each entry is validated on its own. One invalid or unsupported entry is skipped while its siblings still load, and a package whose mcp.json is invalid as a whole loads with no MCP servers instead of failing entirely.

Placeholders

Two placeholders are available: They are expanded only in args, in env values, and in cwd. Expansion is a single pass, so text introduced by one replacement is never expanded again, and any other ${...} text is passed through literally. ${PLUGIN_DATA} is keyed to the package’s location, so it survives updates and is never shared with a same-named package elsewhere. For stdio servers, OpenHands also sets PLUGIN_ROOT and PLUGIN_DATA in the server’s environment. A package cannot override either one.

stdio Servers

command is a single executable token, never a shell string. It is either a bare executable name resolved through the platform’s executable search, or a package-relative path beginning with ./. Absolute paths, ../ and bare relative paths are refused. cwd defaults to the package root. An explicit cwd must stay inside the package root, or inside ${PLUGIN_DATA} when written relative to it.

streamable-http Servers

url must be an absolute http(s) URL with no user information and no fragment. https is required unless the host is a loopback address. Header values are sent literally, with no placeholder expansion. Headers that OpenHands generates itself to implement HTTP and MCP — such as Host, Content-Length and Mcp-Session-Id — are dropped with a warning if a package configures them.

OpenHands Extensions

Commands, agents, hooks and an entry command are not part of the portable core, so OpenHands reads them from its own extension namespace, dev.openhands. File-based components live in a top-level directory of that name:
The layout inside dev.openhands/ is the same as in the OpenHands plugin format, so porting a package is a matter of moving those directories rather than rewriting them. An entry command is declared in the manifest under the same namespace:
Other clients ignore this member, and OpenHands ignores theirs. An unusable value inside dev.openhands is reported and ignored rather than failing the package. What these components do is unchanged by the format: see Plugin components for commands and agents, and Hooks for the hook contract.

Installing a Package

Agent Plugins packages install exactly like any other OpenHands plugin — from a local directory or a repository, through the same settings and commands. The format changes only how a package is laid out, not how it is installed.

Install a plugin

Installation paths for every OpenHands surface

Agent Canvas

Browse, install and attach plugins in the UI

When Something Is Invalid

Failures are contained as narrowly as the specification allows, so a partly broken package still contributes what is valid: Loading decisions are logged, so the log is the place to look when a component you expect is missing.

Further Reading

Agent Plugins specification

The portable format this page implements

OpenHands plugin format

The .plugin/ layout and its components

Skills

Writing and distributing skills

SDK plugins guide

Loading plugins programmatically