Plugins

Import agent plugins

Install a supported agent-plugin format and understand its conversion.

For: plugin authors.

Install a Claude Code plugin or an Agent Plugin without converting its files first. The edge reads either supported format and builds an Orchestrator Zero manifest from it:

  • a Claude Code plugin: an optional .claude-plugin/plugin.json and the default folders skills/, commands/, agents/, hooks/hooks.json and .mcp.json;
  • an Agent Plugin 1.0: a plugin.json at the root, skills/ and mcp.json (without a dot).

They install from every source a plugin can come from: a Git repository, a folder, an archive, an upload, an OCI registry or the workspace. plugin lint reads them too, so you can see what carries over before you install.

A Claude Code subagent and an Orchestrator Zero Markdown agent have the same shape, front matter and a body. apiVersion: oz0/v1 in the front matter tells them apart: a file that says it is an Orchestrator Zero agent, with its fields and its own runtime; one that does not, under agents/, imports as a subagent on the Claude harness. A Markdown file with apiVersion: oz0/v1 at a folder's root, or a SKILL.md there without the folders below, is a plugin of that one part (A plugin that is one part), not a Claude Code plugin.

On a laptop a plugin runs on one machine. On the fleet it runs on every node of a tenant, so the rules are stricter: every install is locked to a commit or a digest and never updates by itself, an admin sees every hook and MCP command before it installs, and imported prompts are marked untrusted.

What becomes what

In the pluginOn Orchestrator Zero
skills/<name>/SKILL.mda skill, as it is
commands/<name>.mda skill named after the command
agents/<name>.mdan agent on the Claude harness (harness:claude-agent-sdk), its body the instructions
model: sonnet in an agentthe tenant's model alias sonnet
tools: of an agentthe tools of its harness session (no tools:: Read, Write, Edit, Glob, Grep, Bash)
maxTurns:limits.max_steps
a stdio server npx -y [email protected]a tool with the package npm:[email protected]
a stdio server uvx pkg==1.2.3a tool with the package pypi:pkg==1.2.3
uv run --directory ${CLAUDE_PLUGIN_ROOT} …a tool of the plugin's own Python project
"env": {"TOKEN": "${GITHUB_TOKEN}"}the declared secret TOKEN
an HTTP servera remote tool
"headers": {"Authorization": "Bearer ${DOCS_TOKEN}"} of an HTTP serverthe header, its value from the declared secret DOCS_TOKEN
a PreToolUse, PostToolUse, UserPromptSubmit, Stop or SessionEnd command hooka hook answered by the node's hook runner

A server without a version, such as npx -y @modelcontextprotocol/server-filesystem, is pinned once at install to the newest version, and stays on it until you install again. You need the Claude harness plugin for the agents, and an alias of each model name your agents use:

Terminal
oz0 model alias set sonnet anthropic/claude-sonnet-5-5

What does not carry over

The loader reports each of these as a warning that starts with does not carry over, and leaves the part out:

  • interactive prompts: Claude Code asks a user for userConfig values, nobody is asked on the fleet. A value a server's environment reads becomes a tenant secret instead;
  • bin/, which Claude Code puts on the Bash tool's PATH: harness sessions start in a job's own folder without the plugin's files;
  • agents whose name has a : (Claude Code names an agent in a subfolder folder:name), since jobs find agents by name;
  • servers that use ${CLAUDE_PLUGIN_DATA}, run a container, speak SSE or WebSocket, or take variables in their arguments;
  • http, prompt, agent and mcp_tool hooks, and events other than the five above;
  • output styles, LSP servers, monitors, themes, workflows and channels;
  • the shell lines of a skill or a command (!`git log`, or a code block that opens with ```!): Orchestrator Zero runs no shell in skills, so the line stays text;
  • skills past the 128 a plugin keeps, past 1 MiB of skill bodies, or with the name of a skill or command already kept (Skills). A command with a skill's name is left out without a warning: in Claude Code too, the skill wins.

Header helpers (headersHelper) and OAuth configurations of HTTP servers are errors: they would run a command on the edge or need a person at a browser. A remote server without secret headers that answers with 401 or 403 at install is refused for the same reason. Leave such a server out with an overlay's skip:.

A header whose value names a variable, such as "Authorization": "Bearer ${DOCS_TOKEN}", is sent with every request to the server, its value filled on the node from the tenant secret of that name, which the plugin then declares (oz0 secret set DOCS_TOKEN). A credential written out in the plugin's files (an Authorization, Cookie or *-Key header without a variable) is not sent: give it in the overlay instead, with tools.<id>.headers: {X-Api-Key: "${DOCS_KEY}"} and the secret in requires.secrets.

The inventory lists each header as it is sent, with the secret named and never its value, so a changed header asks for a new acceptance. An OZ0 plugin's remote tools take headers: the same way.

Install one

Terminal
oz0 plugin install https://github.com/acme/review-kit

The first install is refused with the plugin's inventory: every MCP server and hook it runs on your nodes, with the commands, the environment and the secrets each gets, and the digest of each of the plugin's own files a command names (a hook's script, a server's code).

review-kit 1.3.0 is a claude-code plugin (read as [email protected]). It runs these on every node of the tenant:
  mcp     greet                    python -m greet_server
          an MCP server of the plugin's own files
  mcp     github                   npm:@modelcontextprotocol/[email protected]  [secrets: GITHUB_PERSONAL_ACCESS_TOKEN]
          an MCP server from npm:@modelcontextprotocol/[email protected], installed on each node
  hook    PreToolUse mcp__greet__hello ${CLAUDE_PLUGIN_ROOT}/scripts/check-greeting.sh
          answers pre_tool_call of the plugin's tools greet.hello
          runs scripts/check-greeting.sh sha256:2108079fb358…
Inventory digest: sha256:92945a5edf15…

Check what runs, then install it with: --accept-inventory sha256:92945a5edf15… --ref 4f0c2a91…

Read it, then install with its digest and the commit it was shown for, so a branch that moves in between installs nothing you did not see:

Terminal
oz0 plugin install https://github.com/acme/review-kit --accept-inventory sha256:92945a5edf15… --ref 4f0c2a91…

plugin preview <source> shows the same without trying an install. A later version with the same inventory installs without a new acceptance; one that adds, removes or changes a command, a server's environment or a script a command names is refused again with its new inventory, so a hook cannot change quietly. Every hook runs with the hook runner's environment, which an overlay cannot set (tools.<plugin>-hooks.env is an error), and a server binary of any size in a bundle or OCI artifact is covered by its digest.

When the plugin has a hook, or any server that runs on a node (a pinned package's included: every such process starts in the plugin's folder, where its interpreter and the environment the plugin sets, such as NODE_OPTIONS or PYTHONPATH, can load the plugin's files), the inventory also lists the plugin's files, a digest over every file of the plugin: a script a hook reaches after cd, or a module a server imports or is told to load, cannot change without a new acceptance either.

Only a plugin whose servers are all remote has no such item, so a changed README asks for nothing there. No file of a plugin is in a .git folder: a bundle, an OCI artifact or a marketplace archive that carries one is refused, and so is the artifact on a node. An install with --ref <commit> stays on that commit; to move to a newer one, install again from the branch or tag, and the refusal, if its inventory changed, names the new commit.

In the console, Plugins, Install shows the inventory and its Accept the inventory and install button.

Every part of the inventory is the plugin's own text, so the CLI and the console show it with each control or invisible character written as an escape, such as \u{001B} or \u{202E}: a carriage return or a terminal sequence in a hook's command cannot write over what comes before it, and a bidi or zero-width character cannot hide part of a command. What you see is every character that will run.

The plugin's description and its agents', commands' and skills' descriptions are kept the same way at the import, so the catalog, the console and the MCP and A2A listings show the text a model reads.

A tool hook's matcher holds at most 512 bytes (Claude Code's are a few tool names or a short expression): a longer one is refused at the import, and so is a manifest whose hook runner's config holds one. The node compiles each hook's matcher once, when its hook runner first answers, never at each call. It reads a matcher as an RE2 expression, Go's syntax. Claude Code reads it as a JavaScript one, which may look ahead or behind or refer back (mcp__srv__(?!read).*).

Such a hook would never run here, so the import leaves it out, and the inventory's "does not carry over" list names it: write the matcher without them.

plugin list and the catalog mark imported plugins, agents and skills with their format: their prompts come from outside and are treated as untrusted.

Hooks and MCP servers in harness sessions

Claude Code runs a plugin's hooks and MCP servers in its own sessions. On the fleet they stay out of harness sessions unless the install opts in:

Default scope

Without the opt-in, tool hooks see only calls to the plugin's own MCP tools. Job hooks see only jobs of the plugin's agents.

Scope with --harness-extras

With --harness-extras, a hook matching Claude's own tools (Bash, Write|Edit, *) also sees calls from every harness session. The plugin's agent sessions run its MCP servers themselves.

A session call named mcp__<tool>__<function> is treated as a call to the plugin function <tool>.<function>. It passes the tenant's tool policy, including refusals, conditions, the taint-default floor and origin checks, plus hooks and approvals.

Functions offered to the session

The session receives only functions in the node's listing and approved snapshot. A function awaiting approval or stopped by a kill switch is not called.

Server resources, prompts and instructions are not exposed. Import leaves out ListMcpResourcesTool and ReadMcpResourceTool, reports their omission, and refuses them in the session.

Names containing characters other than letters, digits, _ or - are rewritten to the session form. For example, issues.delete becomes mcp__<tool>__issues_delete. If two functions of one server become identical, neither is offered. A name outside the session's server functions is refused.

Refusals and tool-name clashes

These calls are refused under a clearance or wall because their results enter the session without labels.

A server whose tool ID is also provided by another tenant plugin runs in no session, just as it runs on no node. A job that would start that server fails at startup with ToolClash; see tool IDs.

Accepting or removing the opt-in

The opt-in is part of what you accept: its inventory has its own digest, and installing the same commit again without it reaches the nodes at once (their runtimes reload, and a running session's next call of such a server is refused). Only the install opts in; a plugin's own overlay cannot (harness: {mcp_servers: …} in it is an error). Know what you accept: servers that run inside a session are processes of that session, so the session's shell can read their environments, the plugin's secrets included.

A session of a job an outside event started (a webhook, a routine), whose agent can run a shell, therefore gets none of the plugin's servers that hold secrets, as such sessions hold no write token (sandboxes).

Terminal
oz0 plugin preview https://github.com/acme/review-kit --harness-extras
oz0 plugin install https://github.com/acme/review-kit --harness-extras --accept-inventory sha256:…

Servers that hold secrets, in sessions of outside jobs

With --guarded-servers as well, such a session gets the plugin's servers that hold secrets too, but not as its own processes. Each one runs as the job's own tool server outside the session, in a sandbox of its own that receives the plugin's secrets from the node's runtime when it starts (the host API), never through an environment or a file the session can reach. The session reaches it through the runtime, which makes a call only after the job allowed that same call: a request the session's shell sends to the server on its own, without asking, is refused.

The tools' snapshots and kill switches hold as for any call of the plugin's tools.

Terminal
oz0 plugin install https://github.com/acme/review-kit --harness-extras --guarded-servers --accept-inventory sha256:…

The opt-in needs --harness-extras, adds an item to the inventory (so it has a digest of its own), and is kept by plugin update. It works on nodes with sandbox tier 1, which are the only ones that run such sessions.

The node's hook runner (orchestrator-zero-node hook-runner) runs each hook command in the plugin's folder with the event on stdin, as Claude Code does. ${CLAUDE_PLUGIN_DATA} is a new empty private folder for each run, removed after it: nodes keep no data for a plugin between runs. Exit code 2, permissionDecision: "deny" or decision: "block" denies the call with the hook's reason; ask asks a person; updatedInput changes the arguments. A hook may run 10 seconds unless it says otherwise, at most 30, and a hook that does not answer in time denies.

The hook runner's functions declare themselves not destructive and open-world, so the tool policy's defaults let a hook answer at once; a tenant that refuses open-world functions refuses these hooks too, and each call then gets the hook's answer for a failure (deny).

Previous section links
Copyright © 2026