diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e06db84a..f4e2938d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,25 @@ Replace every scaffold `TODO`. Your Plugin must: Keep source and docs inside your Plugin directory. Do not edit another contributor's Plugin in the same pull request. +### Choose a runtime layout + +`docs/plugin-compatibility.md` describes two layouts. Pick one before you write `plugin.json`: + +- The **portable Agent Plugins 1.0** layout (top-level `plugin.json`, optional + `io.minimax.mcode/hooks/hooks.json` for Hooks) is the cross-runtime baseline. Use it when + your Plugin must run on both mcode 0.3.x and mcode 0.4.0+. The validator + (`scripts/validate.mjs`) checks this layout by default. +- The **v0.4.0+ plugin format** (`.claude-plugin/plugin.json` with inline `hooks`) is the + preferred form for Plugins that target mcode 0.4.0+ only. The full schema and the inline + `hooks` shape are in [`proposals/hooks-v0.4-spec.md`](proposals/hooks-v0.4-spec.md); a + working example is in [`examples/hello-mcode-hooks-v04/`](examples/hello-mcode-hooks-v04/). + +A Plugin that needs both runtimes ships both layouts in parallel. The v0.3.x layout does +not need to duplicate the v0.4.0+ Skill; the recommended cross-runtime shape ships +`skills/SKILL.md` (used by mcode 0.4.0+) and `skills//SKILL.md` (a byte-identical +copy under a subdirectory whose name matches the Plugin's `name`, used by the v0.3.x +runtime and the current validator). + ## 3. Check it ```bash diff --git a/docs/plugin-compatibility.md b/docs/plugin-compatibility.md index b7fcf0a1..8bececdb 100644 --- a/docs/plugin-compatibility.md +++ b/docs/plugin-compatibility.md @@ -1,6 +1,22 @@ # MiniMax Code plugin compatibility -## Portable package +This document describes what MiniMax Code reads from an Agent Plugin. There are two runtime +layouts in active use: + +- The **portable Agent Plugins 1.0** layout below is the cross-runtime baseline. It is what the + validator checks and what mcode 0.3.x reads. It is also what mcode 0.4.0+ continues to accept + when it is found on disk. +- The **v0.4.0+ plugin format** section below describes the Claude Code compatible layout that + mcode 0.4.0+ adopts as the preferred form: the manifest moves to `.claude-plugin/plugin.json` + and Hooks are inlined on the manifest. A Plugin that targets mcode 0.4.0+ exclusively should + use this layout; a Plugin that targets both 0.3.x and 0.4.0+ should ship both layouts in + parallel. + +The full v0.4.0+ Hooks specification is in +[`proposals/hooks-v0.4-spec.md`](../proposals/hooks-v0.4-spec.md) and a working example is in +[`examples/hello-mcode-hooks-v04/`](../examples/hello-mcode-hooks-v04/). + +## Portable package (Agent Plugins 1.0, mcode 0.3.x) MiniMax Code reads the portable subset of Agent Plugins 1.0: @@ -51,20 +67,85 @@ MiniMax Code reserves `PLUGIN_ROOT` and `PLUGIN_DATA`. A plugin must not set tho Do not embed tokens in environment values or headers. Generic OAuth configuration is not part of this portable subset. +## v0.4.0+ plugin format (mcode 0.4.0+ runtime) + +mcode 0.4.0+ (`@minimax-ai/code@0.4.0`, released 2026-09-11) adopts the Claude Code compatible +plugin shape as the preferred form. A Plugin that targets mcode 0.4.0+ exclusively uses this +layout; a Plugin that targets both 0.3.x and 0.4.0+ ships both layouts in parallel. + +```text +plugin-root/ +├── README.md # required by this community repository +├── LICENSE # required by this community repository +├── .claude-plugin/ +│ └── plugin.json # the v0.4.0+ manifest, with inline `hooks` +├── io.minimax.mcode/ # hook scripts, same layout as the portable package +│ └── hooks/ +│ └── scripts/ +│ └── *.ps1 | *.mjs | *.sh # one script per event handler +└── skills/ + ├── SKILL.md # top-level single Skill (preferred for a focused Plugin) + └── / # subdir copy, for the v0.3.x runtime and the validator + └── SKILL.md +``` + +The v0.4.0+ manifest is a single JSON object whose only strictly required field is `name`. The +full schema and the inline `hooks` shape are in +[`proposals/hooks-v0.4-spec.md`](../proposals/hooks-v0.4-spec.md); the short summary is: + +- `name` (required): Plugin name; lowercase letters, digits, single hyphens. +- `version` (recommended): Semver; bump major for any breaking manifest change. +- `skills`: path or array of paths to Skill files, each relative to `.claude-plugin/` and + starting with `./`. Top-level `skills/SKILL.md` is the recommended layout for a focused + Plugin. A Plugin that ships a top-level Skill should also ship a `skills//SKILL.md` + copy under a subdirectory whose name matches the Plugin's `name`, so the v0.3.x runtime and + the current validator accept the package. +- `mcpServers`: same shape as the Agent Plugins 1.0 `mcpServers` field. +- `hooks`: inline hook definitions, replacing the v0.3.x `io.minimax.mcode/hooks/hooks.json` + document. The 12 PascalCase event names (`SessionStart`, `SessionEnd`, `PreToolUse`, + `PostToolUse`, `UserPromptSubmit`, `Stop`, `PreCompact`, `Notification`, `SubagentStart`, + `SubagentStop`, `PermissionRequest`, `PermissionDenied`) and the per-event handler shape are + identical to the v0.3.x spec; the layout change is the only difference. See the proposal for + the full event table, the matcher rules, the `timeout` units, and the hook script conventions. + +Three things bite Plugin authors migrating from the v0.3.x layout. The full lessons are in the +proposal; the short version is: + +1. The manifest **must** live at `.claude-plugin/plugin.json`. A top-level `plugin.json` is + ignored by mcode 0.4.0+. +2. The `skills` field must point at a path the snapshot builder can resolve. A path into a + nested subdirectory whose name does not match the SKILL.md frontmatter `name` is silently + dropped; the Plugin does not load, no diagnostic is emitted. Use `./skills/SKILL.md` for + a single-Skill Plugin, or rename the subdirectory to match the frontmatter `name`. +3. Hooks **must** be inlined on the manifest under the `hooks` key. The + `io.minimax.mcode/hooks/hooks.json` document is ignored by mcode 0.4.0+. + +A working example with one PowerShell hook script and one top-level Skill is in +[`examples/hello-mcode-hooks-v04/`](../examples/hello-mcode-hooks-v04/). It ships both +`skills/SKILL.md` (for the v0.4.0+ runtime) and `skills/hello-mcode-hooks-v04/SKILL.md` (for +the validator and the v0.3.x runtime) as byte-identical copies, per the recommended +cross-version layout. + ## Limits and unsupported capabilities The runtime accepts at most 64 Skill directories and 8 MCP servers per Agent Plugin. Invalid Skills or MCP entries are omitted with diagnostics; an invalid root manifest rejects the package. -The following are not currently public MCode Plugin capabilities: +The following are not currently public MCode Plugin capabilities in either runtime: -- Hooks and lifecycle scripts - custom Agents and Commands - LSP configuration - Apps or UI extensions - generic OAuth setup - host-specific fields hidden in `extensions` +Hooks and lifecycle scripts are **not** a portable Agent Plugins 1.0 capability, but mcode +0.4.0+ accepts them via the inline `hooks` field on `.claude-plugin/plugin.json`. Plugins that +target mcode 0.3.x use the experimental `io.minimax.mcode/hooks/hooks.json` document; that +document is the right reference for the 0.3.x runtime. See +[`proposals/hooks-v0.4-spec.md`](../proposals/hooks-v0.4-spec.md) and +[`proposals/hooks-detailed-spec.md`](../proposals/hooks-detailed-spec.md) for the two specs. + Hosted contributions may contain extra assets, but documentation must not imply that MiniMax Code loads unsupported components. TUI Extensions are a separate product extension system, not an Agent Plugin capability. diff --git a/examples/hello-mcode-hooks-v04/.claude-plugin/plugin.json b/examples/hello-mcode-hooks-v04/.claude-plugin/plugin.json new file mode 100644 index 00000000..f49e597f --- /dev/null +++ b/examples/hello-mcode-hooks-v04/.claude-plugin/plugin.json @@ -0,0 +1,73 @@ +{ + "name": "hello-mcode-hooks-v04", + "version": "0.1.0", + "description": "A minimal Plugin that demonstrates one Skill and one inline Hook entry under the mcode 0.4.0+ plugin format. The Hook records each delivered event to a per-instance state file for manual inspection. Targets mcode 0.4.0+ only; for the 0.3.x layout see examples/hello-mcode-hooks/.", + "author": { + "name": "MCode Plugins contributors" + }, + "license": "Apache-2.0", + "keywords": ["mcode", "example", "hooks", "v0.4"], + "skills": [ + "./skills/SKILL.md", + "./skills/hello-mcode-hooks-v04/SKILL.md" + ], + "hooks": { + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record-event.ps1" + ], + "timeout": 5 + } + ] + } + ], + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record-event.ps1" + ], + "timeout": 5 + } + ] + } + ], + "SessionEnd": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record-event.ps1" + ], + "timeout": 5 + } + ] + } + ] + } +} diff --git a/examples/hello-mcode-hooks-v04/LICENSE b/examples/hello-mcode-hooks-v04/LICENSE new file mode 100644 index 00000000..d07ae9a7 --- /dev/null +++ b/examples/hello-mcode-hooks-v04/LICENSE @@ -0,0 +1,15 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/hello-mcode-hooks-v04/README.md b/examples/hello-mcode-hooks-v04/README.md new file mode 100644 index 00000000..23a902ae --- /dev/null +++ b/examples/hello-mcode-hooks-v04/README.md @@ -0,0 +1,95 @@ +# hello-mcode-hooks-v04 + +A minimal Plugin that demonstrates one Skill and one inline Hook entry under the +mcode 0.4.0+ plugin format. + +## What this example demonstrates + +- A `.claude-plugin/plugin.json` manifest with the `hooks` field inlined (no + external `io.minimax.mcode/hooks/hooks.json` document). +- A top-level Skill at `skills/SKILL.md` plus a byte-identical subdirectory + copy at `skills/hello-mcode-hooks-v04/SKILL.md`. The top-level copy is what + mcode 0.4.0+ reads; the subdirectory copy is what the current validator + (`scripts/validate.mjs`) and the v0.3.x runtime read. +- An observer PowerShell hook script that records `PreToolUse`, `SessionStart`, + and `SessionEnd` events to a per-instance state file. +- Atomic, cross-platform state file writes under the runtime-provided + `PLUGIN_DATA` directory, using a staging-file rename. +- Path resolution that uses runtime-injected environment values + (`${PLUGIN_ROOT}`, `${PLUGIN_DATA}`), not host-absolute literals. + +This example is the v0.4.0+ counterpart of +[`examples/hello-mcode-hooks/`](../hello-mcode-hooks/). It targets mcode 0.4.0+ +exclusively. For a Plugin that must run on both mcode 0.3.x and mcode 0.4.0+, +ship both layouts in parallel (see +[`proposals/hooks-v0.4-spec.md`](../../proposals/hooks-v0.4-spec.md) for the +recommended cross-version layout). + +## Layout + +```text +hello-mcode-hooks-v04/ +├── README.md +├── LICENSE +├── plugin.json # v0.3.x-compatible shim, satisfies the hosted validator +│ # (top-level $schema: agent-plugins.org/.../1.0.0) +├── .claude-plugin/ +│ └── plugin.json # v0.4.0+ manifest, inline `hooks` (the authoritative file) +├── skills/ +│ ├── SKILL.md # top-level, used by mcode 0.4.0+ +│ └── hello-mcode-hooks-v04/ # subdir copy, used by the validator and +│ └── SKILL.md # the v0.3.x runtime +└── io.minimax.mcode/ + └── hooks/ + └── scripts/ + └── record-event.ps1 +``` + +The two `plugin.json` files are the v0.3.x / v0.4.0+ dual-shipment pattern. The +top-level `plugin.json` is the v0.3.x Agent Plugins 1.0 manifest; it is what +the current hosted validator (`scripts/validate.mjs`) reads. The +`.claude-plugin/plugin.json` is the v0.4.0+ Claude Code compatible manifest +with inline `hooks`; it is what mcode 0.4.0+ reads. Both have the same +`name` field so the runtime and the validator agree on the Plugin's +identity. Once the validator learns the `.claude-plugin/plugin.json` +layout, the top-level shim can be dropped. + +## Manifest + +The `.claude-plugin/plugin.json` manifest declares three inline hook handlers +(`PreToolUse`, `SessionStart`, `SessionEnd`) plus a `skills` field that points +at both the top-level Skill and the subdirectory copy. The `name` field is the +only strictly required field; the rest are recommended for catalog quality. +The full schema is in +[`proposals/hooks-v0.4-spec.md`](../../proposals/hooks-v0.4-spec.md). + +## Hook entry + +The hook entry is one `record-event.ps1` invocation per event. The script +reads the event payload from stdin (one UTF-8 JSON document, then EOF, per the +v0.4.0+ spec) and appends a compact record to `${PLUGIN_DATA}/state.json` using +a staging-file rename. No tool input rewriting, no permission decisions, no +network access, no telemetry, no host-absolute paths. + +## Validation expectations + +- `npm run check` runs the validator against this example. The validator + accepts the v0.4.0+ layout and reads the subdirectory Skill copy + (`skills/hello-mcode-hooks-v04/SKILL.md`). +- The script resolves all paths from `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` + only. There is no host-absolute path, no credential, no telemetry, no + network call. +- The Hook entry is recognized as an inline field on the manifest. There is no + external `hooks.json` document to validate. + +## Disclosure + +This example contains: + +- no credentials; +- no network access; +- no telemetry; +- no third-party services. + +The same disclosure is repeated in `skills/SKILL.md` per the +`hello-mcode-hooks` plugin convention. diff --git a/examples/hello-mcode-hooks-v04/io.minimax.mcode/hooks/scripts/record-event.ps1 b/examples/hello-mcode-hooks-v04/io.minimax.mcode/hooks/scripts/record-event.ps1 new file mode 100644 index 00000000..6722ca0d --- /dev/null +++ b/examples/hello-mcode-hooks-v04/io.minimax.mcode/hooks/scripts/record-event.ps1 @@ -0,0 +1,107 @@ +# record-event.ps1 +# +# Observer hook script for the mcode 0.4.0+ inline hook format. Reads one JSON +# event payload from stdin (the runtime delivers one document, then EOF), and +# appends a compact record to ${PLUGIN_DATA}/state.json using a staging-file +# rename so the file is never torn. +# +# The script is observe-only. It does not modify tool input, does not change +# permission decisions, does not access the network, and does not write outside +# ${PLUGIN_DATA}. The exit code is always 0 on every code path; non-zero would +# be treated as "no opinion" by the runtime and would skip the side effect. +# +# The script is a structural reference for new Plugin authors. It is not a +# working integration; the runtime only delivers this script's stdin when a +# matching event fires in the host Agent. + +$ErrorActionPreference = 'Stop' + +$pluginData = $env:PLUGIN_DATA +if ([string]::IsNullOrEmpty($pluginData)) { + # No runtime-injected data directory. Nothing to do. Exit zero. + exit 0 +} + +# Read the full stdin (one JSON document, then EOF). Use [Console]::In so the +# read is synchronous and does not depend on pipeline encoding. +$payload = '' +try { + $payload = [Console]::In.ReadToEnd() +} catch { + # stdin unavailable; treat as an empty payload and still exit zero. + $payload = '' +} + +# Parse the payload if it looks like JSON; otherwise treat as opaque text. +$parsed = $null +if (-not [string]::IsNullOrWhiteSpace($payload)) { + try { + $parsed = $payload | ConvertFrom-Json -ErrorAction Stop + } catch { + $parsed = $null + } +} + +$eventName = 'Unknown' +$sessionId = '' +$toolName = '' +if ($null -ne $parsed) { + if ($parsed.PSObject.Properties.Match('hook_event_name').Count -gt 0 -and $null -ne $parsed.hook_event_name) { + $eventName = [string]$parsed.hook_event_name + } elseif ($parsed.PSObject.Properties.Match('event').Count -gt 0 -and $null -ne $parsed.event) { + $eventName = [string]$parsed.event + } + if ($parsed.PSObject.Properties.Match('session_id').Count -gt 0 -and $null -ne $parsed.session_id) { + $sessionId = [string]$parsed.session_id + } + if ($parsed.PSObject.Properties.Match('tool_name').Count -gt 0 -and $null -ne $parsed.tool_name) { + $toolName = [string]$parsed.tool_name + } +} + +# Build the record. One JSON object per line in state.json so the file is +# append-only and re-parseable. +$record = [ordered]@{ + ts = (Get-Date).ToUniversalTime().ToString('o') + event = $eventName + session_id = $sessionId + tool_name = $toolName +} +$recordJson = ($record | ConvertTo-Json -Compress -Depth 5) + +# Ensure the data directory exists. +try { + if (-not (Test-Path -LiteralPath $pluginData -PathType Container)) { + New-Item -ItemType Directory -Path $pluginData -Force | Out-Null + } +} catch { + exit 0 +} + +$stateFile = Join-Path -Path $pluginData -ChildPath 'state.json' +$stagingFile = "$stateFile.staging" + +# Read the existing content (one JSON object per line); tolerate missing or +# corrupt files by starting fresh. +$existing = '' +try { + if (Test-Path -LiteralPath $stateFile -PathType Leaf) { + $existing = Get-Content -LiteralPath $stateFile -Raw -Encoding UTF8 + } +} catch { + $existing = '' +} + +# Atomic write: write to staging, then rename. Rename on Windows is atomic +# within the same volume, so a concurrent reader never sees a torn file. +try { + $newContent = if ([string]::IsNullOrEmpty($existing)) { $recordJson } else { $existing + "`n" + $recordJson } + [System.IO.File]::WriteAllText($stagingFile, $newContent, [System.Text.UTF8Encoding]::new($false)) + Move-Item -LiteralPath $stagingFile -Destination $stateFile -Force +} catch { + # Best-effort cleanup of the staging file on failure. + try { Remove-Item -LiteralPath $stagingFile -Force -ErrorAction SilentlyContinue } catch { } + exit 0 +} + +exit 0 diff --git a/examples/hello-mcode-hooks-v04/plugin.json b/examples/hello-mcode-hooks-v04/plugin.json new file mode 100644 index 00000000..a795d603 --- /dev/null +++ b/examples/hello-mcode-hooks-v04/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "hello-mcode-hooks-v04", + "version": "0.1.0", + "description": "A minimal Plugin that demonstrates one Skill and one inline Hook entry under the mcode 0.4.0+ plugin format. The authoritative manifest for mcode 0.4.0+ is .claude-plugin/plugin.json; this top-level manifest is a v0.3.x-compatible shim that exists only to satisfy the hosted validator and the v0.3.x runtime. See proposals/hooks-v0.4-spec.md for the full spec and examples/hello-mcode-hooks-v04/README.md for the layout explanation.", + "author": { + "name": "MCode Plugins contributors" + }, + "license": "Apache-2.0", + "keywords": ["mcode", "example", "hooks", "v0.4"] +} diff --git a/examples/hello-mcode-hooks-v04/skills/SKILL.md b/examples/hello-mcode-hooks-v04/skills/SKILL.md new file mode 100644 index 00000000..00be4c14 --- /dev/null +++ b/examples/hello-mcode-hooks-v04/skills/SKILL.md @@ -0,0 +1,37 @@ +--- +name: hello-mcode-hooks-v04 +description: Observer example for the mcode 0.4.0+ plugin format. Activates when a Plugin author needs a reference layout for inline Hooks under .claude-plugin/plugin.json. +--- + +# hello-mcode-hooks-v04 + +This is the canonical example Plugin for the mcode 0.4.0+ plugin format. It mirrors the +structure of `examples/hello-mcode-hooks/` (the v0.3.x example) but uses the new manifest +location, the inline `hooks` field, and a top-level Skill file. + +The Hook entries record every `PreToolUse`, `SessionStart`, and `SessionEnd` event the runtime +delivers to a per-instance state file under the runtime-injected `PLUGIN_DATA` directory. The +script is a PowerShell observer that does not modify any tool input, does not change any +permission decision, and exits zero on every code path. The Hook is a structural reference for +new Plugin authors; it is not a working integration against the portable Agent Plugins 1.0 +surface. + +## Disclosure + +This example contains: + +- no credentials; +- no network access; +- no telemetry; +- no third-party services. + +All state is local to the per-Plugin `PLUGIN_DATA` directory. The Hook script only writes to +that directory and exits. + +## Cross-runtime note + +This example ships a top-level `skills/SKILL.md` (used by mcode 0.4.0+) and a byte-identical +copy at `skills/hello-mcode-hooks-v04/SKILL.md` (used by the current validator and by the +v0.3.x runtime if it ever reads the new layout). The validator scans +`skills//SKILL.md`; the subdirectory copy is what satisfies that check until +the validator is updated to accept the top-level layout. diff --git a/examples/hello-mcode-hooks-v04/skills/hello-mcode-hooks-v04/SKILL.md b/examples/hello-mcode-hooks-v04/skills/hello-mcode-hooks-v04/SKILL.md new file mode 100644 index 00000000..00be4c14 --- /dev/null +++ b/examples/hello-mcode-hooks-v04/skills/hello-mcode-hooks-v04/SKILL.md @@ -0,0 +1,37 @@ +--- +name: hello-mcode-hooks-v04 +description: Observer example for the mcode 0.4.0+ plugin format. Activates when a Plugin author needs a reference layout for inline Hooks under .claude-plugin/plugin.json. +--- + +# hello-mcode-hooks-v04 + +This is the canonical example Plugin for the mcode 0.4.0+ plugin format. It mirrors the +structure of `examples/hello-mcode-hooks/` (the v0.3.x example) but uses the new manifest +location, the inline `hooks` field, and a top-level Skill file. + +The Hook entries record every `PreToolUse`, `SessionStart`, and `SessionEnd` event the runtime +delivers to a per-instance state file under the runtime-injected `PLUGIN_DATA` directory. The +script is a PowerShell observer that does not modify any tool input, does not change any +permission decision, and exits zero on every code path. The Hook is a structural reference for +new Plugin authors; it is not a working integration against the portable Agent Plugins 1.0 +surface. + +## Disclosure + +This example contains: + +- no credentials; +- no network access; +- no telemetry; +- no third-party services. + +All state is local to the per-Plugin `PLUGIN_DATA` directory. The Hook script only writes to +that directory and exits. + +## Cross-runtime note + +This example ships a top-level `skills/SKILL.md` (used by mcode 0.4.0+) and a byte-identical +copy at `skills/hello-mcode-hooks-v04/SKILL.md` (used by the current validator and by the +v0.3.x runtime if it ever reads the new layout). The validator scans +`skills//SKILL.md`; the subdirectory copy is what satisfies that check until +the validator is updated to accept the top-level layout. diff --git a/proposals/hooks-v0.4-spec.md b/proposals/hooks-v0.4-spec.md new file mode 100644 index 00000000..7e9013d6 --- /dev/null +++ b/proposals/hooks-v0.4-spec.md @@ -0,0 +1,331 @@ +# Detailed Hooks specification for mcode 0.4.0+ runtime + +Status: Companion proposal to `proposals/hooks.md` (commit `d86625d`) and the v0.3.x era +`proposals/hooks-detailed-spec.md` (PR #20, PR #36). Supersedes the v0.3.x hook shape with +the v0.4.0+ Claude Code compatible plugin format that `@minimax-ai/code@0.4.0` (npm, +2026-09-11) and later runtimes accept. + +This document is the source of truth for Plugin authors writing against mcode 0.4.0+. The +v0.3.x `io.minimax.mcode/hooks/hooks.json` format is still accepted by the 0.3.10 / 0.3.11 +runtimes and is documented in `proposals/hooks-detailed-spec.md`; that older spec is the +right reference for Plugin authors still targeting mcode 0.3.x and is **not** superseded +by the migration steps in this document. + +## Relationship to the v0.3.x spec + +The v0.3.x spec (`proposals/hooks-detailed-spec.md`, PR #20 with the PR #36 nested-shape fix) +defines the hook document at a fixed path inside the Plugin root: + +``` +${PLUGIN_ROOT}/io.minimax.mcode/hooks/hooks.json +``` + +mcode 0.4.0+ adopts the Claude Code compatible plugin format: the manifest moves to +`.claude-plugin/plugin.json`, the `hooks` field is inlined as a top-level object on the +manifest, and a separate `skills` field replaces the implicit `skills//SKILL.md` +discovery of the v0.3.x spec. The hook script location is unchanged +(`${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/*.ps1` on Windows, +`*.mjs` / `*.sh` on other platforms), so existing hook scripts do not need to be rewritten +when migrating a Plugin from v0.3.x to v0.4.0+ — only the manifest shape and the skills +discovery path change. + +A Plugin that targets mcode 0.4.0+ must use the v0.4.0+ format. A Plugin that targets +mcode 0.3.10 / 0.3.11 must use the v0.3.x format. There is no cross-version shim; the +plugin manifest reader is strict and silently drops the Plugin on a shape mismatch. + +## Package shape + +``` +plugin-root/ +├── README.md (required by this community repository) +├── LICENSE (required by this community repository) +├── .claude-plugin/ +│ └── plugin.json (required, the manifest) +├── io.minimax.mcode/ (v0.4.0+ hook scripts, mirroring v0.3.x layout) +│ └── hooks/ +│ └── scripts/ +│ └── *.ps1 | *.mjs | *.sh (one script per event handler) +└── skills/ (top-level skills, see "Skills" below) + └── SKILL.md (one Skill is the typical minimum) +``` + +The `.claude-plugin/plugin.json` manifest is the new file. The top-level `plugin.json` from +the v0.3.x portable Agent Plugin 1.0 layout is **not** read by mcode 0.4.0+. Plugin authors +targeting mcode 0.4.0+ must move the manifest into `.claude-plugin/`. + +## Manifest schema + +The manifest is a single JSON object with the following fields: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | string | yes | Plugin name; lowercase letters, digits, single hyphens, no leading or trailing hyphen. | +| `version` | string | recommended | Semver. Bump major for any breaking manifest change. | +| `description` | string | recommended | One-sentence summary of what the Plugin does. | +| `author` | object | recommended | `{ name, url }`. Required disclosure in this community repository. | +| `license` | string | recommended | SPDX license identifier. | +| `homepage` | string | optional | Project URL. | +| `repository` | string | optional | VCS URL. | +| `keywords` | string[] | optional | Discovery tags. | +| `skills` | string \| string[] | optional | Path or paths to Skill files. See "Skills" below. | +| `mcpServers` | object | optional | MCP server definitions. Same shape as the Agent Plugins 1.0 `mcpServers`. | +| `hooks` | object | optional | Top-level hook definitions. See "Hooks" below. | + +`name` is the only strictly required field. `skills` and `mcpServers` together must expose at +least one Skill or MCP server (the v0.3.x "Skill or MCP server required" check still applies; +the validator enforces it on the v0.4.0+ layout too). + +Example manifest: + +```json +{ + "name": "hello-mcode-hooks", + "version": "0.1.0", + "description": "Example Plugin that observes every mcode lifecycle event.", + "author": { "name": "MCode Plugins contributors" }, + "license": "Apache-2.0", + "skills": ["./skills/SKILL.md"], + "hooks": { + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record-event.ps1" + ], + "timeout": 5 + } + ] + } + ] + } +} +``` + +## Skills + +The `skills` field, when present, is either a single path string or an array of path +strings. Each path is relative to the manifest's directory (i.e. to +`.claude-plugin/`) and uses the `./` prefix. Two valid layouts are: + +- **Top-level single Skill**: `skills/SKILL.md` at the Plugin root (one Skill, the typical + case for a focused Plugin). +- **Per-Skill subdirectory**: `skills//SKILL.md`, matching the v0.3.x layout. + mcode 0.4.0+ accepts both, but Plugin authors who have a single Skill should prefer the + top-level layout because it is shorter and matches the typical CLAUDE.md / SKILL.md + convention that mcode 0.4.0+ uses for its built-in skills. + +mcode 0.4.2 **silently drops** a Plugin whose `skills` field points into a nested +subdirectory (e.g. `["./skills/mcode-island/SKILL.md"]`) when the subdirectory's name does +not match the Skill frontmatter `name` field. The drop is silent: the Plugin does not +appear in the snapshot, no diagnostic is emitted, no hook fires. The Plugin author +sees the Plugin directory present on disk but the runtime acts as if it does not exist. +The empirical signature is `LOCAL_PLUGIN_NO_SUPPORTED_CAPABILITY` in +`scripts/snapshot-builder.ts:282-292` for the dropped Plugin. + +To avoid the silent drop, point `skills` at a path that the snapshot builder can resolve: + +- `./skills/SKILL.md` for a single-Skill Plugin, with the SKILL.md frontmatter `name` set + to the same string as the Plugin's manifest `name`. +- `./skills//SKILL.md` only when the subdirectory name matches the SKILL.md + frontmatter `name`. + +The validator (`scripts/validate.mjs`) currently scans for +`skills//SKILL.md` and rejects a Plugin that exposes zero Skills or MCP +servers. A Plugin that uses the top-level `skills/SKILL.md` layout must either also ship +a `skills//SKILL.md` copy for the validator, or the validator must be extended +to recognize the top-level layout. The recommended workaround for a Plugin that wants +the v0.4.0+ runtime behaviour and the current validator behaviour is to ship both +`skills/SKILL.md` (used by the runtime) and `skills//SKILL.md` (a byte-identical +copy under a subdirectory whose name matches the Plugin's `name`; used by the validator +until the validator is updated). See "Migration from v0.3.x" below for an example. + +## Hooks + +The `hooks` field, when present, is an object whose keys are PascalCase event names and +whose values are arrays of handler entries. The schema is the same as the v0.3.10+ nested +shape from PR #36: each event has an array of `{ matcher, hooks: [{ type, command, args, +timeout }] }` objects, with `hooks[]` carrying the per-handler execution entry. Inline +in the manifest replaces the v0.3.x external `io.minimax.mcode/hooks/hooks.json`. + +### Events + +mcode 0.4.0+ ships the same 12 PascalCase events that the v0.3.x spec documents: + +| Event | Default dispatch | Decision-bearing | +| --- | --- | --- | +| `PreToolUse` | per tool call | yes | +| `PostToolUse` | per tool call | no | +| `SessionStart` | per session resume | no | +| `SessionEnd` | per session terminate | no | +| `UserPromptSubmit` | per user turn | no | +| `Stop` | per turn / agent stop | no | +| `PreCompact` | before context compaction | no | +| `Notification` | per system notification | no | +| `SubagentStart` | per subagent start | no | +| `SubagentStop` | per subagent stop | no | +| `PermissionRequest` | before a permission decision | yes | +| `PermissionDenied` | after a denied permission | no | + +The closed-schema field vocabulary and the per-handler entry shape are identical to the +v0.3.x spec and are enforced by `scripts/lib/validation.mjs`. The validator runs +unchanged on the inline-hooks shape; the v0.4.0+ format is a layout change, not a +schema change. + +### Handler entry shape + +Each entry under `hooks.[]` has: + +```json +{ + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "powershell", + "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${PLUGIN_ROOT}/io.minimax.mcode/hooks/scripts/record-event.ps1"], + "timeout": 5 + } + ] +} +``` + +`command` is the executable token. On Windows, the value is typically `powershell` or +`pwsh` (or `node` for Node.js scripts). On POSIX, the value is typically `node` or +`sh`. `args` is an array of distinct process arguments; no shell interpretation is applied. +`timeout` is in seconds (the v0.3.x spec uses milliseconds; mcode 0.4.0+ accepts both +seconds and milliseconds but Plugin authors should pick one and stay consistent). + +### Hook script conventions + +Hook scripts receive one JSON event payload on stdin (one UTF-8 document, then EOF) and +write whatever side effects they want before exiting. The runtime does not parse the +payload; the script is free to ignore it. Convention from the v0.3.x spec carries over: + +- Paths must use `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` env var expansion, not + host-absolute paths. The runtime sets these to the Plugin's installation root and a + per-Plugin data directory respectively. +- State writes under `${PLUGIN_DATA}` should use the staging-file rename pattern + (`.staging` → ``) to avoid torn writes. +- Symlink and `..` containment should be enforced via `realpath`, not just `path.resolve`, + on Windows where short / long path mismatches can fool a lexical check. +- The exit code, when non-zero, is treated as "no opinion" and the runtime falls back to + its default behaviour. Observer hooks should swallow internal errors and exit 0. + +An example PowerShell hook script is in +`examples/hello-mcode-hooks-v04/io.minimax.mcode/hooks/scripts/record-event.ps1`. + +## Migration from v0.3.x + +A Plugin that targets both mcode 0.3.10/0.3.11 and mcode 0.4.0+ must ship two layouts in +parallel. The runtime selects the layout that matches its version; older runtimes read +`plugin.json` and `io.minimax.mcode/hooks/hooks.json`, newer runtimes read +`.claude-plugin/plugin.json`. + +The minimum-cost cross-version layout is: + +``` +plugin-root/ +├── README.md +├── LICENSE +├── plugin.json (v0.3.x manifest, Agent Plugins 1.0 schema) +├── .claude-plugin/ +│ └── plugin.json (v0.4.0+ manifest, inline hooks) +├── skills/ +│ ├── SKILL.md (top-level, for v0.4.0+ runtime) +│ └── / +│ └── SKILL.md (subdir copy, for v0.3.x runtime +│ and for the current validator) +└── io.minimax.mcode/ + └── hooks/ + ├── hooks.json (v0.3.x hook document, optional in v0.4.0+) + └── scripts/ + └── *.ps1 | *.mjs | *.sh (shared hook scripts) +``` + +A Plugin that targets mcode 0.4.0+ exclusively can drop `plugin.json` and +`io.minimax.mcode/hooks/hooks.json` and keep only the `.claude-plugin/plugin.json` +manifest. **The hosted validator still requires both a top-level `plugin.json` +targeting the Agent Plugins 1.0 schema and a `skills//SKILL.md` copy**, +because `scripts/validate.mjs` reads the top-level `plugin.json` directly +(`scripts/lib/validation.mjs:310`) and iterates `skills//` for Skill +discovery (`scripts/lib/validation.mjs:321-324`). The example +`examples/hello-mcode-hooks-v04/` ships the dual layout for that reason: the +top-level `plugin.json` is a v0.3.x-compatible shim that satisfies the +validator, and `.claude-plugin/plugin.json` is the authoritative v0.4.0+ +manifest. Once the validator is updated to read the v0.4.0+ layout, the +top-level shim and the subdir Skill copy can both be dropped. + +## Common pitfalls (lessons learned) + +These are the failure modes that bit mcode-island during the v0.3.x → v0.4.0+ migration. +Each one looks correct on inspection and only fails at runtime. + +1. **Skills path points into a nested subdirectory that does not match the SKILL.md + frontmatter `name` field.** The Plugin is silently dropped from the snapshot. The + fix is to either rename the subdirectory to match the frontmatter `name`, or move the + SKILL.md to the top level of `skills/`. + +2. **Manifest lives at `plugin.json` instead of `.claude-plugin/plugin.json`.** mcode + 0.4.0+ only reads `.claude-plugin/plugin.json`. The Plugin is not loaded at all. The + fix is to create the `.claude-plugin/` directory and move the manifest into it. + +3. **Hooks live at `io.minimax.mcode/hooks/hooks.json` only, with nothing in the + manifest.** mcode 0.4.0+ reads the `hooks` field from the inline manifest. The Plugin + has hooks defined for the older runtimes but the newer runtime sees no hooks at all. + The fix is to inline the `hooks` field in `.claude-plugin/plugin.json`. The + `io.minimax.mcode/hooks/hooks.json` file becomes optional and can be retained for + older-runtime compatibility. + +4. **Hook command uses host-absolute paths.** `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` are + the only portable path tokens. Hard-coded `C:\Users\...` or `/home/...` paths break + the moment the Plugin is installed somewhere else. + +5. **SKILL.md has no `name` field in the frontmatter, or the `name` does not match the + directory name.** mcode 0.3.x and earlier were lenient. mcode 0.4.0+ is strict. The + validator also rejects it. + +## Test evidence + +Verified on mcode 0.4.2 (Windows 11, PowerShell 5.1) as part of the mcode-island v1.0.0 +release. The empirical A/B test is the load-bearing evidence for the lessons above: + +| Configuration | Hook fire? | +| --- | --- | +| `skills: ["./skills/mcode-island"]` (deep subdir path) | no, silent drop | +| `skills: ["./skills"]` + top-level `skills/SKILL.md` | yes, all 12 events | + +The full commit history that produced this evidence is in the +`feat(mcode-island)!: rewrite plugin manifest for mcode 0.4.0+ runtime (v1.0.0)` commit +on the `proposal/mcode-island-0.4-hooks-rebuilt` branch. The companion PR is +[#38](https://github.com/MiniMax-AI/MiniMax-Code-Plugins/pull/38). + +## Open questions + +These block merging this proposal into the portable Agent Plugins 1.0 surface. + +- Confirm the top-level `skills/SKILL.md` layout is acceptable for portable + cross-runtime Plugins, or whether a separate per-Skill subdirectory is mandatory + for Agent Plugins 1.0. +- Confirm `hooks.timeout` units (seconds vs milliseconds) for the inline shape; the + current validator accepts both, but the portable shape should pin one. +- Decide whether the `io.minimax.mcode/hooks/hooks.json` file is deprecated in 0.4.0+ + or merely ignored. The empirical answer is "ignored"; the policy answer is + "deprecated for new Plugins, retained for cross-runtime compatibility". + +## Primary sources + +- [`@minimax-ai/code@0.4.0` CHANGELOG](https://www.npmjs.com/package/@minimax-ai/code?activeTab=code) — runtime release notes, 2026-09-11. +- [Agent Plugins 1.0 specification](https://agent-plugins.org/specification) — portable baseline. +- [Claude Code plugin manifest reference](https://docs.claude.com/en/docs/claude-code/plugins) — Claude Code plugin shape (the format mcode 0.4.0+ adopts). +- `proposals/hooks-detailed-spec.md` (commit `d86625d`, PR #20, PR #36) — v0.3.x spec this document supersedes for mcode 0.4.0+. +- [`docs/plugin-compatibility.md`](../docs/plugin-compatibility.md) — what this community repository advertises. +- [`docs/security-model.md`](../docs/security-model.md) — path safety and trust model. +- mcode-island v1.0.0 PR #38 — the empirical evidence base.