Creating a plugin
The canonical package layout — skills, MCP, agents, hooks — and how to test and publish one
Creating a plugin
uze is Plugin First, Capability Aware (ADR-013): the package you author is the distribution unit, and each capability inside it is the compatibility unit. You write one canonical layout; uze delivers it through each harness's own native surface — see Standards, not another format for why the layout looks the way it does.
The canonical layout
Only plugin.json is required; everything else is optional — ship what your plugin needs. uze's
Store preserves every byte verbatim, and nothing here is re-serialized into an internal schema.
plugin.json
The Agent Plugins 1.0.0 schema, minimal:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "flow",
"description": "What this plugin does, in one sentence."
}name is the only required field beyond the schema. This is the same shape as this repository's
own official plugin (plugins/uze/plugin.json) — a real manifest, not a hypothetical one.
Skills
A Skill is the canonical capability — skills/<name>/SKILL.md, Markdown with frontmatter:
---
name: review
description: Review the current changes
invoke:
model: false
user: true
---
Body the model reads when the skill runs.The portable semantics are invoke: {model, user} — who may invoke it, not a separate
capability kind:
model | user | Meaning |
|---|---|---|
true | true | default — normal interactive/discoverable skill |
true | false | background/model-only capability |
false | true | explicit user action |
false | false | invalid — nobody could invoke it; uze never projects it |
No invoke: block means the default (model: true, user: true) — an existing SKILL.md with no
frontmatter block behaves exactly as before. Every harness translates this policy into its own
encoding (Claude's frontmatter, Codex's agents/openai.yaml, OpenCode's metadata.opencode) —
see Capabilities for the per-harness route.
commands/ is not a canonical directory. A vendor-administered commands/ folder inside an
explicit vendor envelope is native delivery you shipped yourself; uze never rediscovers it as
a portable capability.
MCP servers
mcp.json at the package root, the standard mcpServers shape:
{
"mcpServers": {
"search": { "command": "my-mcp-server", "args": ["--stdio"] }
}
}Delivered natively wherever the harness has a native mechanism (claude mcp add, codex mcp add,
agy mcp add, OpenCode's global config), or folded into the native/generated plugin envelope when
one exists.
Agents
A portable agent profile — Markdown, agents/<name>.md:
---
name: planner
description: Breaks a task into an ordered implementation plan.
---
Everything below the frontmatter is the agent's instructions.Only name, description, and the body are claimed as the portable subset (ADR-031). Claude
Code, OpenCode, and Antigravity receive their documented native Markdown-agent surface; Codex has
no on-disk Markdown agent format, so uze generates its documented standalone TOML file instead —
name/description/developer_instructions mapped straight from your frontmatter and body,
never hand-edited afterward. Vendor-only fields (model, permissions, delegation) stay unclaimed:
an integration either genuinely preserves a field or reports the route as adapted, never a silent
best guess.
Hooks
One authored hooks.json plus plain scripts — a narrow command ABI, not a plugin runtime
(ADR-033):
{
"hooks": {
"PreToolUse": [
{
"matcher": "shell|file.write",
"effect": "deny",
"hooks": [{ "type": "command", "command": "${PLUGIN_ROOT}/scripts/check", "timeout": 10 }]
}
]
}
}- Events:
PreToolUse,PostToolUse,Stop— no others are canonical. - Matcher:
|-separated portable tool aliases (shell,file.read,file.write,file.edit,search.files,search.web,agent.spawn,agent.message), or an explicitnative:<tool>escape hatch. Omitted, it matches every tool. - Effect:
observe(default),allow,ask,deny, ortransform(PreToolUseonly). Not every harness carries every effect — see what each harness can actually carry. - Your handler reads one normalized JSON object on stdin and may write one bounded JSON decision
(
{"decision": "allow|ask|deny", "reason": "..."}) on stdout. Exit3is the canonical hard deny; any other failure is fail-open forobserve/allowand fail-closed fordeny/ask/transform.
Claude, Codex, and Antigravity get native hook configuration; OpenCode — which only exposes hooks as TypeScript plugin callbacks — gets an owned, regenerable bridge, so you never need a TypeScript toolchain to ship a hook that works there too.
Hook commands and MCP server commands are executable capabilities — uze's single acquisition trust prompt lists every process your package can cause to run before anything is installed, so authors can't smuggle in a surprise command and consumers always see the full list up front. Consent is not inherited across an update: a new executable capability is asked about again.
Instructions
A package may ship its own AGENTS.md. Its content is composed into the project's AGENTS.md
as one delimited, receipt-owned region when someone runs uze install — one region per
contributing package, and nothing outside it is touched. This is how a plugin ships standing
project instructions without asking anyone to maintain a second file per harness.
Write it as the instructions themselves, not as a document about your plugin: it lands inside someone else's project file.
Testing it locally
A plugin is always resolved through a marketplace, even a one-plugin local one — there is no bare
uze add <path>. Point uze at the repository holding your marketplace.json (see
below), not at the plugin itself:
$ uze market add ./my-marketplace
$ uze my-plugin@my-marketplace # add it to the current project
$ uze plugin inspect my-plugin # capabilities + per-harness delivery, before or after installA local marketplace is still a Git repository, and installing clones it at a commit rather
than reading your working tree. So git commit in the marketplace is what makes an edit
installable — which is also what lets a lock say which bytes it installed.
plugin inspect is read-only and safe to run repeatedly — the fastest way to see what uze thinks
your package contains and how each harness will receive it, before committing to an install.
Publishing through a marketplace
A marketplace is a marketplace.json registry manifest at the root of a Git repo or local
directory, listing one or more plugins by relative source:
{
"name": "my-marketplace",
"owner": { "name": "you", "url": "https://github.com/you" },
"plugins": [
{
"name": "my-plugin",
"source": "./plugins/my-plugin",
"description": "One sentence.",
"keywords": ["example"]
}
]
}This is the repo's own manifest shown verbatim — this repository is uze's official marketplace,
plugins/uze included. Push it and anyone can consume it:
$ uze market add https://github.com/you/my-marketplace
$ uze my-plugin@my-marketplacemarketplace.json was briefly named agents.json and reverted (ADR-032) — the same manifest
under its current filename.