Beta·every release is a pre-releaseAPIs and harness behavior are still changing·every release until v1 is a pre-release
uzev1.0.0-beta.12
Reference

Plugin format

Every file a plugin can ship, and how each one reaches each agent

hooks.json
mcp.json
plugin.json

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.json carries the $schema above and only the standard's fields.
  • Each skill sits at skills/<name>/SKILL.md, one level deep.
  • mcp.json carries its own $schema and a type on 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.
modeluserThe skill is
truetrueAvailable to both (the default, and what no invoke block means)
truefalseBackground knowledge the agent uses on its own
falsetrueAn 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: harness must be a map of harness names, and it may not redefine name, description or invoke. A model or tools at 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 model that is not provider/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, or native:<tool>. Omitted, it matches every tool. On SessionStart the matcher names how the session began instead: startup, resume, clear. Omitted, it matches all three.
  • Effect: observe (default), allow, ask, deny, transform. SessionStart only 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:

ScriptLinux and macOSWindows
executable fileruns itself, so its shebang decidesnot applicable
.pypython3the Python that answers: py -3, python or python3
.js, .mjs, .cjsnodenode
.ps1pwshWindows PowerShell 5.1
.shshnone
.exenoneruns 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.

On this page