Plugin format
Every file a plugin can ship, and how each one reaches each agent
Only plugin.json is required. Every file is stored byte for byte, never
rewritten into a format of uze's own.
plugin.json
The Agent Plugins 1.0 manifest:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "flow",
"description": "What this plugin does, in one sentence.",
"keywords": ["review", "commits"]
}plugin.json is the one place that describes a plugin. uze installs it by
name, and shows its description and keywords wherever the plugin is
listed: the plugins screen, uze market inspect and the JSON reports. The
standard also allows version, author (name, email, url),
homepage, repository, license and extensions, and nothing else at
the top level. Settings for one tool go under extensions, keyed by that
tool's namespace; uze's is sh.uze.
Requirements
A plugin whose scripts need a program on the machine says so under
extensions["sh.uze"].requirements:
{
"name": "flow",
"extensions": {
"sh.uze": {
"requirements": [
{ "executable": "python3", "version": ">=3.10", "purpose": "the commit guard is Python" }
]
}
}
}Each entry names an executable looked up on PATH, and may give a minimum
version (>=3.10 or 3.10) and a purpose. A malformed entry stops the
install before anything is stored. A missing or too old program does not:
the plugin installs, and uze install, uze status -m, uze inspect and
uze doctor name what is missing, who needs it, and the command that
installs it with a package manager the machine has. uze never runs that
command; you do.
A program only counts when it answers. Asked its version, a Windows
python3 that only opens the Microsoft Store, or the macOS python3 that
asks to install the developer tools, is reported missing.
You declare only what your own scripts use. What uze generates declares its
own needs: a hook delivered through the shell wrapper needs jq, and shows
up as needed by the hook wrapper. The check runs in the shell that ran the
command, so a harness started from a desktop launcher, with a shorter
PATH, may still miss a program that check found.
Agent Plugins 1.0
A uze plugin is also a valid Agent Plugins 1.0 plugin, the open standard that Codex, GitHub Copilot, VS Code, Cursor and Kiro load unchanged, as long as it keeps to the standard in the three places it covers:
plugin.jsoncarries the$schemaabove and only the standard's fields.- Each skill sits at
skills/<name>/SKILL.md, one level deep. mcp.jsoncarries its own$schemaand atypeon every server.
Agents, hooks, AGENTS.md, a skill's invoke and harness blocks, and
${PLUGIN_ROOT} outside mcp.json are uze's additions. The standard leaves
these to each tool, so its clients ignore them rather than reject the plugin.
uze agent plugin check says whether a plugin is valid under the standard,
or names what keeps it from being one. That is advice: uze's own format
decides what installs, and a plugin written before the standard installs as
it always did. uze agent plugin create writes a plugin that is valid under
both.
Skills
skills/<name>/SKILL.md, an Agent Skill. The optional
invoke block says who may call it:
---
name: review
description: Review the current changes
invoke:
model: false # the agent may not pick it on its own
user: true # you may call it
---
What the agent does when the skill runs.model | user | The skill is |
|---|---|---|
true | true | Available to both (the default, and what no invoke block means) |
true | false | Background knowledge the agent uses on its own |
false | true | An explicit action only you can call |
Each agent receives the policy in its own encoding. A skill is always called by
its plugin-qualified name, like /flow:review; the prefix per agent is in
Harness support, in each harness's tab.
Agents
agents/<name>.md: a name, a description, and the agent's instructions as
the body.
---
name: planner
description: Breaks a task into an ordered plan.
---
Everything below the front matter is the agent's instructions.Every agent is offered as <plugin>:<name>, and a subdirectory under agents/
joins the name: agents/review/security.md in plugin flow is
flow:review:security. A name in the front matter replaces only the file's
part, so name: audit in that file is flow:review:audit. Dispatch an agent by
that full name.
name and description are read the same way by every harness. A model, a
tool list or a reasoning effort is written differently by each one, so it goes
under harness, one entry per harness:
---
name: security
description: Reviews the diff for security flaws.
harness:
claude-code: { model: haiku, tools: [Read, Grep] }
codex: { model: gpt-6-luna, model_reasoning_effort: high }
opencode: { model: anthropic/claude-haiku-4-5, tools: { read: true } }
---Each harness receives its own entry in its own format, and never the
harness block itself. A harness with no entry receives name, description
and the instructions. The same block works in a skill's SKILL.md.
uze agent plugin check validates the block in two layers:
- Common rules:
harnessmust be a map of harness names, and it may not redefinename,descriptionorinvoke. Amodelortoolsat the top level is a warning, because only the harness that writes it that way can use it. - Each harness's rules: a value the harness would reject is an error, such
as an OpenCode
modelthat is notprovider/model. A field Codex does not read is left out with a warning, because Codex refuses an agent that carries one. A field no test has confirmed on a harness is delivered as written, with a warning.
The install names what each harness did not receive. Codex has no Markdown agent format, so uze generates its TOML file from yours.
Files your plugin ships
A skill, an agent, a hook or an MCP server can use any other file of the
plugin, such as a script, a template or a reference page. Name it with
${PLUGIN_ROOT}, which uze resolves to the installed copy of your plugin in
SKILL.md, agent definitions, hooks.json and mcp.json, for every harness:
Read the checklist in ${PLUGIN_ROOT}/phases/plan.md before you start.That copy holds the whole plugin and is rebuilt whenever the plugin changes, so a hook may build into it, and the build is redone after an update. Your plugin's installed files are never changed by it.
Relative paths inside a skill's own directory (scripts/, references/) keep
working as well. Folders a single harness would load as a feature of its own,
such as Claude Code's commands/ or bin/, are not delivered: uze delivers
what every harness shares.
MCP servers
mcp.json, the Agent Plugins 1.0 shape:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"search": { "type": "stdio", "command": "my-mcp-server", "args": ["--stdio"] },
"local": { "type": "stdio", "command": "./bin/server", "args": ["${PLUGIN_ROOT}/config.json"] }
}
}command is one executable: a name found on PATH, or a ./ path to one
the plugin ships. A server without type or $schema still installs.
${PLUGIN_DATA} is not provided yet; checking a plugin that uses it says so.
Each agent receives it through its own mechanism (claude mcp add,
codex mcp add, OpenCode's config, …).
Hooks
hooks.json runs a script when a session starts, when an agent is about to
use a tool or has used one, or when it stops:
{
"hooks": {
"PreToolUse": [
{
"matcher": "shell|file.write",
"effect": "deny",
"hooks": [{ "type": "command", "command": "hooks/guard.py", "args": ["--strict"], "timeout": 10 }]
}
]
}
}- Events:
PreToolUse,PostToolUse,Stop,SessionStart. - Matcher: tool aliases separated by
|:shell,file.read,file.write,file.edit,search.files,search.web,agent.spawn,agent.message, ornative:<tool>. Omitted, it matches every tool. OnSessionStartthe matcher names how the session began instead:startup,resume,clear. Omitted, it matches all three. - Effect:
observe(default),allow,ask,deny,transform.SessionStartonly observes: any other effect there is refused.
Naming the script: one file for every platform
With args (a list, empty when there are none), command is the path of a
script inside your plugin, relative to its root, and each word in args
reaches it exactly as written. Nothing is read by a shell, so nothing needs
quoting. uze starts the script with the program the platform calls for:
| Script | Linux and macOS | Windows |
|---|---|---|
| executable file | runs itself, so its shebang decides | not applicable |
.py | python3 | the Python that answers: py -3, python or python3 |
.js, .mjs, .cjs | node | node |
.ps1 | pwsh | Windows PowerShell 5.1 |
.sh | sh | none |
.exe | none | runs itself |
On OpenCode a .js, .mjs, .cjs or .ts script runs in OpenCode's own
runtime and needs nothing from the machine. interpreter (a list of words,
such as ["uv", "run", "--script"]) replaces the table for one handler.
What the launcher needs from the machine, python3 or node, becomes a
requirement of the plugin: uze install and uze doctor say when it is
missing and how to install it. A guard whose interpreter is missing denies
every call it matches until it is installed.
Python and JavaScript run the same on every platform, so a hook written in
either works everywhere without a second version. A .sh script cannot run
on Windows; a deny, ask or transform hook that uses one keeps the plugin
from installing there.
A shell line, per platform
Without args, command is one line for the POSIX shell, or a pair with a
line for each platform:
{ "type": "command", "command": { "posix": "${PLUGIN_ROOT}/check", "windows": "& \"${PLUGIN_ROOT}/check.ps1\"" } }Each platform runs only the line written for it, in sh or in Windows
PowerShell 5.1. ${PLUGIN_ROOT} is your plugin's directory. A guard with no
line for a platform keeps the plugin from installing there.
Older uze builds do not know args, and refuse a plugin that uses it.
The handler's contract
The handler reads the context from HOOK_* environment variables and answers
with its exit code:
import os, sys
command = os.environ.get("HOOK_COMMAND", "")
if ".env" in command or "id_rsa" in command:
sys.stderr.write(f"blocked: {command} touches a secret file")
sys.exit(3)0 allows, 3 denies with stderr as the reason. Anything else, or a timeout,
lets the tool run for observe and allow hooks and blocks it for the rest.
A transform handler exits 0 and writes the whole replacement input, as one
JSON object in the harness's own shape, to stdout.
A SessionStart handler gets HOOK_EVENT=session_start and HOOK_SOURCE
(startup, resume or clear), and nothing it answers keeps the session from
opening. Keep it fast and safe to run twice.
Which events, effects and tools each agent carries is in Harness support, generated from uze's own delivery plan. A hook an agent cannot honour is reported before it is installed, never silently weakened, and the agent still receives the plugin's other hooks. The full contract, every variable included, is in portable-hooks.md.
Commands a plugin runs
MCP servers and hooks run commands on the user's machine. Every one is listed
before installing, with a question, and asks again when an update adds a new one. Where
it cannot ask, in CI, it refuses unless --trust is passed.
marketplace.json
A marketplace is a Git repository with a marketplace.json at its root:
{
"name": "my-marketplace",
"owner": { "name": "you" },
"plugins": [
{
"name": "my-plugin",
"source": "./plugins/my-plugin",
"category": "productivity"
}
]
}An entry locates and files a plugin, and nothing else. It needs name and
source, a path inside the repository, and may carry category, one word
for the kind of work the plugin is for, such as productivity, development
or security: the catalogue's classification, which is why it lives here and
not in plugin.json.
What describes the plugin belongs in its plugin.json. A description or
keywords left on an entry is not read, and uze agent market check warns
on each one with what to do: move it into plugin.json, delete it because
plugin.json already says the same, or keep one of two values that differ.