This is an executable tutorial for developers and Agents. It takes you from the example plugin through the manifest, components, local testing, marketplace registration, and release checks so the result is installable, verifiable, and publishable.
- Creating a plugin: follow Section 1 for the shortest path, then read only the relevant reference in Sections 2–4.
- Changing an existing plugin: inspect its manifest, README files, and component directories first; then load only the section for the component you are changing.
- Looking up a field or protocol: jump directly to Section 2 (fields), Section 3 (complete JSON), or Section 4 (Hooks).
- Agent reading order: read this section, Sections 1.1–1.4, and the definition of done first. Load later reference sections only when the task reaches them.
- Copy
plugins/example-plugin/toplugins/<your-plugin-name>/. - Edit
.zcode-plugin/plugin.jsonsoname,version, and the description reflect the real capability. - Keep the components you need and delete unused samples. Leave at least one command, skill, hook, agent, or MCP server.
- Write equivalent
README.mdandREADME_CN.mdfiles covering purpose, dependencies, permissions, network access, and side effects. - Register the plugin in the root
marketplace.json. Keep the registration'sname,version, description, and category aligned with the manifest. - Run validation, build the distribution, and smoke-test the main capability.
- Read the target plugin's
.zcode-plugin/plugin.json, both README files, the rootmarketplace.json, and the actual component directories. Treat those files as the repository's sources of truth. - Classify the task as creating a plugin, changing a component, or changing documentation. Load only the later sections required by that branch.
- Change the manifest and component first, then synchronize README files, the marketplace entry, and the version. Every file that enters the installable package requires a version bump in both the manifest and
marketplace.json. - Use the repository's validation commands instead of inferring success from static inspection. Preserve the complete error boundary and stop where a human decision is required.
- Report changed files, plugin version, validation commands, and results. If local installation or a real run was not completed, say so explicitly.
.zcode-plugin/plugin.jsonis the primary plugin manifest. Component directories and.mcp.jsonare behavior sources.- The root
marketplace.jsonis the catalog source. The manifest and marketplace entry must have the samenameandversion. - Root documentation-only changes do not require a plugin version bump. Any file included in the installable package does.
- Keep secrets, private endpoints, customer data, machine-specific paths, and unreproducible build output out of the plugin.
Plugin development is not complete until all of these are true:
- The plugin directory is unique, kebab-case, and accepted by the validator.
- At least one component is discoverable, and the README explains how a person or Agent can trigger it.
- The marketplace entry and manifest agree on name, version, description, and category.
python3 scripts/validate.pyandpython3 scripts/build_dist.pyboth succeed.- The main capability has been run in ZCode, or the missing real-run verification is recorded explicitly.
A plugin is a directory with one manifest at its root and any number of optional component directories:
my-plugin/
├── .zcode-plugin/
│ └── plugin.json manifest (required)
├── commands/ slash commands, one .md file each
├── skills/ skills, each with a SKILL.md
├── agents/ sub-agent .md files
├── hooks/hooks.json Hooks configuration
└── .mcp.json MCP server declarations
Use .zcode-plugin/plugin.json as the manifest entry point.
The smallest manifest only needs name; all other fields are optional:
{
"name": "hello-world",
"version": "0.1.0",
"description": "My first plugin",
"skills": "skills",
"userConfig": {
"api_key": { "title": "API key", "type": "string", "required": true, "sensitive": true },
"device": { "type": "string", "default": "iPhone 16" }
}
}name must start with a lowercase letter or number and may contain ., _, and -, with 1–128 characters. commands, skills, hooks, mcpServers, and agents may be directory names, path arrays, or inline objects. Sensitive configuration values can be referenced from MCP declarations as ${user_config.key}.
| Component | Format and location |
|---|---|
| Command | commands/*.md, with YAML frontmatter and a body; use $ARGUMENTS for command arguments. |
| Skill | skills/<name>/SKILL.md; make name and description precise so the Agent can discover it. |
| Sub-agent | agents/*.md; frontmatter requires name and description, and the body is its system prompt. |
| Hooks | hooks/hooks.json; can run at seven built-in events and follow the plugin's enabled state. |
| MCP server | Root .mcp.json or manifest mcpServers; server keys are automatically namespaced. |
- Create a local
marketplace.jsonwhoseplugins[].sourcepoints to the plugin directory with a relative path. - In the client, open Settings → Plugin management → Discover, click +, and add the local marketplace path.
- Install and enable the plugin, trigger each changed component in a session, and repeat after refreshing the source.
Put plugins under plugins/, list them in the root marketplace.json, and publish the marketplace repository. A teammate can add the repository in the Discover page and receive the catalog. Hooks run only when the plugin is installed from an official marketplace or a local directory.
The built-in plugins are useful examples: start with a small skill, then add commands, Hooks, and MCP only after the basic path works.
The following tables describe the fields used by the public marketplace and plugin formats. A check mark means required.
Top-level fields:
| Field | Required | Meaning |
|---|---|---|
name |
✅ | Marketplace name; follows the plugin name rules. |
description |
Marketplace description. | |
plugins |
✅ | Array of plugin entries. |
pluginRoot |
Base directory for resolving relative source paths. |
|
allowCrossMarketplaceDependenciesOn |
Marketplace names allowed for cross-market dependencies. |
Fields on plugins[] entries:
| Field | Required | Meaning |
|---|---|---|
name |
✅ | Plugin name. |
source |
Plugin location; a relative path or a source object. | |
description / version |
Display description and immutable version. | |
category / tags |
Category string and searchable tag array. | |
dependencies |
Other plugins, written as name@market or a same-market name. |
|
strict |
Boolean; apply stricter validation to this entry. |
Source forms:
| Form | Meaning |
|---|---|
"./plugins/hello" |
Relative directory in the same marketplace repository. |
{ "source": "directory", "path": "/abs/path" } |
Local absolute directory. |
{ "source": "github", "repo": "owner/repo", "path": "subdir", "ref": "main" } |
A subdirectory from a Git repository. |
{ "source": "git", "url": "https://...git", "path": "subdir", "ref": "..." } |
A directory from any Git repository. |
{ "source": "file", "path": "..." } |
A local manifest file. |
{ "source": "url", "url": "https://.../marketplace.json" } |
A JSON manifest fetched over HTTP. |
{ "source": "npm", "package": "..." } |
An npm package. |
| Field | Required | Meaning |
|---|---|---|
name |
✅ | Must match ^[a-z0-9][a-z0-9._-]{0,127}$. |
version |
Immutable semantic version; defaults to 0.0.0. |
|
description |
One-line description shown in the plugin manager. | |
author |
String or { name, email, url }. |
|
homepage / repository |
Project and source URLs. | |
license |
License identifier such as MIT. |
|
keywords |
Search keyword array. | |
commands / skills / hooks / mcpServers / agents |
Component declarations as paths, path arrays, or inline objects. | |
dependencies |
Other plugin dependencies. | |
userConfig |
User-configurable values. |
The runtime records but does not execute channels, lspServers, outputStyles, and settings; unsupported fields produce diagnostics without preventing other components from loading.
userConfig fields:
| Field | Meaning |
|---|---|
type |
string, number, boolean, directory, or file. |
title |
Label shown in the UI. |
description |
Explanation of the setting. |
default |
Default value. |
required |
Whether the value is required. |
sensitive |
Masked value; sensitive values are not entered directly in the UI. |
| Field | Required | Meaning |
|---|---|---|
description |
✅ | Description shown for the command, unless the body is non-empty. |
argument-hint |
Short hint shown while entering arguments. | |
allowed-tools |
Tools available to the command when the client supports the field. | |
model |
Optional model selector. | |
disable-model-invocation |
Prevent automatic invocation when supported. |
| Field | Required | Meaning |
|---|---|---|
name |
✅ | Skill name; keep it specific and stable. |
description |
✅ | Say what the skill does and when it should be used. Include concrete trigger terms. |
Keep the main instruction path short. Put long, branch-specific material in focused reference files and link them with relative paths.
{
"name": "ios-simulator",
"version": "1.2.0",
"description": "An iOS simulator development loop: skills, commands, MCP, and Hooks",
"author": { "name": "Your Name", "email": "you@example.com", "url": "https://example.com" },
"homepage": "https://example.com/ios-simulator",
"repository": "https://github.com/your-team/ios-simulator",
"license": "MIT",
"keywords": ["ios", "simulator", "mobile"],
"commands": "commands",
"skills": ["skills", "extra-skills"],
"agents": "agents",
"hooks": "hooks/hooks.json",
"mcpServers": ".mcp.json",
"dependencies": ["skill-creator@zcode-plugins-official"],
"userConfig": {
"api_key": {
"title": "API key",
"description": "Used to access a third-party service",
"type": "string",
"required": true,
"sensitive": true
},
"default_device": { "title": "Default device", "type": "string", "default": "iPhone 16" },
"max_retries": { "type": "number", "default": 3 },
"verbose": { "type": "boolean", "default": false },
"workspace_dir": { "type": "directory" },
"config_file": { "type": "file" }
}
}commands, skills, hooks, mcpServers, and agents accept a directory string, a path array, or inline objects. The example demonstrates strings, arrays, and a file path.
{
"name": "my-market",
"description": "An internal team marketplace",
"pluginRoot": "plugins",
"allowCrossMarketplaceDependenciesOn": ["zcode-plugins-official"],
"plugins": [
{
"name": "hello-world",
"source": "./hello-world",
"description": "A greeting plugin",
"version": "0.1.0",
"category": "template",
"tags": ["starter", "template"],
"strict": true
},
{
"name": "from-github",
"source": { "source": "github", "repo": "your-team/another", "path": "plugins/x", "ref": "main" },
"dependencies": ["hello-world", "skill-creator@zcode-plugins-official"]
},
{
"name": "from-git",
"source": { "source": "git", "url": "https://git.example.com/x.git", "path": "sub", "ref": "v1.0" }
},
{
"name": "from-dir",
"source": { "source": "directory", "path": "/abs/path/to/plugin" }
},
{
"name": "from-url",
"source": { "source": "url", "url": "https://example.com/plugin-manifest.json", "headers": { "Authorization": "Bearer xxx" } }
},
{
"name": "from-npm",
"source": { "source": "npm", "package": "@scope/plugin" }
}
]
}With pluginRoot set, a relative source such as "./hello-world" resolves to plugins/hello-world.
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${ZCODE_PLUGIN_ROOT}/hooks/run.sh\" start",
"async": false,
"shell": true,
"timeout": 30,
"statusMessage": "Initializing…"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "process",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/hooks/check.js"],
"timeoutMs": 5000,
"statusMessage": "Checking command…"
}
]
}
],
"PostToolUse": [
{ "hooks": [ { "type": "command", "command": "echo done" } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "\"${ZCODE_PLUGIN_ROOT}/hooks/cleanup.sh\"", "async": true } ] }
]
}
}Supported events are SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure, and Stop. Standard hooks/hooks.json is auto-discovered; do not point the manifest at the same file again. A Hook enters a new session only after the plugin is enabled.
{
"mcpServers": {
"ios-simulator": {
"type": "stdio",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/dist/mcp/server.js"],
"cwd": "${ZCODE_PROJECT_DIR}",
"env": {
"IOS_SIM_ROOT": "${ZCODE_PLUGIN_ROOT}",
"IOS_SIM_DEVICE": "${user_config.default_device}"
},
"enabled": true,
"timeoutMs": 60000
}
}
}type may be omitted: command defaults to stdio, and url defaults to http. Available variables include ${ZCODE_PLUGIN_ROOT}, ${ZCODE_PLUGIN_DATA}, ${ZCODE_PROJECT_DIR}, and ${user_config.key}. Server keys are namespaced as plugin:<plugin-name>:<server-name>.
A stdio server must speak newline-delimited JSON-RPC on stdin/stdout (one message per line) — not LSP-style Content-Length framing. A runnable reference implementation is plugins/example-plugin/mcp/hello-server.mjs.
This section is for plugin developers who want to inspect, enrich, or enforce behavior during sessions, model requests, tool calls, and stopping. A Hook is a local subprocess protocol: ZCode writes one JSON line to stdin, and the process returns an exit code plus optional JSON on stdout.
New session → SessionStart
User submits → UserPromptSubmit → main model
Model requests a tool → PreToolUse → PermissionRequest when confirmation is required → tool execution
Tool succeeds → PostToolUse; tool fails → PostToolUseFailure
Model prepares to stop → Stop → finish, or continue the model with feedback
| Event | Matcher | Main use and effect |
|---|---|---|
SessionStart |
Matches source, commonly `startup |
clear |
UserPromptSubmit |
Always runs | Add context before a model call or block the request; do not rewrite the original prompt. |
PreToolUse |
Matches the tool name | Allow, ask, or deny a tool; may replace the complete tool input. |
PermissionRequest |
Matches the tool name | Runs when a permission result needs confirmation; may allow, deny, or update input/rules. |
PostToolUse |
Matches the tool name | Add model-visible context after success; cannot replace the tool output. |
PostToolUseFailure |
Matches the tool name | Add recovery advice, diagnostics, or retry constraints. |
Stop |
Always runs | Check the result; a block can continue the current model for up to three consecutive rounds. |
| Source | Use it for | How it takes effect |
|---|---|---|
~/.zcode/cli/config.json |
All workspaces for the current user | Set hooks.enabled: true. |
<workspace>/.zcode/config.json |
Versioned team rules | Set hooks.enabled: true. |
Plugin hooks/hooks.json |
Rules distributed with a plugin | Auto-discovered and follows the plugin enabled state. |
.agents/settings.json / legacy settings |
Migration | Read-only display; import explicitly into .zcode. |
Hooks execute in this order: user, workspace, then enabled-plugin Hooks. Within a source they run in array order. User and workspace settings are concatenated, not overridden.
Each session captures a Hook configuration snapshot. After editing a file, saving settings, or enabling/disabling a plugin, start a new session before verifying the change.
{
"hooks": {
"enabled": true,
"timeoutMs": 60000,
"maxOutputBytes": 32768,
"events": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "process",
"command": "node",
"args": ["scripts/check-write.mjs"],
"enabled": true,
"timeoutMs": 10000
}
]
}
]
}
}
}The following plugin injects one team constraint when a new session starts. It requires node on PATH:
context-guard/
├── .zcode-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── context.mjs
{
"name": "context-guard",
"version": "0.1.0",
"description": "Inject team development constraints at session start"
}hooks/hooks.json is the standard location and is auto-loaded, so the manifest does not need a hooks field:
{
"description": "Context Guard hooks",
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "process",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/hooks/context.mjs"],
"timeoutMs": 5000
}
]
}
]
}
}let raw = "";
process.stdin.setEncoding("utf8");
for await (const chunk of process.stdin) raw += chunk;
const input = JSON.parse(raw);
process.stderr.write("[context-guard] " + input.hook_event_name + "\n");
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: input.hook_event_name,
additionalContext: "Run type checks and affected tests before committing."
}
}));- Add the local marketplace or plugin source in Settings → Plugin management → Discover, then install and enable it.
- Open Settings → Hooks and confirm the plugin Hook appears read-only with the expected event, matcher, command, and source path.
- Start a new session and send a request; the first model request should see the injected constraint.
- Write diagnostics to stderr. Keep stdout limited to the protocol result so diagnostic text cannot corrupt JSON.
| Type | Semantics | Recommendation |
|---|---|---|
process |
Runs command + args[] directly by argv without a shell; synchronous only. |
Prefer for Node, Python, and binary scripts because argument boundaries and portability are explicit. |
command |
Sends a complete string to the system shell; supports shell, async, and timeout. |
Use when a shell string is required; account for quoting differences across operating systems. |
timeoutMsis in milliseconds; the compatibilitytimeoutfield is in seconds.timeoutMswins when both exist.- The root default timeout is 60000 ms, and the default stdout limit is 32768 bytes.
- A Hook may set
enabled: false; the runtime skips it rather than merely dimming it in the UI. commandwithasync: trueis fire-and-forget: the current event continues immediately, and background stdout cannot block, change input, or inject context. Its lifecycle is still recorded.statusMessageis stored and shown in settings, but is not yet a live runtime status indicator.
Matcher rules:
- Empty, omitted, or
*matches everything. - A value containing only letters, numbers, underscores, and
|is an exact name list, such asWrite|Edit. - Other characters make it a JavaScript regular expression. An invalid expression is skipped with a diagnostic.
- Tool events match the actual tool name and accept the
Agent/Taskaliases. SessionStartmatchessource;UserPromptSubmitandStopdo not filter by matcher.
ZCode writes one JSON object followed by a newline to every Hook. The payload keeps ZCode camelCase fields and legacy snake_case aliases so older plugins can continue reading the latter.
{
"session_id": "session-123",
"transcript_path": "/tmp/zcode-hook/transcript.jsonl",
"cwd": "/workspace/demo",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "src/index.ts",
"content": "..."
},
"tool_use_id": "tool-123"
}| Event | Important fields |
|---|---|
SessionStart |
source, optionally agent_type and model |
UserPromptSubmit |
prompt |
PreToolUse |
tool_name, tool_input, tool_use_id |
PermissionRequest |
tool_name, tool_input, and permission_suggestions when real data exists |
PostToolUse |
Structured tool_response, tool name, input, and call ID |
PostToolUseFailure |
String error, is_interrupt, and tool fields |
Stop |
stop_hook_active, last_assistant_message |
transcript_path points to a temporary JSONL file readable by the current Hook. ZCode removes the temporary directory after the Hook completes; write durable plugin data to ZCODE_PLUGIN_DATA instead.
Empty stdout means success with no additional effect. Non-JSON stdout is diagnostic only and is not added to model context. The runtime parses only valid JSON whose first non-whitespace character is {. Unknown fields are ignored; invalid known fields or event names cause a recoverable Hook failure without stopping later Hooks.
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Only modify files relevant to the current task."
}
}Top-level additionalContext and additional_context are also accepted. Prefer hookSpecificOutput because the event is explicit.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Redirected to an allowed directory",
"updatedInput": {
"file_path": "generated/index.ts",
"content": "..."
},
"additionalContext": "The file was redirected to generated/."
}
}updatedInput is a complete replacement object, not a partial patch; ZCode revalidates it against the tool schema. To deny, return permissionDecision: "deny" and a permissionDecisionReason. When multiple Hooks aggregate results, deny wins over ask, and ask wins over allow.
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"message": "Production directories may only be changed by the release flow"
}
}
}Set behavior to allow to allow a request. A decision may also contain updatedInput and updatedPermissions; the legacy permissionUpdates field is accepted. Explicit deny rules, Plan-mode write restrictions, and hard tool limits cannot be bypassed by an allow response.
{
"continue": false,
"reason": "Provide a ticket number first",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "The request was blocked by a team policy."
}
}{
"decision": "block",
"reason": "The test command and result are still missing."
}decision: "block" needs a reason or additionalContext to continue. For legacy configurations, continue: true with additional context is also accepted. After three consecutive continuation rounds, the runtime stops to prevent an infinite loop.
Exit codes:
0: success; stdout is parsed.2: blocking shortcut; produces block/deny on blockable events or one more round onStop.- Any other non-zero code: recoverable Hook failure; later Hooks still run.
- The manifest entry point is
.zcode-plugin/plugin.json. - Standard
hooks/hooks.jsonis auto-discovered. The manifest may also reference a relative JSON path, inline object, or an array of both; do not point it at the same standard file twice. - Plugin processes can read
ZCODE_PLUGIN_ROOT,ZCODE_PLUGIN_DATA,ZCODE_PLUGIN_ID, andZCODE_PLUGIN_NAME. - Plugin path variables in commands and arguments are substituted before execution. Store durable data in
ZCODE_PLUGIN_DATA, never in the install directory. - Hooks are read-only in settings and follow the plugin enabled state.
Enabling a plugin grants code-execution trust. Review its source, Hook configuration, and scripts before enabling it.
- Open Settings → Hooks.
- Choose user or workspace scope, then add the event, executor, matcher, command/arguments, timeout, async mode, and status text.
- Existing
.zcodeHooks can be viewed, edited, deleted, and toggled individually. Scope cannot be changed in place; delete and recreate the Hook at the target scope.
When maintaining plugin documentation, prioritize information that changes execution:
- Path before background: state the goal, reading order, and shortest path first; keep field tables, full JSON, and Hook protocol details in reference sections.
- One action per step: give each step one verifiable action and its completion condition, command, or expected result.
- Explicit branches: document the next action and stop condition for missing dependencies, permission requests, sensitive configuration, and failed validation.
- One source of truth: treat repository files and validators as authoritative for paths, commands, and fields; use documentation for conventions, rationale, and traps that the files cannot express.
- Copyable examples: examples should be safe to rename, run, or compare against. Keep versions, paths, and categories aligned with the repository.
This organization follows progressive disclosure from the Agent Skills specification and instruction-writing guidance for Agents: load the short path first, then load detailed references only when the task reaches them; make actions, edge cases, and completion criteria checkable.
Official references:
- Agent Skills Specification: directory structure, reference files, and progressive disclosure.
- A practical guide to building agents: clear actions, smaller steps, edge cases, and guardrails.