From d7111a4bf910d2085955c85f19320711f68865ab Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 00:49:17 +0000 Subject: [PATCH 1/3] fix(claude): re-pin hooks.schema.json to the documented handler contract and track the sub-agents frontmatter drift in the agents capability rows Fixes #477, Fixes #478 --- .../477-claude-hooks-schema-agents-rows.md | 5 + .../adapters/capabilities/claude-2.1.250.json | 64 +++- packages/agent-bundle/src/adapters/claude.ts | 2 +- .../src/adapters/hook-contract.ts | 10 +- .../adapters/schemas/claude/PROVENANCE.json | 7 +- .../adapters/schemas/claude/hooks.schema.json | 204 +++++++++-- .../tests/adapter-capability-states.test.ts | 35 +- .../tests/adapter-metadata.test.ts | 4 +- .../tests/claude-hooks-schema.test.ts | 223 ++++++++++++ .../cases/agent-missing-prompt.json | 13 + .../cases/agent-on-session-start.json | 14 + .../cases/all-documented-events.json | 334 ++++++++++++++++++ .../cases/allowed-env-vars-not-array.json | 15 + .../cases/args-not-strings.json | 17 + .../cases/args-on-http.json | 15 + .../cases/args-string.json | 15 + .../cases/async-on-http.json | 15 + .../cases/async-on-prompt.json | 15 + .../cases/async-rewake-on-mcp.json | 16 + .../cases/async-string.json | 15 + .../cases/continue-on-block-on-agent.json | 15 + .../cases/continue-on-block-on-command.json | 15 + .../cases/description-number.json | 15 + .../cases/empty-command.json | 14 + .../cases/empty-hooks-array.json | 9 + .../claude-hooks-schema/cases/full.json | 113 ++++++ .../cases/group-missing-hooks.json | 9 + .../cases/headers-not-object.json | 17 + .../cases/headers-value-number.json | 17 + .../cases/hooks-empty-object.json | 3 + .../cases/http-missing-url.json | 13 + .../cases/http-on-session-start.json | 14 + .../claude-hooks-schema/cases/if-number.json | 15 + .../cases/input-not-object.json | 16 + .../cases/matcher-number.json | 15 + .../cases/mcp-tool-missing-tool.json | 14 + .../cases/missing-command.json | 13 + .../cases/missing-type.json | 13 + .../cases/model-on-command.json | 15 + .../cases/no-hooks-key.json | 3 + .../cases/once-false-async-false.json | 17 + .../cases/once-string.json | 15 + .../cases/prompt-empty.json | 14 + .../cases/prompt-missing-prompt.json | 13 + .../cases/prompt-on-session-start.json | 14 + .../cases/prompt-on-subagent-start.json | 14 + .../cases/server-empty.json | 15 + .../cases/shell-invalid.json | 15 + .../cases/shell-on-mcp.json | 16 + .../cases/status-message-number.json | 15 + .../cases/timeout-float.json | 15 + .../cases/timeout-string.json | 15 + .../cases/timeout-zero.json | 15 + .../cases/unknown-event.json | 14 + .../cases/unknown-group-field.json | 15 + .../cases/unknown-handler-field.json | 15 + .../cases/unknown-top-level-field.json | 15 + .../cases/unknown-type.json | 14 + .../claude-hooks-schema/cases/url-empty.json | 14 + .../cases/url-on-command.json | 15 + .../reports/2.1.260/agent-missing-prompt.json | 27 ++ .../2.1.260/agent-on-session-start.json | 13 + .../2.1.260/all-documented-events.json | 13 + .../2.1.260/allowed-env-vars-not-array.json | 27 ++ .../reports/2.1.260/args-not-strings.json | 27 ++ .../reports/2.1.260/args-on-http.json | 13 + .../reports/2.1.260/args-string.json | 27 ++ .../reports/2.1.260/async-on-http.json | 13 + .../reports/2.1.260/async-on-prompt.json | 13 + .../reports/2.1.260/async-rewake-on-mcp.json | 13 + .../reports/2.1.260/async-string.json | 27 ++ .../2.1.260/continue-on-block-on-agent.json | 13 + .../2.1.260/continue-on-block-on-command.json | 13 + .../reports/2.1.260/description-number.json | 27 ++ .../reports/2.1.260/empty-command.json | 13 + .../reports/2.1.260/empty-hooks-array.json | 13 + .../reports/2.1.260/full.json | 13 + .../reports/2.1.260/group-missing-hooks.json | 27 ++ .../reports/2.1.260/headers-not-object.json | 27 ++ .../reports/2.1.260/headers-value-number.json | 27 ++ .../reports/2.1.260/hooks-empty-object.json | 13 + .../reports/2.1.260/http-missing-url.json | 27 ++ .../2.1.260/http-on-session-start.json | 13 + .../reports/2.1.260/if-number.json | 27 ++ .../reports/2.1.260/input-not-object.json | 27 ++ .../reports/2.1.260/matcher-number.json | 27 ++ .../2.1.260/mcp-tool-missing-tool.json | 27 ++ .../reports/2.1.260/missing-command.json | 27 ++ .../reports/2.1.260/missing-type.json | 27 ++ .../reports/2.1.260/model-on-command.json | 13 + .../reports/2.1.260/no-hooks-key.json | 27 ++ .../2.1.260/once-false-async-false.json | 13 + .../reports/2.1.260/once-string.json | 27 ++ .../reports/2.1.260/prompt-empty.json | 13 + .../2.1.260/prompt-missing-prompt.json | 27 ++ .../2.1.260/prompt-on-session-start.json | 13 + .../2.1.260/prompt-on-subagent-start.json | 13 + .../reports/2.1.260/server-empty.json | 13 + .../reports/2.1.260/shell-invalid.json | 27 ++ .../reports/2.1.260/shell-on-mcp.json | 13 + .../2.1.260/status-message-number.json | 27 ++ .../reports/2.1.260/timeout-float.json | 13 + .../reports/2.1.260/timeout-string.json | 27 ++ .../reports/2.1.260/timeout-zero.json | 27 ++ .../reports/2.1.260/unknown-event.json | 27 ++ .../reports/2.1.260/unknown-group-field.json | 13 + .../2.1.260/unknown-handler-field.json | 13 + .../2.1.260/unknown-top-level-field.json | 13 + .../reports/2.1.260/unknown-type.json | 27 ++ .../reports/2.1.260/url-empty.json | 27 ++ .../reports/2.1.260/url-on-command.json | 13 + .../record-claude-hooks-schema-fixtures.mjs | 82 +++++ website/docs/en/guide/authoring/index.mdx | 8 +- website/docs/zh/guide/authoring/index.mdx | 7 +- 114 files changed, 2775 insertions(+), 40 deletions(-) create mode 100644 .changeset/477-claude-hooks-schema-agents-rows.md create mode 100644 packages/agent-bundle/tests/claude-hooks-schema.test.ts create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/allowed-env-vars-not-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-not-strings.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-rewake-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-agent.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/description-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-hooks-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/full.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/group-missing-hooks.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-value-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/hooks-empty-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-missing-url.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/if-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/input-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/matcher-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/mcp-tool-missing-tool.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/no-hooks-key.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-false-async-false.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-subagent-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/server-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-invalid.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/status-message-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-float.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-zero.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-event.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-group-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-handler-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-top-level-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/allowed-env-vars-not-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-not-strings.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-rewake-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-agent.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/description-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-hooks-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/full.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/group-missing-hooks.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-value-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/hooks-empty-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-missing-url.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/if-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/input-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/matcher-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/mcp-tool-missing-tool.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/no-hooks-key.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-false-async-false.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-subagent-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/server-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-invalid.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/status-message-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-float.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-zero.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-event.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-group-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-handler-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-top-level-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-on-command.json create mode 100644 scripts/record-claude-hooks-schema-fixtures.mjs diff --git a/.changeset/477-claude-hooks-schema-agents-rows.md b/.changeset/477-claude-hooks-schema-agents-rows.md new file mode 100644 index 000000000..77e64a68f --- /dev/null +++ b/.changeset/477-claude-hooks-schema-agents-rows.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Re-pin the Claude Code `hooks.schema.json` to the documented hook handler contract so a `claude.nativeHooks` document may use every handler type (`command`, `http`, `mcp_tool`, `prompt`, `agent`), the per-type fields (`args`, `async`, `asyncRewake`, `shell`, `url`, `headers`, `allowedEnvVars`, `server`, `tool`, `input`, `prompt`, `model`, `continueOnBlock`), the common `if`, `once`, `statusMessage`, and `timeout` fields, and all 33 documented events, each closed to the handler types the reference allows for it; the compiler still emits shell-form `command` handlers only. Update the `claude` host's agents capability rows with `color`, `initialPrompt`, and `experimental.cacheTtl`, record that `permissionMode`, `mcpServers`, and `hooks` are ignored for plugin subagents, pin the anchored `^:$` `agent_type` matcher note, and bump the Claude `adapterRevision` to `1.26.0`. (#488) diff --git a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json index b56ee4ff5..ca5f38d16 100644 --- a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json +++ b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json @@ -119,17 +119,43 @@ "features": { "timeout": { "evidence": [ - "2026-09-03: the pinned schemas/claude/hooks.schema.json declares an integer `timeout` (minimum 1) on every command hook entry; the hook contract lowers `timeoutMs` to whole seconds." + "2026-09-03: the pinned schemas/claude/hooks.schema.json declares an integer `timeout` (minimum 1) on every handler type (hooks reference \"Common fields\": seconds before cancelling, not enforced on an async command hook); the hook contract lowers `timeoutMs` to whole seconds on the command handlers it emits." ], "state": "supported" }, "toolMatchers": { "evidence": [ - "2026-09-03: the pinned schemas/claude/hooks.schema.json declares a string `matcher` on every hook group, and hooks.matchers above pins the canonical tool selectors (file.read, file.write, mcp, shell) to Claude tool-name regexes; an unmapped selector fails the plan (claude.hook.tool.)." + "2026-09-03: the pinned schemas/claude/hooks.schema.json declares a string `matcher` on every hook group of every handler type, and hooks.matchers above pins the canonical tool selectors (file.read, file.write, mcp, shell) to Claude tool-name regexes; an unmapped selector fails the plan (claude.hook.tool.)." ], "state": "supported" } }, + "handlerContract": { + "types": ["agent", "command", "http", "mcp_tool", "prompt"], + "commonFields": ["if", "once", "statusMessage", "timeout", "type"], + "fields": { + "agent": ["model", "prompt"], + "command": ["args", "async", "asyncRewake", "command", "shell"], + "http": ["allowedEnvVars", "headers", "url"], + "mcp_tool": ["input", "server", "tool"], + "prompt": ["continueOnBlock", "model", "prompt"] + }, + "emitted": { "command": ["command", "timeout", "type"] }, + "evidence": [ + "uploaded 2026-09-03: Claude Code docs hooks reference (hooks-2.md, \"Hook handler fields\", \"Common fields\", \"Command hook fields\", \"Exec form and shell form\", \"HTTP hook fields\", \"MCP tool hook fields\", \"Prompt and agent hook fields\", \"Prompt-based hooks\", \"Agent hook configuration\", \"Run hooks in the background\") documents the five handler types, the common if/timeout/statusMessage/once fields, and the per-type fields pinned in schemas/claude/hooks.schema.json (#477); `async` and `args` belong to command hooks only, and agent hooks have no continueOnBlock.", + "2026-09-03: Claude Code 2.1.260 `claude plugin validate --strict --json` accepts a hooks/hooks.json using every documented type and field (tests/fixtures/claude-hooks-schema/cases/full.json) and rejects under --strict a missing per-type required field, an unknown handler type, an unknown event, and each wrong-typed field; it ignores unknown hooks.json keys and cross-type fields without a warning, which the closed pinned schema rejects instead (schemas/claude/PROVENANCE.json hooksSchemaNotes).", + "2026-09-03: the compiler emits only shell-form command handlers carrying type, command, and timeout (hook-contract planHooks); args (exec form, the reference's recommendation for path placeholders such as ${CLAUDE_PLUGIN_ROOT}), async, asyncRewake, shell, if, once, statusMessage, and the http/mcp_tool/prompt/agent handlers reach a bundle only through an authored `claude.nativeHooks` document, validated against the pinned schema and merged after the generated groups." + ] + }, + "agentTypeMatcher": { + "field": "agent_type", + "pluginScopedTemplate": "^:$", + "evidence": [ + "uploaded 2026-09-03: Claude Code docs hooks reference (hooks-2.md, \"Matcher patterns\" and \"SubagentStart\") — SubagentStart and SubagentStop match on agent type, and the example plugin-scoped value is `^my-plugin:reviewer$`; a matcher containing any character outside letters, digits, `_`, `-`, spaces, `,`, and `|` is evaluated as an unanchored JavaScript regular expression, so the colon of a plugin-scoped identifier requires `^` and `$` for a whole-string match.", + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Project-level hooks for subagent events\" and \"Supported frontmatter fields\") — the matcher value is the frontmatter `name` for project and user subagents and the plugin-scoped identifier such as `my-plugin:db-agent` for plugin subagents, anchored as `^my-plugin:db-agent$` to match only that agent; hooks receive `name` as `agent_type`, and a `name` containing `:` is not loaded (Claude Code 2.1.218 or later) because the colon is reserved for plugin-scoped identifiers.", + "2026-09-03: no agent/start or agent/stop route selector lowers an agent name yet (the agents component stays behind the #100 stage-2 G5 gate, #478); when one does, the Claude lowering must emit the anchored plugin-scoped form for agents this plugin ships, not the bare frontmatter name." + ] + }, "matchers": { "file.read": "^Read$", "file.write": "^(?:Write|Edit)$", @@ -225,6 +251,7 @@ "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent name field together with agents/ emission.", "evidence": [ "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents name as the agent identifier using lowercase letters and hyphens, while https://code.claude.com/docs/en/plugins-reference documents that a plugin agent without name falls back to its filename.", + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\" and \"Subagent files Claude Code skips\") — hooks receive name as agent_type; a name may not contain a colon, reserved for plugin-scoped identifiers such as my-plugin:reviewer, and a file whose name contains one is not loaded on Claude Code 2.1.218 or later; a plugin subagent with no name or unparseable frontmatter still loads under its filename, unlike project and user agents, which are skipped; the hook matcher for a plugin subagent is the anchored scoped form recorded in hooks.agentTypeMatcher (#478).", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." ] }, @@ -310,30 +337,57 @@ }, "hooks": { "state": "unavailable", - "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit hooks under the host security contract.", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would lower hooks as unsupported on Claude plugins: the sub-agents reference marks the field \"Ignored for plugin subagents\", so a re-admitted agents/ component must reject it with a diagnostic rather than emit a field the host drops silently.", "evidence": [ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that hooks is unsupported and ignored when an agent is loaded from a plugin.", + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks hooks \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring hooks without warning, proving the validator does not enforce the loader's ignore contract.", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] }, "mcpServers": { "state": "unavailable", - "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit mcpServers under the host security contract.", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would lower mcpServers as unsupported on Claude plugins: the sub-agents reference marks the field \"Ignored for plugin subagents\", so a re-admitted agents/ component must reject it with a diagnostic rather than emit a field the host drops silently.", "evidence": [ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that mcpServers is unsupported and ignored when an agent is loaded from a plugin.", + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks mcpServers \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring mcpServers without warning, proving the validator does not enforce the loader's ignore contract.", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] }, "permissionMode": { "state": "unavailable", - "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit permissionMode under the host security contract.", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would lower permissionMode as unsupported on Claude plugins: the sub-agents reference marks the field \"Ignored for plugin subagents\", so a re-admitted agents/ component must reject it with a diagnostic rather than emit a field the host drops silently.", "evidence": [ "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that permissionMode is unsupported and ignored when an agent is loaded from a plugin.", + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") marks permissionMode \"Ignored for [plugin subagents]\" in the frontmatter table (#478).", "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring permissionMode: bypassPermissions without warning, proving the validator does not enforce the loader's ignore contract.", "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." ] + }, + "color": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent color field together with agents/ emission.", + "evidence": [ + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents color as the display color for the subagent in the task list and transcript, accepting red, blue, green, yellow, purple, orange, pink, or cyan.", + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + ] + }, + "initialPrompt": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent initialPrompt field together with agents/ emission.", + "evidence": [ + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents initialPrompt as the text auto-submitted as the first user turn when the agent runs as the main session agent via --agent or the agent setting, with commands and skills processed and the text prepended to any user-provided prompt.", + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + ] + }, + "experimentalCacheTtl": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent experimental.cacheTtl field together with agents/ emission.", + "evidence": [ + "uploaded 2026-09-03: Claude Code docs sub-agents reference (sub-agents-3.md, \"Supported frontmatter fields\") documents experimental as a map whose cacheTtl key, 5m or 1h and written inside the experimental map, chooses the prompt cache lifetime for the subagent's requests; Claude Code ignores any other value, ignores 1h while a subscription is using usage credits, reads the field only from subagent files, and requires Claude Code 2.1.248 or later.", + "2026-09-03: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment; this row is contract bookkeeping from the uploaded sub-agents reference (#478), not enablement." + ] } }, "bin": { diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index 584ffcce2..7af391281 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -431,7 +431,7 @@ const hookContract = Object.freeze({ wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'), } satisfies TargetHookContract); const metadata = Object.freeze({ - adapterRevision: '1.25.0', + adapterRevision: '1.26.0', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); diff --git a/packages/agent-bundle/src/adapters/hook-contract.ts b/packages/agent-bundle/src/adapters/hook-contract.ts index 883110b6d..a42b78dfc 100644 --- a/packages/agent-bundle/src/adapters/hook-contract.ts +++ b/packages/agent-bundle/src/adapters/hook-contract.ts @@ -363,7 +363,12 @@ export const readTargetNativeHookCommands = ( } }; -/** Enumerates commands from the native Claude/Codex hook document shape. */ +/** + * Enumerates commands from the native Claude/Codex hook document shape. Only + * `command` handlers carry a command; the other handler types the pinned hooks + * schemas admit (`http`, `mcp_tool`, `prompt`, `agent`) are skipped, so a + * native document that mixes them still enumerates its command hooks. + */ export const readStandardNativeHookCommands = (document: unknown): TargetNativeHookCommandsReadResult => { if (!isPlainDataRecord(document)) return Object.freeze({ status: 'invalid' }); const hooks = ownDataValue(document, 'hooks'); @@ -382,9 +387,10 @@ export const readStandardNativeHookCommands = (document: unknown): TargetNativeH if (!isPlainDataRecord(hook)) return Object.freeze({ status: 'invalid' }); const command = ownDataValue(hook, 'command'); const type = ownDataValue(hook, 'type'); - if (command === undefined || type === undefined || !type.found || type.value !== 'command') { + if (command === undefined || type === undefined || !type.found || typeof type.value !== 'string') { return Object.freeze({ status: 'invalid' }); } + if (type.value !== 'command') continue; if (command.found && typeof command.value === 'string') { commands.push(Object.freeze({ command: command.value })); } else { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json index 389dd8299..02d499e4d 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json @@ -9,12 +9,13 @@ "schemaSource": "https://docs.anthropic.com/en/docs/claude-code/plugins", "notes": "lsp.schema.json and plugin.json's `lspServers` property were pinned from the Claude Code 2.1.x plugin reference LSP servers section (retrieved 2026-09-01), which documents `.lsp.json` at the plugin root or inline `lspServers` in the manifest, required `command` / `extensionToLanguage`, and the optional `args`, `transport`, `env`, `initializationOptions`, `settings`, `workspaceFolder`, `startupTimeout`, `shutdownTimeout`, `restartOnCrash`, `maxRestarts`, and `diagnostics` fields. `restartOnCrash` and `shutdownTimeout` require Claude Code v2.1.205 or later, which the pinned 2.1.250 revision satisfies. Manifest `lspServers` keeps the documented `string|array|object` union rather than being narrowed to the one emitted form the way `hooks` is; the emitted document itself is `.lsp.json` at the plugin root. Two agent-bundle tightenings over the documented text: a server map and an `extensionToLanguage` map must both be nonempty, because an empty map claims no extension and can never start a server. The current hooks reference at https://code.claude.com/docs/en/hooks supplies the SubagentStart/SubagentStop wire and decision evidence recorded in claude-2.1.250.json. settings.schema.json was pinned (retrieved 2026-09-01) from the \"Ship default settings with your plugin\" section of https://code.claude.com/docs/en/plugins and the file-locations row of https://code.claude.com/docs/en/plugins-reference, which bound the plugin-root settings.json to the `agent` and `subagentStatusLine` keys, plus https://code.claude.com/docs/en/statusline for the subagentStatusLine command-object shape. Three agent-bundle tightenings over the documented text: the closed schema rejects the unknown keys the host \"silently ignores\", so a requested default never disappears at runtime; minProperties 1 rejects an empty settings.json, which declares no default configuration at all; and subagentStatusLine admits only the two fields its own examples show (`type` and `command`) - statusLine's optional `padding` is documented for the user status line, never for the plugin default, so it stays out of the pinned shape. The plugins-reference placeholder table (\"Which fields substitute them inline depends on the plugin component\") lists Skill and agent content, hook and monitor commands, MCP servers, and LSP servers but not settings.json, so the adapter rejects Agent Bundle path tokens in settings values rather than emitting a placeholder Claude Code never resolves. plugin.json's `userConfig` property and closed `userConfigOption` definition were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle deliberately tightens the documented \"valid identifiers\" requirement to `^[A-Za-z_][A-Za-z0-9_]*$`, rejects option keys that collide after uppercasing because both would map to one `CLAUDE_PLUGIN_OPTION_` environment variable, requires the userConfig record to be nonempty, and rejects `sensitive: true` with `default` because a baked-in secure-storage default would ship a secret in the manifest. plugin.schema.json's `dependencies` property was pinned (retrieved 2026-09-01) from https://code.claude.com/docs/en/plugin-dependencies and the manifest schema in https://code.claude.com/docs/en/plugins-reference: a nonempty array whose entries are nonempty plugin-name strings or closed objects with required name and optional version and marketplace strings. Agent Bundle tightens dependency names to the manifest's existing lowercase kebab-case name pattern, rejects an empty array, and closes object fields so malformed declarations fail before distribution; semver range grammar remains plan-time validation because JSON Schema cannot honestly encode npm range syntax. plugin.schema.json's `displayName`, `metadata`, and `defaultEnabled` properties were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle tightens Claude Code's warning-and-ignore handling for a non-object `metadata` value into build error claude.manifest.metadata.invalid, rejects an empty or whitespace-only `displayName` with claude.manifest.displayName.invalid, and rejects a non-boolean `defaultEnabled` with claude.manifest.defaultEnabled.invalid. The normalized generic model currently carries description but not homepage, repository, license, keywords, or `$schema`, so this slice deliberately emits only the three new Claude host-config fields and does not widen the generic model. Component path fields are deliberately excluded from the emitted schema and config surface: the generator owns the canonical default commands/, skills/, hooks/hooks.json, .mcp.json, .lsp.json, settings.json, workflows/, and output-styles/ layout, while custom replace/add path rules remain documented host-discovery evidence in claude-2.1.250.json. plugin.json's `channels` property was pinned from the Channels section of https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01): a nonempty array of closed declarations with required nonempty `server` and optional per-channel `userConfig` reusing the top-level option definition. Agent Bundle tightens the documented contract by rejecting an empty channels array, empty per-channel userConfig, unknown channel fields, invalid or uppercase-colliding option identifiers, and any server name absent from the MCP server map successfully planned for the selected target. Duplicate channel declarations for one server remain allowed and preserve authored order because the reference imposes no uniqueness rule. Per-channel `sensitive: true` follows the top-level secure-storage semantics because the reference says the channel field uses the same schema; the existing prohibition on sensitive defaults therefore applies unchanged. Claude Code 2.1.257 strict validation accepts both valid bindings and deliberately dangling server names, so claude.channels.server.unknown is an intentional compiler tightening and the only pre-enable cross-document binding guard. Workflows and output styles deliberately reuse the bin slice's normalized directory/file payload shape and byte-faithful copy entries, but retain separate registry hooks, normalized fields, destination paths, and diagnostics so the executable policy cannot leak into non-executable components. The shared enumerator preserves source file modes through copy entries and rejects lexical or realpath escapes, including a configured directory symlink that resolves outside the project. The adapter emits only the canonical plugin-root workflows/ and output-styles/ directories, so it does not emit the optional `workflows` or `outputStyles` manifest path fields and leaves plugin.schema.json plus its SHA-256 pin unchanged. https://code.claude.com/docs/en/output-styles (retrieved 2026-09-01) explicitly defines output styles as Markdown, so Agent Bundle tightens the directory to `.md` files with claude.outputStyles.file.invalid. It does not validate frontmatter: `name` is optional because the filename supplies it, other documented fields are optional, and Claude Code 2.1.257 strict plugin validation accepts a Markdown output style with no frontmatter. The plugins reference gives workflow scripts no deeper file schema, so workflow file contents and suffixes remain opaque. monitors.schema.json and theme.schema.json were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01), which marks both components experimental and warns their manifest schema may change between releases. The monitor schema is a nonempty array of closed entries requiring nonempty unique name, command, and description; optional when admits only always or on-skill-invoke:, and the plan tightens the latter by requiring the named skill to be emitted by this plugin. The schema and plan both reject ${user_config.*} in monitor commands because Claude Code rejects it at shell execution time and supplies no CLAUDE_PLUGIN_OPTION_ variables to monitors. The theme schema is closed, requires a nonempty base, and admits optional nonempty name plus a sparse string-valued overrides map. Agent Bundle further requires a nonempty themes declaration, safe file-stem keys, defaults omitted name to that key, and rejects empty override strings; it deliberately accepts any nonempty color string because the reference shows hex examples but does not mandate hex syntax. Generated themes and monitors use the default locations, so no unstable experimental.* manifest fields are emitted. Local proof against Claude Code 2.1.257 shows strict validation accepts valid emitted themes and monitors but does not inspect either default-location document's contents: malformed themes missing base and carrying non-string overrides pass, as do monitors missing command. The pinned schemas and compiler diagnostics are therefore the content guard. The same strict host rejects the deprecated top-level monitors manifest key, so default-location emission also avoids the warning-to-error migration path. The artifact contract records themes/*.json as a schema family and validates every concrete generated theme path. marketplace.schema.json was re-pinned from https://code.claude.com/docs/en/plugin-marketplaces, https://code.claude.com/docs/en/plugins-reference, https://code.claude.com/docs/en/plugin-dependencies, and the linked relevance contract (retrieved 2026-09-02). It loosens the old generated-only snapshot by making top-level description and plugin-entry description/version optional, allowing documented owner email/url, top-level $schema/version/metadata/renames/allowCrossMarketplaceDependenciesOn, and every documented plugin-entry metadata, component, relevance, authentication, dependency, channel, userConfig, settings, and experimental field; plugin-entry version is now any nonempty documented version string rather than generated-only semver. It deliberately retains only the relative ./ source form: github, git URL, git-subdir, npm, archive, and command sources are deferred to the source-matrix follow-up. The authored overlay is narrower than the validation schema and does not expose plugin name/source or component routing, because generated identity/layout remains authoritative and the agents component remains gate-deferred. Agent Bundle tightens the prose contract by closing every structured object (free-form metadata uses recursive patternProperties with additionalProperties false), requiring nonempty arrays/maps where an empty declaration has no effect, requiring unique kebab-case cross-marketplace allowlist names and kebab-case rename keys/targets, rejecting the documented exact reserved marketplace names, requiring relative sources and component paths to stay inside the marketplace, limiting owner/author URLs to absolute HTTP(S), requiring metadata.pluginRoot to be an internal ./ path, requiring nonempty header names/values, and requiring explicit strict:false with headersHelper. Relevance enforces the documented topic, item-count, character-count, bare-hostname, and regular-expression bounds; Claude Code 2.1.257 strict validation does not reject an empty allowCrossMarketplaceDependenciesOn entry. The same native validator warns that headers and headersHelper have no effect on the generated relative source and promotes that warning to failure under --strict, so the authored overlay rejects both fields with claude.marketplace.plugin.headers.inapplicable and claude.marketplace.plugin.headersHelper.inapplicable rather than emitting a manifest the host's strict validation refuses; the pinned schema keeps the documented fields, including the strict:false requirement beside headersHelper, for the URL-capable source-matrix follow-up.", "sourceMatrixNotes": "Source-matrix addendum (retrieved 2026-09-02): marketplace.schema.json now admits the documented relative, github, git URL, git-subdir, npm, archive, and command plugin sources as closed alternatives. Agent Bundle tightens relative paths and git-subdir paths against lexical escapes; GitHub shorthand to one owner/repo pair; git URLs to HTTPS or git@ forms; npm names to npm's lowercase package grammar and npm versions to exact, ^, or ~ semantic versions; registry URLs to HTTP(S); archive URLs to HTTPS with lexical rejection for localhost, IPv4/IPv6 loopback, 169.254/16 and fe80::/10 link-local addresses, and known AWS, Azure, and Google metadata hostnames; git sha to 40 hex; archive sha256 to 64 case-insensitive hex; command text to 1-500 printable ASCII characters without four-space runs; timeout to integer seconds from 1 through 600; and mode to copy or link. Entry headers remain a nonempty record of nonempty string values, and headersHelper requires a nonempty command plus strict:false. These checks do not resolve DNS, follow redirects, contact registries or repositories, download archives, verify archive bytes, execute command sources, inspect produced directories, or attempt to predict Windows link-mode support; those remain native install-time behavior.", - "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment.", + "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment. Drift addendum (#478, uploaded 2026-09-03 sub-agents reference sub-agents-3.md, \"Supported frontmatter fields\"): the contract gained color (red, blue, green, yellow, purple, orange, pink, cyan), initialPrompt (main-session agents via --agent or the agent setting), and experimental.cacheTtl (5m or 1h, Claude Code 2.1.248 or later, read only from subagent files); permissionMode, mcpServers, and hooks are each documented as \"Ignored for plugin subagents\"; name may not contain a colon (reserved for plugin-scoped identifiers, unloaded since 2.1.218) and hooks receive it as agent_type. \"Project-level hooks for subagent events\" and the hooks reference's SubagentStart matcher row give the plugin-scoped matcher form ^:$, anchored because the colon puts it on the unanchored regular-expression path. claude-2.1.250.json plugin.agents and hooks.agentTypeMatcher record these rows; the component stays deferred.", "hooksManifestNotes": "Manifest `hooks` addendum (2026-09-03, Claude Code plugins reference \"Plugin manifest schema\" > \"Component path fields\" and \"Plugin directory structure\" > \"File locations reference\"; #462/#463, adapter fix in #470): the default hooks location is `hooks/hooks.json`, which Claude Code loads on its own, and the manifest `hooks` field (string|array|object) is documented as \"Hook config paths or inline config\" with the example `./my-extra-hooks.json`, i.e. additional hook files. Claude Code 2.1.250, 2.1.251, 2.1.257, and 2.1.259 (`claude --plugin-dir plugin list --json`, observed 2026-09-03) all refuse a plugin whose manifest names the auto-loaded file again with `errors: [\"Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json. The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files.\"]`; `claude plugin validate --strict` and `claude plugin details` accept that manifest, so only the list verb exposes the refusal. plugin.schema.json therefore admits the documented `./`-prefixed path, path array, and inline object forms for `hooks` but rejects the literal `./hooks/hooks.json`, and the Claude and unified `plugin` adapters emit the hook document at the default location without a manifest `hooks` key (#470). The same 2.1.259 binary lists exactly one hook set for a plugin shipping `hooks/hooks.json` beside `hooks/hooks-cursor.json` and no manifest `hooks` key, so the unified bundle needs no pointer to keep Claude Code off the Cursor document.", + "hooksSchemaNotes": "hooks.schema.json was re-pinned (uploaded 2026-09-03 hooks reference hooks-2.md, #477) from the single shell-form command handler to the documented handler union: \"Hook handler fields\" names five types (command, http, mcp_tool, prompt, agent); \"Common fields\" gives every type if, timeout, statusMessage, and once; \"Command hook fields\" adds command, args (exec form), async, asyncRewake, and shell (bash or powershell); \"HTTP hook fields\" adds url, headers, and allowedEnvVars; \"MCP tool hook fields\" adds server, tool, and input; \"Prompt and agent hook fields\" adds prompt and model, with continueOnBlock on prompt hooks only (\"Agent hook configuration\": agent hooks have no continueOnBlock field). Each handler carries the documented required fields (command; url; server and tool; prompt) and is closed. The `hooks` map is closed to the 33 events under \"Hook events\", and \"Prompt-based hooks\" partitions them: 13 events admit all five types, 18 admit command, http, and mcp_tool, and SessionStart and Setup admit command and mcp_tool only. Cross-check against Claude Code 2.1.260 `claude plugin validate --strict --json` (tests/fixtures/claude-hooks-schema, recorded by scripts/record-claude-hooks-schema-fixtures.mjs): the CLI accepts a document using every type and field, knows all 33 events, and rejects under --strict an unknown event, an unknown handler type, a missing type, each missing required field, and each wrong-typed field (timeout 0 or a string, shell other than bash/powershell, non-string args, non-object headers or input, non-array allowedEnvVars, non-boolean async/once, non-string if/statusMessage/matcher, a non-string description, and a document without `hooks`), all of which the schema rejects too. Agent Bundle tightenings the CLI does not share: unknown keys on the document, a matcher group, or a handler are rejected (plugins-reference \"plugin validate\" documents unrecognized manifest fields as warnings that --strict promotes to errors, but 2.1.260 --strict ignores unknown hooks.json keys silently, so the closed pin is the only guard against a misspelled field); a field of another handler type (args or async on http, continueOnBlock on agent, model or url on command, shell or asyncRewake on mcp_tool) is rejected rather than silently ignored; a prompt, agent, or http handler on an event the reference excludes it from is rejected although the CLI accepts it; and the pre-existing minLength 1 on command (now also server, tool, prompt, url, model), integer timeout (the hook contract lowers timeoutMs to whole seconds; the CLI accepts 1.5), and minItems 1 on a group's hooks array stay, where the CLI accepts empty strings, fractional timeouts, and empty groups. The undocumented top-level `modules` key the 2.1.260 validator mentions is not admitted. The compiler still emits only shell-form command handlers with command and timeout; exec form, async, if, and statusMessage are pass-through fields for authored nativeHooks documents until a separate emission increment.", "schemas": { "hooks.schema.json": { - "bytes": 1108, - "sha256": "3c6f3e4391f3dca939d75bd0b200ea88e68db939a2cb885d46f0b143293efb84", + "bytes": 7229, + "sha256": "70cd71b42bf9f9e03e86dcffb82b50038f92605baadd2fbc12dda488502d5cd7", "url": "https://code.claude.com/docs/en/hooks" }, "lsp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json index 539e08570..f14732c15 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json @@ -1,36 +1,192 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://agent-bundle.dev/schemas/claude/2.1.250/hooks.schema.json", + "$defs": { + "commandHandler": { + "type": "object", + "additionalProperties": false, + "properties": { + "args": { "type": "array", "items": { "type": "string" } }, + "async": { "type": "boolean" }, + "asyncRewake": { "type": "boolean" }, + "command": { "type": "string", "minLength": 1 }, + "if": { "type": "string" }, + "once": { "type": "boolean" }, + "shell": { "enum": ["bash", "powershell"] }, + "statusMessage": { "type": "string" }, + "timeout": { "type": "integer", "minimum": 1 }, + "type": { "const": "command" } + }, + "required": ["type", "command"] + }, + "httpHandler": { + "type": "object", + "additionalProperties": false, + "properties": { + "allowedEnvVars": { "type": "array", "items": { "type": "string" } }, + "headers": { "type": "object", "additionalProperties": { "type": "string" } }, + "if": { "type": "string" }, + "once": { "type": "boolean" }, + "statusMessage": { "type": "string" }, + "timeout": { "type": "integer", "minimum": 1 }, + "type": { "const": "http" }, + "url": { "type": "string", "minLength": 1 } + }, + "required": ["type", "url"] + }, + "mcpToolHandler": { + "type": "object", + "additionalProperties": false, + "properties": { + "if": { "type": "string" }, + "input": { "type": "object" }, + "once": { "type": "boolean" }, + "server": { "type": "string", "minLength": 1 }, + "statusMessage": { "type": "string" }, + "timeout": { "type": "integer", "minimum": 1 }, + "tool": { "type": "string", "minLength": 1 }, + "type": { "const": "mcp_tool" } + }, + "required": ["type", "server", "tool"] + }, + "promptHandler": { + "type": "object", + "additionalProperties": false, + "properties": { + "continueOnBlock": { "type": "boolean" }, + "if": { "type": "string" }, + "model": { "type": "string", "minLength": 1 }, + "once": { "type": "boolean" }, + "prompt": { "type": "string", "minLength": 1 }, + "statusMessage": { "type": "string" }, + "timeout": { "type": "integer", "minimum": 1 }, + "type": { "const": "prompt" } + }, + "required": ["type", "prompt"] + }, + "agentHandler": { + "type": "object", + "additionalProperties": false, + "properties": { + "if": { "type": "string" }, + "model": { "type": "string", "minLength": 1 }, + "once": { "type": "boolean" }, + "prompt": { "type": "string", "minLength": 1 }, + "statusMessage": { "type": "string" }, + "timeout": { "type": "integer", "minimum": 1 }, + "type": { "const": "agent" } + }, + "required": ["type", "prompt"] + }, + "allHandlerGroups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "matcher": { "type": "string" }, + "hooks": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { "$ref": "#/$defs/commandHandler" }, + { "$ref": "#/$defs/httpHandler" }, + { "$ref": "#/$defs/mcpToolHandler" }, + { "$ref": "#/$defs/promptHandler" }, + { "$ref": "#/$defs/agentHandler" } + ] + } + } + }, + "required": ["hooks"] + } + }, + "nonModelHandlerGroups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "matcher": { "type": "string" }, + "hooks": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { "$ref": "#/$defs/commandHandler" }, + { "$ref": "#/$defs/httpHandler" }, + { "$ref": "#/$defs/mcpToolHandler" } + ] + } + } + }, + "required": ["hooks"] + } + }, + "startupHandlerGroups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "matcher": { "type": "string" }, + "hooks": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { "$ref": "#/$defs/commandHandler" }, + { "$ref": "#/$defs/mcpToolHandler" } + ] + } + } + }, + "required": ["hooks"] + } + } + }, "type": "object", "additionalProperties": false, "properties": { "description": { "type": "string" }, "hooks": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "matcher": { "type": "string" }, - "hooks": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "command": { "type": "string", "minLength": 1 }, - "timeout": { "type": "integer", "minimum": 1 }, - "type": { "const": "command" } - }, - "required": ["type", "command"] - } - } - }, - "required": ["hooks"] - } + "additionalProperties": false, + "properties": { + "ConfigChange": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "CwdChanged": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "DirectoryAdded": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "Elicitation": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "ElicitationResult": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "FileChanged": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "InstructionsLoaded": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "MessageDisplay": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "Notification": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PermissionDenied": { "$ref": "#/$defs/allHandlerGroups" }, + "PermissionRequest": { "$ref": "#/$defs/allHandlerGroups" }, + "PostCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PostModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PostToolBatch": { "$ref": "#/$defs/allHandlerGroups" }, + "PostToolUse": { "$ref": "#/$defs/allHandlerGroups" }, + "PostToolUseFailure": { "$ref": "#/$defs/allHandlerGroups" }, + "PreCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PreModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "PreToolUse": { "$ref": "#/$defs/allHandlerGroups" }, + "SessionEnd": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "SessionStart": { "$ref": "#/$defs/startupHandlerGroups" }, + "Setup": { "$ref": "#/$defs/startupHandlerGroups" }, + "Stop": { "$ref": "#/$defs/allHandlerGroups" }, + "StopFailure": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "SubagentStart": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "SubagentStop": { "$ref": "#/$defs/allHandlerGroups" }, + "TaskCompleted": { "$ref": "#/$defs/allHandlerGroups" }, + "TaskCreated": { "$ref": "#/$defs/allHandlerGroups" }, + "TeammateIdle": { "$ref": "#/$defs/allHandlerGroups" }, + "UserPromptExpansion": { "$ref": "#/$defs/allHandlerGroups" }, + "UserPromptSubmit": { "$ref": "#/$defs/allHandlerGroups" }, + "WorktreeCreate": { "$ref": "#/$defs/nonModelHandlerGroups" }, + "WorktreeRemove": { "$ref": "#/$defs/nonModelHandlerGroups" } } } }, diff --git a/packages/agent-bundle/tests/adapter-capability-states.test.ts b/packages/agent-bundle/tests/adapter-capability-states.test.ts index 1dfb22c3e..6b3d67bec 100644 --- a/packages/agent-bundle/tests/adapter-capability-states.test.ts +++ b/packages/agent-bundle/tests/adapter-capability-states.test.ts @@ -403,11 +403,14 @@ it('reports Claude plugin settings support and honest unavailable composite cove const claudeAgentCapabilityRows = { background: 'agents.background', + color: 'agents.color', component: 'agents', description: 'agents.description', disallowedTools: 'agents.disallowedTools', effort: 'agents.effort', + experimentalCacheTtl: 'agents.experimentalCacheTtl', hooks: 'agents.hooks', + initialPrompt: 'agents.initialPrompt', isolationWorktree: 'agents.isolationWorktree', maxTurns: 'agents.maxTurns', mcpServers: 'agents.mcpServers', @@ -447,7 +450,9 @@ it('records dated unavailable Claude agent rows and mirrors them through the uni expect(row.reason).toContain('PR #220'); expect(row.reason).toContain('#107 revision 3'); expect(row.evidence.length).toBeGreaterThan(0); - expect(row.evidence.every((line) => line.startsWith('retrieved 2026-09-02:'))).toBe(true); + // Dated evidence: the 2026-09-02 web retrieval, the maintainer-uploaded + // 2026-09-03 references (sub-agents-3.md, #478), or same-day repository facts. + expect(row.evidence.every((line) => /^(?:retrieved 2026-09-02|uploaded 2026-09-03|2026-09-03):/u.test(line))).toBe(true); expect(registry.get('claude').capabilities[capability]).toEqual({ reason: row.reason, state: 'unavailable', @@ -471,6 +476,34 @@ it('records dated unavailable Claude agent rows and mirrors them through the uni } }); +it('tracks the uploaded sub-agents contract in the Claude agent rows without enabling the component (#478)', () => { + const agents = claudeCapabilityTable.plugin.agents; + const uploaded = (line: string): boolean => line.startsWith('uploaded 2026-09-03:') && line.includes('sub-agents-3.md'); + + // New frontmatter fields: still unavailable behind the G5 gate, cited to the uploaded reference. + expect(agents.color.evidence.some((line) => uploaded(line) && /red, blue, green, yellow, purple, orange, pink, or cyan/u.test(line))).toBe(true); + expect(agents.initialPrompt.evidence.some((line) => uploaded(line) && line.includes('--agent'))).toBe(true); + expect(agents.experimentalCacheTtl.evidence.some((line) => uploaded(line) && line.includes('5m or 1h') && line.includes('2.1.248'))).toBe(true); + expect(agents.experimentalCacheTtl.reason).toContain('experimental.cacheTtl'); + + // Fields the host ignores for plugin subagents: the reason names the ignore, not merely a deferral. + for (const field of ['hooks', 'mcpServers', 'permissionMode'] as const) { + expect(agents[field].reason).toContain('Ignored for plugin subagents'); + expect(agents[field].reason).toContain('diagnostic'); + expect(agents[field].evidence.some((line) => uploaded(line) && line.includes(field))).toBe(true); + } + + // Name constraints and the anchored plugin-scoped agent_type matcher. + expect(agents.name.evidence.some((line) => uploaded(line) && line.includes('agent_type') && line.includes('colon'))).toBe(true); + const matcher = claudeCapabilityTable.hooks.agentTypeMatcher; + expect(matcher.field).toBe('agent_type'); + expect(matcher.pluginScopedTemplate).toBe('^:$'); + expect(matcher.evidence.some((line) => line.startsWith('uploaded 2026-09-03:') && line.includes('hooks-2.md') && line.includes('^my-plugin:reviewer$'))).toBe(true); + expect(matcher.evidence.some((line) => uploaded(line) && line.includes('^my-plugin:db-agent$'))).toBe(true); + // The tool-selector map the hook contract lowers stays untouched by the note. + expect(Object.keys(claudeCapabilityTable.hooks.matchers).sort()).toEqual(['file.read', 'file.write', 'mcp', 'shell']); +}); + it('records the dated G5-gated Cursor agents component row beside the documented Cursor agents format', () => { const registry = createDefaultRegistry(); const row = cursorCapabilityTable.plugin.agents.component; diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index f3dfb7170..afc90b339 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -99,13 +99,13 @@ it('records exact immutable metadata for every built-in target', () => { ], }); expect(registryMetadata(registry, 'claude')).toEqual({ - adapterRevision: '1.25.0', + adapterRevision: '1.26.0', observedVersion: '2.1.250', schemas: [ { name: 'hooks', revision: '2.1.250', - sha256: '3c6f3e4391f3dca939d75bd0b200ea88e68db939a2cb885d46f0b143293efb84', + sha256: '70cd71b42bf9f9e03e86dcffb82b50038f92605baadd2fbc12dda488502d5cd7', }, { name: 'lsp', diff --git a/packages/agent-bundle/tests/claude-hooks-schema.test.ts b/packages/agent-bundle/tests/claude-hooks-schema.test.ts new file mode 100644 index 000000000..d72b93f47 --- /dev/null +++ b/packages/agent-bundle/tests/claude-hooks-schema.test.ts @@ -0,0 +1,223 @@ +import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { expect, it } from '@rstest/core'; + +import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; +import { readStandardNativeHookCommands } from '../src/adapters/hook-contract.ts'; +import { createDefaultRegistry } from '../src/adapters/registry.ts'; +import hooksSchema from '../src/adapters/schemas/claude/hooks.schema.json' with { type: 'json' }; +import schemaProvenance from '../src/adapters/schemas/claude/PROVENANCE.json' with { type: 'json' }; +import { createAdapterValidator } from '../src/adapters/types.ts'; +import { normalizeProject } from '../src/config/normalize.ts'; +import type { LoadedConfig } from '../src/config/load.ts'; + +const fixtureRoot = new URL('./fixtures/claude-hooks-schema/', import.meta.url); +/** The Claude CLI whose `plugin validate --strict --json` verdicts are recorded beside the cases. */ +const recordedCliVersion = '2.1.260'; +const validate = createAdapterValidator().compile(hooksSchema); + +interface RecordedReport { + readonly contents: readonly { + readonly errors: readonly { readonly message: string }[]; + readonly file: string; + readonly warnings: readonly { readonly message: string }[]; + }[]; + readonly strict: boolean; + readonly success: boolean; +} + +const readJson = async (path: string): Promise => JSON.parse(await readFile(new URL(path, fixtureRoot), 'utf8')) as T; +const caseNames = async (): Promise => + (await readdir(new URL('./cases/', fixtureRoot))).filter((name) => name.endsWith('.json')).map((name) => name.slice(0, -'.json'.length)).sort(); + +/** + * Cases the pinned schema rejects although Claude Code accepts them: each is a + * deliberate agent-bundle tightening over the documented contract, recorded in + * PROVENANCE.json. Anything else must agree with the recorded CLI verdict. + */ +const deliberateTightenings: Readonly> = { + 'agent-on-session-start': 'hooks reference "Prompt-based hooks": SessionStart supports command and mcp_tool handlers only; the validator does not enforce per-event handler support.', + 'args-on-http': 'closed per-type handler shape: `args` is a command-hook field; Claude silently ignores it on an http handler.', + 'async-on-http': 'closed per-type handler shape: `async` is a command-hook field ("Run hooks in the background": only available on type command).', + 'async-on-prompt': 'closed per-type handler shape: `async` is a command-hook field.', + 'async-rewake-on-mcp': 'closed per-type handler shape: `asyncRewake` is a command-hook field.', + 'continue-on-block-on-agent': 'hooks reference "Agent hook configuration": agent hooks have no continueOnBlock field.', + 'continue-on-block-on-command': 'closed per-type handler shape: `continueOnBlock` is a prompt-hook field.', + 'empty-command': 'an empty command string names no executable; minLength 1 predates this pin.', + 'empty-hooks-array': 'a matcher group with no handlers registers nothing; minItems 1 predates this pin.', + 'http-on-session-start': 'hooks reference "Prompt-based hooks": SessionStart does not support http handlers.', + 'model-on-command': 'closed per-type handler shape: `model` is a prompt/agent-hook field.', + 'prompt-empty': 'an empty prompt sends the model nothing to evaluate.', + 'prompt-on-session-start': 'hooks reference "Prompt-based hooks": SessionStart does not support prompt handlers.', + 'prompt-on-subagent-start': 'hooks reference "Prompt-based hooks": SubagentStart supports command, http and mcp_tool handlers but not prompt or agent.', + 'server-empty': 'an empty server name can never resolve to a connected MCP server.', + 'shell-on-mcp': 'closed per-type handler shape: `shell` is a command-hook field.', + 'timeout-float': 'the hook contract lowers timeoutMs to whole seconds; `timeout` stays an integer as it was before this pin.', + 'unknown-group-field': 'closed matcher-group shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning, so the pin is the only guard against a misspelled field.', + 'unknown-handler-field': 'closed handler shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning.', + 'unknown-top-level-field': 'closed document shape: Claude Code 2.1.260 --strict ignores unknown hooks.json keys without a warning.', + 'url-on-command': 'closed per-type handler shape: `url` is an http-hook field.', +}; + +const documentedEvents = [ + 'ConfigChange', 'CwdChanged', 'DirectoryAdded', 'Elicitation', 'ElicitationResult', 'FileChanged', 'InstructionsLoaded', + 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', 'PostCompact', 'PostModelSwitch', 'PostToolBatch', + 'PostToolUse', 'PostToolUseFailure', 'PreCompact', 'PreModelSwitch', 'PreToolUse', 'SessionEnd', 'SessionStart', 'Setup', 'Stop', + 'StopFailure', 'SubagentStart', 'SubagentStop', 'TaskCompleted', 'TaskCreated', 'TeammateIdle', 'UserPromptExpansion', + 'UserPromptSubmit', 'WorktreeCreate', 'WorktreeRemove', +] as const; +/** hooks reference "Prompt-based hooks": the events that support all five handler types. */ +const allHandlerEvents = [ + 'PermissionDenied', 'PermissionRequest', 'PostToolBatch', 'PostToolUse', 'PostToolUseFailure', 'PreToolUse', 'Stop', + 'SubagentStop', 'TaskCompleted', 'TaskCreated', 'TeammateIdle', 'UserPromptExpansion', 'UserPromptSubmit', +] as const; +const startupEvents = ['SessionStart', 'Setup'] as const; + +const handlerGroup = (handler: Readonly>): unknown => [{ hooks: [handler] }]; + +it('agrees with the recorded Claude CLI verdict on every fixture except the recorded tightenings', async () => { + const names = await caseNames(); + expect(names.length).toBeGreaterThan(40); + expect(await readdir(new URL('./reports/', fixtureRoot))).toEqual([recordedCliVersion]); + for (const tightening of Object.keys(deliberateTightenings)) expect(names).toContain(tightening); + + for (const name of names) { + const document = await readJson(`./cases/${name}.json`); + const report = await readJson(`./reports/${recordedCliVersion}/${name}.json`); + expect(report.strict, name).toBe(true); + const accepted = validate(document); + if (Object.hasOwn(deliberateTightenings, name)) { + expect(report.success, `${name}: a tightening must be a case Claude accepts`).toBe(true); + expect(accepted, `${name}: the schema must reject what the tightening describes`).toBe(false); + continue; + } + expect(accepted, `${name}: schema=${accepted} claude=${report.success} ${JSON.stringify(validate.errors ?? report.contents)}`) + .toBe(report.success); + } +}); + +it('accepts every documented handler type and field, and every documented event, in one document', async () => { + const full = await readJson<{ readonly hooks: Record }>('./cases/full.json'); + expect(validate(full), JSON.stringify(validate.errors)).toBe(true); + const handlers = Object.values(full.hooks).flatMap((groups) => + (groups as readonly { readonly hooks: readonly Record[] }[]).flatMap((group) => group.hooks)); + expect([...new Set(handlers.map((handler) => handler.type))].sort()).toEqual(['agent', 'command', 'http', 'mcp_tool', 'prompt']); + const fields = new Set(handlers.flatMap((handler) => Object.keys(handler))); + for (const field of [ + 'args', 'async', 'asyncRewake', 'shell', 'if', 'statusMessage', 'once', 'timeout', + 'url', 'headers', 'allowedEnvVars', 'server', 'tool', 'input', 'prompt', 'model', 'continueOnBlock', + ]) { + expect(fields.has(field), field).toBe(true); + } + + const allEvents = await readJson<{ readonly hooks: Record }>('./cases/all-documented-events.json'); + expect(validate(allEvents), JSON.stringify(validate.errors)).toBe(true); + expect(Object.keys(allEvents.hooks).sort()).toEqual([...documentedEvents]); + expect(Object.keys(hooksSchema.properties.hooks.properties).sort()).toEqual([...documentedEvents]); + expect(hooksSchema.properties.hooks.additionalProperties).toBe(false); +}); + +it('closes each event to the handler types the hooks reference documents for it', () => { + const prompt = { prompt: 'Verify. $ARGUMENTS', type: 'prompt' }; + const agent = { prompt: 'Verify with tools. $ARGUMENTS', type: 'agent' }; + const http = { type: 'http', url: 'http://localhost:8080/hook' }; + const mcpTool = { server: 'plugin:fixture:db', tool: 'scan', type: 'mcp_tool' }; + const command = { command: 'echo hi', type: 'command' }; + for (const event of documentedEvents) { + const supportsModelHandlers = (allHandlerEvents as readonly string[]).includes(event); + const startup = (startupEvents as readonly string[]).includes(event); + expect(validate({ hooks: { [event]: handlerGroup(command) } }), `${event} command`).toBe(true); + expect(validate({ hooks: { [event]: handlerGroup(mcpTool) } }), `${event} mcp_tool`).toBe(true); + expect(validate({ hooks: { [event]: handlerGroup(http) } }), `${event} http`).toBe(!startup); + expect(validate({ hooks: { [event]: handlerGroup(prompt) } }), `${event} prompt`).toBe(supportsModelHandlers); + expect(validate({ hooks: { [event]: handlerGroup(agent) } }), `${event} agent`).toBe(supportsModelHandlers); + } +}); + +it('requires the per-type fields the hooks reference marks required and types every optional one', () => { + const stop = (handler: Readonly>): unknown => ({ hooks: { Stop: handlerGroup(handler) } }); + expect(validate(stop({ type: 'command' }))).toBe(false); + expect(validate(stop({ type: 'http' }))).toBe(false); + expect(validate(stop({ server: 'db', type: 'mcp_tool' }))).toBe(false); + expect(validate(stop({ tool: 'scan', type: 'mcp_tool' }))).toBe(false); + expect(validate(stop({ type: 'prompt' }))).toBe(false); + expect(validate(stop({ type: 'agent' }))).toBe(false); + expect(validate(stop({ command: 'echo hi' }))).toBe(false); + expect(validate(stop({ command: 'echo hi', type: 'webhook' }))).toBe(false); + + expect(validate(stop({ command: 'node', args: ['${CLAUDE_PLUGIN_ROOT}/hooks/stop.mjs'], type: 'command' }))).toBe(true); + expect(validate(stop({ command: 'node', args: [1], type: 'command' }))).toBe(false); + expect(validate(stop({ command: 'echo hi', shell: 'powershell', type: 'command' }))).toBe(true); + expect(validate(stop({ command: 'echo hi', shell: 'zsh', type: 'command' }))).toBe(false); + expect(validate(stop({ command: 'echo hi', if: 'Bash(git *)', once: true, statusMessage: 'Checking', type: 'command' }))).toBe(true); + expect(validate(stop({ command: 'echo hi', if: 5, type: 'command' }))).toBe(false); + expect(validate(stop({ command: 'echo hi', once: 'yes', type: 'command' }))).toBe(false); + expect(validate(stop({ command: 'echo hi', timeout: 0, type: 'command' }))).toBe(false); + expect(validate(stop({ headers: { Authorization: 'Bearer $TOKEN' }, allowedEnvVars: ['TOKEN'], type: 'http', url: 'http://x' }))).toBe(true); + expect(validate(stop({ headers: { Authorization: 1 }, type: 'http', url: 'http://x' }))).toBe(false); + expect(validate(stop({ input: { file_path: '${tool_input.file_path}' }, server: 'db', tool: 'scan', type: 'mcp_tool' }))).toBe(true); + expect(validate(stop({ input: 'x', server: 'db', tool: 'scan', type: 'mcp_tool' }))).toBe(false); + expect(validate(stop({ continueOnBlock: true, model: 'haiku', prompt: 'Check. $ARGUMENTS', type: 'prompt' }))).toBe(true); + expect(validate(stop({ continueOnBlock: true, prompt: 'Check. $ARGUMENTS', type: 'agent' }))).toBe(false); + expect(validate(stop({ model: 'sonnet', prompt: 'Check. $ARGUMENTS', timeout: 120, type: 'agent' }))).toBe(true); +}); + +it('keeps the pinned schema descriptor and the capability evidence in step with the widened contract', () => { + const claude = createDefaultRegistry().get('claude'); + const descriptor = claude.metadata.schemas.find((schema) => schema.name === 'hooks'); + expect(descriptor?.sha256).toBe(schemaProvenance.schemas['hooks.schema.json'].sha256); + expect(schemaProvenance.hooksSchemaNotes).toContain('hooks.schema.json was re-pinned (uploaded 2026-09-03'); + expect(schemaProvenance.hooksSchemaNotes).toContain('Claude Code 2.1.260'); + + const handlerContract = claudeCapabilityTable.hooks.handlerContract; + expect(handlerContract.types).toEqual(['agent', 'command', 'http', 'mcp_tool', 'prompt']); + expect(handlerContract.emitted).toEqual({ command: ['command', 'timeout', 'type'] }); + expect(handlerContract.evidence.some((line) => line.startsWith('uploaded 2026-09-03:') && line.includes('hooks-2.md'))).toBe(true); + expect(handlerContract.evidence.some((line) => line.includes('Claude Code 2.1.260'))).toBe(true); + for (const feature of ['timeout', 'toolMatchers'] as const) { + expect(claudeCapabilityTable.hooks.features[feature].evidence.some((line) => line.includes('every handler'))).toBe(true); + } +}); + +it('enumerates only command handlers from a native document that mixes the documented handler types', async () => { + const full = await readJson>('./cases/full.json'); + const enumerated = readStandardNativeHookCommands(full); + expect(enumerated.status).toBe('found'); + if (enumerated.status !== 'found') return; + expect(enumerated.commands.map((entry) => entry.command)).toEqual([ + 'node', '"${CLAUDE_PLUGIN_ROOT}"/hooks/pre.sh', '/path/to/run-tests.sh', 'pwsh-thing', 'echo hi', 'echo hi', + ]); + expect(readStandardNativeHookCommands({ hooks: { Stop: [{ hooks: [{ prompt: 'x', type: 5 }] }] } })).toEqual({ status: 'invalid' }); + expect(readStandardNativeHookCommands({ hooks: { Stop: [{ hooks: [{ type: 'command' }] }] } })).toEqual({ status: 'invalid' }); +}); + +it('plans a Claude native hooks document that uses every documented handler type', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-hooks-schema-')); + const registry = createDefaultRegistry(); + const loaded: LoadedConfig = { + config: { + claude: { nativeHooks: './claude-hooks.json' }, + plugin: { name: 'review-tools', version: '1.0.0' }, + targets: ['claude'], + }, + configPath: join(root, 'agent-bundle.config.ts'), + context: { command: 'build', mode: 'production', projectRoot: root, selectedTargets: [] }, + }; + try { + await writeFile(join(root, 'claude-hooks.json'), await readFile(new URL('./cases/full.json', fixtureRoot), 'utf8')); + const plan = registry.get('claude').plan(await normalizeProject(loaded, { skills: [] }, registry)); + expect(plan.diagnostics).toEqual([]); + const emitted = plan.entries.find((entry) => entry.kind === 'write' && entry.relativePath === 'hooks/hooks.json'); + expect(emitted?.kind).toBe('write'); + if (emitted?.kind !== 'write') return; + expect(JSON.parse(emitted.content)).toEqual(await readJson('./cases/full.json')); + + await writeFile(join(root, 'claude-hooks.json'), await readFile(new URL('./cases/prompt-on-session-start.json', fixtureRoot), 'utf8')); + const rejected = registry.get('claude').plan(await normalizeProject(loaded, { skills: [] }, registry)); + expect(rejected.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(['claude.native-hooks.schema']); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-missing-prompt.json new file mode 100644 index 000000000..720b28613 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-missing-prompt.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "agent" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-on-session-start.json new file mode 100644 index 000000000..1376f54cc --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/agent-on-session-start.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "agent", + "prompt": "x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json new file mode 100644 index 000000000..addc2cf74 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json @@ -0,0 +1,334 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo SessionStart" + } + ] + } + ], + "Setup": [ + { + "hooks": [ + { + "type": "command", + "command": "echo Setup" + } + ] + } + ], + "InstructionsLoaded": [ + { + "hooks": [ + { + "type": "command", + "command": "echo InstructionsLoaded" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "echo UserPromptSubmit" + } + ] + } + ], + "UserPromptExpansion": [ + { + "hooks": [ + { + "type": "command", + "command": "echo UserPromptExpansion" + } + ] + } + ], + "MessageDisplay": [ + { + "hooks": [ + { + "type": "command", + "command": "echo MessageDisplay" + } + ] + } + ], + "PreToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PreToolUse" + } + ] + } + ], + "PermissionRequest": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PermissionRequest" + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostToolUse" + } + ] + } + ], + "PostToolUseFailure": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostToolUseFailure" + } + ] + } + ], + "PostToolBatch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostToolBatch" + } + ] + } + ], + "PermissionDenied": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PermissionDenied" + } + ] + } + ], + "Notification": [ + { + "hooks": [ + { + "type": "command", + "command": "echo Notification" + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo SubagentStart" + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo SubagentStop" + } + ] + } + ], + "TaskCreated": [ + { + "hooks": [ + { + "type": "command", + "command": "echo TaskCreated" + } + ] + } + ], + "TaskCompleted": [ + { + "hooks": [ + { + "type": "command", + "command": "echo TaskCompleted" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo Stop" + } + ] + } + ], + "StopFailure": [ + { + "hooks": [ + { + "type": "command", + "command": "echo StopFailure" + } + ] + } + ], + "TeammateIdle": [ + { + "hooks": [ + { + "type": "command", + "command": "echo TeammateIdle" + } + ] + } + ], + "ConfigChange": [ + { + "hooks": [ + { + "type": "command", + "command": "echo ConfigChange" + } + ] + } + ], + "CwdChanged": [ + { + "hooks": [ + { + "type": "command", + "command": "echo CwdChanged" + } + ] + } + ], + "DirectoryAdded": [ + { + "hooks": [ + { + "type": "command", + "command": "echo DirectoryAdded" + } + ] + } + ], + "FileChanged": [ + { + "hooks": [ + { + "type": "command", + "command": "echo FileChanged" + } + ] + } + ], + "WorktreeCreate": [ + { + "hooks": [ + { + "type": "command", + "command": "echo WorktreeCreate" + } + ] + } + ], + "WorktreeRemove": [ + { + "hooks": [ + { + "type": "command", + "command": "echo WorktreeRemove" + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PreCompact" + } + ] + } + ], + "PostCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostCompact" + } + ] + } + ], + "PreModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PreModelSwitch" + } + ] + } + ], + "PostModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostModelSwitch" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "echo SessionEnd" + } + ] + } + ], + "Elicitation": [ + { + "hooks": [ + { + "type": "command", + "command": "echo Elicitation" + } + ] + } + ], + "ElicitationResult": [ + { + "hooks": [ + { + "type": "command", + "command": "echo ElicitationResult" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/allowed-env-vars-not-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/allowed-env-vars-not-array.json new file mode 100644 index 000000000..360ccff3b --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/allowed-env-vars-not-array.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x", + "allowedEnvVars": "A" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-not-strings.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-not-strings.json new file mode 100644 index 000000000..ea69f682a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-not-strings.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "args": [ + 1 + ] + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-on-http.json new file mode 100644 index 000000000..fd68032b5 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-on-http.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x", + "args": [] + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-string.json new file mode 100644 index 000000000..759b5a5a9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/args-string.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "args": "--fix" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-http.json new file mode 100644 index 000000000..6c35e213c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-http.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x", + "async": true + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-prompt.json new file mode 100644 index 000000000..31d7f4b85 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-on-prompt.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "x", + "async": true + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-rewake-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-rewake-on-mcp.json new file mode 100644 index 000000000..057c89f51 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-rewake-on-mcp.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "mcp_tool", + "server": "db", + "tool": "t", + "asyncRewake": true + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-string.json new file mode 100644 index 000000000..26459860b --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/async-string.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "async": "yes" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-agent.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-agent.json new file mode 100644 index 000000000..6dcd2927d --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-agent.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "agent", + "prompt": "x", + "continueOnBlock": true + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-command.json new file mode 100644 index 000000000..6727af2fe --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/continue-on-block-on-command.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "continueOnBlock": true + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/description-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/description-number.json new file mode 100644 index 000000000..7f822e39e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/description-number.json @@ -0,0 +1,15 @@ +{ + "description": 5, + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-command.json new file mode 100644 index 000000000..7f1f22402 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-command.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-hooks-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-hooks-array.json new file mode 100644 index 000000000..1bf1f41da --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/empty-hooks-array.json @@ -0,0 +1,9 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/full.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/full.json new file mode 100644 index 000000000..040b7a7a9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/full.json @@ -0,0 +1,113 @@ +{ + "description": "Every documented handler type and field.", + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/hooks/pre.mjs", + "--fix" + ], + "timeout": 30, + "if": "Bash(git *)", + "statusMessage": "Checking git", + "once": true + }, + { + "type": "command", + "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/pre.sh", + "shell": "bash", + "asyncRewake": true + }, + { + "type": "http", + "url": "http://localhost:8080/hooks/pre", + "timeout": 30, + "headers": { + "Authorization": "Bearer $MY_TOKEN" + }, + "allowedEnvVars": [ + "MY_TOKEN" + ] + }, + { + "type": "mcp_tool", + "server": "plugin:fixture:db", + "tool": "security_scan", + "input": { + "file_path": "${tool_input.file_path}" + }, + "timeout": 20 + } + ], + "matcher": "Bash" + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "command", + "command": "/path/to/run-tests.sh", + "async": true + }, + { + "type": "command", + "command": "pwsh-thing", + "shell": "powershell" + } + ], + "matcher": "Write|Edit" + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "Evaluate if Claude should stop: $ARGUMENTS", + "model": "haiku", + "timeout": 30, + "continueOnBlock": true + }, + { + "type": "agent", + "prompt": "Verify that all unit tests pass. $ARGUMENTS", + "model": "sonnet", + "timeout": 120 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ], + "matcher": "^fixture:reviewer$" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + }, + { + "type": "mcp_tool", + "server": "db", + "tool": "warm" + } + ], + "matcher": "startup|resume" + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/group-missing-hooks.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/group-missing-hooks.json new file mode 100644 index 000000000..e49cb6c76 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/group-missing-hooks.json @@ -0,0 +1,9 @@ +{ + "hooks": { + "Stop": [ + { + "matcher": "x" + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-not-object.json new file mode 100644 index 000000000..f9068584b --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-not-object.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x", + "headers": [ + "a" + ] + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-value-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-value-number.json new file mode 100644 index 000000000..952b25b7c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/headers-value-number.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x", + "headers": { + "a": 1 + } + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/hooks-empty-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/hooks-empty-object.json new file mode 100644 index 000000000..deffac973 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/hooks-empty-object.json @@ -0,0 +1,3 @@ +{ + "hooks": {} +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-missing-url.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-missing-url.json new file mode 100644 index 000000000..3851d2f52 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-missing-url.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-on-session-start.json new file mode 100644 index 000000000..9af563198 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/http-on-session-start.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "http", + "url": "http://x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/if-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/if-number.json new file mode 100644 index 000000000..9aa5829d2 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/if-number.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "if": 5 + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/input-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/input-not-object.json new file mode 100644 index 000000000..2f36ed68e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/input-not-object.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "mcp_tool", + "server": "db", + "tool": "t", + "input": "x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/matcher-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/matcher-number.json new file mode 100644 index 000000000..6d132eada --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/matcher-number.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ], + "matcher": 5 + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/mcp-tool-missing-tool.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/mcp-tool-missing-tool.json new file mode 100644 index 000000000..abde73a52 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/mcp-tool-missing-tool.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "mcp_tool", + "server": "db" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-command.json new file mode 100644 index 000000000..6e3217b8d --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-command.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-type.json new file mode 100644 index 000000000..979befcc4 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/missing-type.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "command": "echo hi" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-on-command.json new file mode 100644 index 000000000..4983a5b0b --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-on-command.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "model": "haiku" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/no-hooks-key.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/no-hooks-key.json new file mode 100644 index 000000000..15cbc68e2 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/no-hooks-key.json @@ -0,0 +1,3 @@ +{ + "description": "x" +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-false-async-false.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-false-async-false.json new file mode 100644 index 000000000..0c02b8f23 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-false-async-false.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "once": false, + "async": false, + "asyncRewake": false + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-string.json new file mode 100644 index 000000000..5b32d8546 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/once-string.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "once": "yes" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-empty.json new file mode 100644 index 000000000..6c34f2884 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-empty.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-missing-prompt.json new file mode 100644 index 000000000..2a7c38be0 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-missing-prompt.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "prompt" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-session-start.json new file mode 100644 index 000000000..27e0de385 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-session-start.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-subagent-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-subagent-start.json new file mode 100644 index 000000000..edbf290c7 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/prompt-on-subagent-start.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SubagentStart": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/server-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/server-empty.json new file mode 100644 index 000000000..78188e6f5 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/server-empty.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "mcp_tool", + "server": "", + "tool": "t" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-invalid.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-invalid.json new file mode 100644 index 000000000..5adf5e5c4 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-invalid.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "shell": "zsh" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-on-mcp.json new file mode 100644 index 000000000..6306f027c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/shell-on-mcp.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "mcp_tool", + "server": "db", + "tool": "t", + "shell": "bash" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/status-message-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/status-message-number.json new file mode 100644 index 000000000..2a1c361e9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/status-message-number.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "statusMessage": 5 + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-float.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-float.json new file mode 100644 index 000000000..aa9a082bf --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-float.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "timeout": 1.5 + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-string.json new file mode 100644 index 000000000..282390306 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-string.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "timeout": "30" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-zero.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-zero.json new file mode 100644 index 000000000..6e2f114d8 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/timeout-zero.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "timeout": 0 + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-event.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-event.json new file mode 100644 index 000000000..6515e61dd --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-event.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "FooEvent": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-group-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-group-field.json new file mode 100644 index 000000000..08b0b7c15 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-group-field.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ], + "foo": 1 + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-handler-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-handler-field.json new file mode 100644 index 000000000..68c5cbc15 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-handler-field.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "foo": 1 + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-top-level-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-top-level-field.json new file mode 100644 index 000000000..0ee932401 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-top-level-field.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi" + } + ] + } + ] + }, + "foo": 1 +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-type.json new file mode 100644 index 000000000..d8371c787 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/unknown-type.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "webhook", + "url": "http://x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-empty.json new file mode 100644 index 000000000..e70c29c53 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-empty.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-on-command.json new file mode 100644 index 000000000..9e7a3b5e5 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/url-on-command.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hi", + "url": "http://x" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-missing-prompt.json new file mode 100644 index 000000000..2b45b39b1 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-missing-prompt.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid agent hook (prompt: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-on-session-start.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/agent-on-session-start.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/allowed-env-vars-not-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/allowed-env-vars-not-array.json new file mode 100644 index 000000000..acce4597e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/allowed-env-vars-not-array.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid http hook (allowedEnvVars: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-not-strings.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-not-strings.json new file mode 100644 index 000000000..6e670913f --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-not-strings.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (args.0: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-on-http.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-on-http.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-string.json new file mode 100644 index 000000000..f534b0ac6 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/args-string.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (args: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-http.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-http.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-prompt.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-on-prompt.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-rewake-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-rewake-on-mcp.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-rewake-on-mcp.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-string.json new file mode 100644 index 000000000..c0648a8ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/async-string.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (async: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-agent.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-agent.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-agent.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-command.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/continue-on-block-on-command.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/description-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/description-number.json new file mode 100644 index 000000000..f34e1d448 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/description-number.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "description", + "message": "Invalid input", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-command.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-command.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-hooks-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-hooks-array.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/empty-hooks-array.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/full.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/full.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/full.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/group-missing-hooks.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/group-missing-hooks.json new file mode 100644 index 000000000..c66ec28bc --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/group-missing-hooks.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0: Hook matcher \"hooks\" must be an array of hook entries; received undefined; entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-not-object.json new file mode 100644 index 000000000..bc6db8f7d --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-not-object.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid http hook (headers: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-value-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-value-number.json new file mode 100644 index 000000000..04c2e5378 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/headers-value-number.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid http hook (headers.a: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/hooks-empty-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/hooks-empty-object.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/hooks-empty-object.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-missing-url.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-missing-url.json new file mode 100644 index 000000000..73d6b6d2f --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-missing-url.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid http hook (url: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-on-session-start.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/http-on-session-start.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/if-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/if-number.json new file mode 100644 index 000000000..c08a29f37 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/if-number.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (if: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/input-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/input-not-object.json new file mode 100644 index 000000000..b370a7808 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/input-not-object.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid mcp_tool hook (input: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/matcher-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/matcher-number.json new file mode 100644 index 000000000..27b19a2eb --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/matcher-number.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0: Invalid hook matcher (matcher: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/mcp-tool-missing-tool.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/mcp-tool-missing-tool.json new file mode 100644 index 000000000..e8505a11c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/mcp-tool-missing-tool.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid mcp_tool hook (tool: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-command.json new file mode 100644 index 000000000..ba420b097 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-command.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (command: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-type.json new file mode 100644 index 000000000..2f4851e33 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/missing-type.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Hook entry has no \"type\"; entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-on-command.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-on-command.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/no-hooks-key.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/no-hooks-key.json new file mode 100644 index 000000000..39c47ac1e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/no-hooks-key.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "root", + "message": "hooks.json must have `hooks` (the hook matchers) or `modules` (hooks modules), or both", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-false-async-false.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-false-async-false.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-false-async-false.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-string.json new file mode 100644 index 000000000..9277660d4 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/once-string.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (once: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-empty.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-empty.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-missing-prompt.json new file mode 100644 index 000000000..6e49be572 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-missing-prompt.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid prompt hook (prompt: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-session-start.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-session-start.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-subagent-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-subagent-start.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/prompt-on-subagent-start.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/server-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/server-empty.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/server-empty.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-invalid.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-invalid.json new file mode 100644 index 000000000..e7e94df65 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-invalid.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (shell: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-on-mcp.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/shell-on-mcp.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/status-message-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/status-message-number.json new file mode 100644 index 000000000..6f7ab5377 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/status-message-number.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (statusMessage: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-float.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-float.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-float.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-string.json new file mode 100644 index 000000000..ee6130e8e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-string.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (timeout: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-zero.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-zero.json new file mode 100644 index 000000000..ee6130e8e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/timeout-zero.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid command hook (timeout: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-event.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-event.json new file mode 100644 index 000000000..a5e2cd3e9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-event.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.FooEvent: unknown hook event; entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-group-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-group-field.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-group-field.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-handler-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-handler-field.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-handler-field.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-top-level-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-top-level-field.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-top-level-field.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-type.json new file mode 100644 index 000000000..263fbee0c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/unknown-type.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Unknown hook type \"webhook\"; entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-empty.json new file mode 100644 index 000000000..73d6b6d2f --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-empty.json @@ -0,0 +1,27 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [], + "warnings": [ + { + "path": "hooks", + "message": "hooks.Stop.0.hooks.0: Invalid http hook (url: Invalid input); entry ignored at runtime", + "code": null + } + ], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-on-command.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/url-on-command.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/scripts/record-claude-hooks-schema-fixtures.mjs b/scripts/record-claude-hooks-schema-fixtures.mjs new file mode 100644 index 000000000..c2dfe06b3 --- /dev/null +++ b/scripts/record-claude-hooks-schema-fixtures.mjs @@ -0,0 +1,82 @@ +/** + * Re-records the Claude CLI verdicts the pinned `schemas/claude/hooks.schema.json` + * is cross-checked against (tests/claude-hooks-schema.test.ts, #477). + * + * Every `tests/fixtures/claude-hooks-schema/cases/.json` is written as the + * `hooks/hooks.json` of a minimal plugin and validated with + * `claude plugin validate /.claude-plugin/plugin.json --strict --json`; + * the JSON report lands in `reports//.json` with the + * temporary plugin path replaced by `/bundle/claude`, the placeholder the other + * recorded Claude reports under tests/fixtures use. The run needs no login: it + * points CLAUDE_CONFIG_DIR at an empty directory. + * + * node scripts/record-claude-hooks-schema-fixtures.mjs + */ +import { execFile as executeFile } from 'node:child_process'; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const execFile = promisify(executeFile); +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const fixtureRoot = join(repositoryRoot, 'packages/agent-bundle/tests/fixtures/claude-hooks-schema'); +const pluginPathPlaceholder = '/bundle/claude'; + +/** The manifest every case shares; `author` keeps `--strict` from failing on the attribution warning. */ +const manifest = { + author: { name: 'agent-bundle' }, + description: 'Claude hooks schema fixture.', + name: 'hooks-schema-fixture', + version: '1.0.0', +}; + +const claude = async (args, env) => { + try { + const { stdout, stderr } = await execFile('claude', args, { encoding: 'utf8', env }); + return { exitCode: 0, stderr, stdout }; + } catch (error) { + if (typeof error?.code !== 'number') throw error; + return { exitCode: error.code, stderr: error.stderr ?? '', stdout: error.stdout ?? '' }; + } +}; + +const scratch = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-hooks-schema-')); +try { + const env = { ...process.env, CLAUDE_CONFIG_DIR: join(scratch, 'config') }; + await mkdir(env.CLAUDE_CONFIG_DIR, { recursive: true }); + const version = (await claude(['--version'], env)).stdout.trim().split(/\s+/u)[0]; + if (!/^\d+\.\d+\.\d+/u.test(version ?? '')) throw new Error(`could not read the Claude CLI version: ${version}`); + const reportRoot = join(fixtureRoot, 'reports', version); + await mkdir(reportRoot, { recursive: true }); + + const cases = (await readdir(join(fixtureRoot, 'cases'))).filter((name) => name.endsWith('.json')).sort(); + let disagreements = 0; + for (const file of cases) { + const name = file.slice(0, -'.json'.length); + const pluginRoot = join(scratch, name); + await mkdir(join(pluginRoot, '.claude-plugin'), { recursive: true }); + await mkdir(join(pluginRoot, 'hooks'), { recursive: true }); + await writeFile(join(pluginRoot, '.claude-plugin', 'plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`); + await writeFile(join(pluginRoot, 'hooks', 'hooks.json'), await readFile(join(fixtureRoot, 'cases', file), 'utf8')); + + const manifestPath = join(pluginRoot, '.claude-plugin', 'plugin.json'); + const result = await claude(['plugin', 'validate', manifestPath, '--strict', '--json'], env); + if (result.exitCode === 2 || result.stdout.trim().length === 0) { + throw new Error(`claude plugin validate did not produce a report for ${name} (exit ${result.exitCode}): ${result.stderr}`); + } + const report = JSON.parse(result.stdout.replaceAll(pluginRoot, pluginPathPlaceholder)); + if (report.success !== (result.exitCode === 0)) disagreements += 1; + await writeFile(join(reportRoot, file), `${JSON.stringify(report, null, 2)}\n`); + const findings = (report.contents ?? []).flatMap((content) => [ + ...(content.errors ?? []).map((finding) => `error ${finding.path}: ${finding.message}`), + ...(content.warnings ?? []).map((finding) => `warning ${finding.path}: ${finding.message}`), + ]); + console.log(`${report.success ? 'accepted' : 'rejected'} ${name}${findings.length === 0 ? '' : ` — ${findings.join('; ')}`}`); + } + console.log(`Recorded ${cases.length} reports from Claude Code ${version} under ${reportRoot}.`); + if (disagreements > 0) throw new Error(`${disagreements} report(s) disagree with their exit code.`); +} finally { + await rm(scratch, { force: true, recursive: true }); +} diff --git a/website/docs/en/guide/authoring/index.mdx b/website/docs/en/guide/authoring/index.mdx index d9265895b..5ae432941 100644 --- a/website/docs/en/guide/authoring/index.mdx +++ b/website/docs/en/guide/authoring/index.mdx @@ -83,7 +83,13 @@ of leaking into compiler core. Ordinary projects need no extension key at all. Every host key is optional and every field inside it is optional. `claude` and `codex` extend the shared `AgentBundleHostConfig`, whose single field `nativeHooks` names a project-authored, target-native hook document (`hooks.json`) the adapter validates and merges beside the compiled -hooks. The adapter-owned fields are: +hooks. For Claude Code the pinned schema admits the documented handler contract: every hook event +the hooks reference lists, the five handler types (`command`, `http`, `mcp_tool`, `prompt`, +`agent`), their per-type fields (`args`, `async`, `shell`, `url`, `headers`, `server`, `tool`, +`input`, `prompt`, `model`, …), and the common `if`, `once`, `statusMessage`, and `timeout` +fields; unknown keys, a field of another handler type, and a prompt/agent/http handler on an event +the reference excludes it from are rejected. The compiler itself still emits shell-form `command` +handlers only. The adapter-owned fields are: | Key | Fields | Where they land | | --- | --- | --- | diff --git a/website/docs/zh/guide/authoring/index.mdx b/website/docs/zh/guide/authoring/index.mdx index 1341d54a2..b557f662b 100644 --- a/website/docs/zh/guide/authoring/index.mdx +++ b/website/docs/zh/guide/authoring/index.mdx @@ -78,7 +78,12 @@ export default defineConfig({ 每个宿主键都是可选的,其中的每个字段也都是可选的。`claude` 与 `codex` 扩展自共享的 `AgentBundleHostConfig`,它唯一的字段 `nativeHooks` 指向一份由项目编写、target 原生的钩子文档 -(`hooks.json`),适配器会校验它并与编译出的钩子合并。各适配器拥有的字段如下: +(`hooks.json`),适配器会校验它并与编译出的钩子合并。对 Claude Code 而言,固定的 schema 接纳文档化的 +处理器契约:hooks 参考列出的每个钩子事件、五种处理器类型(`command`、`http`、`mcp_tool`、`prompt`、 +`agent`)、各类型自己的字段(`args`、`async`、`shell`、`url`、`headers`、`server`、`tool`、`input`、 +`prompt`、`model` 等)以及公共字段 `if`、`once`、`statusMessage` 与 `timeout`;未知键、属于另一种处理器 +类型的字段,以及参考文档明确排除的事件上的 prompt/agent/http 处理器都会被拒绝。编译器自身仍只输出 +shell 形式的 `command` 处理器。各适配器拥有的字段如下: | 键 | 字段 | 落点 | | --- | --- | --- | From 14f7fea524227c451cbadda0ca7e8e4a90fa0b63 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 00:59:19 +0000 Subject: [PATCH 2/3] chore: point changeset at #496 --- .changeset/477-claude-hooks-schema-agents-rows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/477-claude-hooks-schema-agents-rows.md b/.changeset/477-claude-hooks-schema-agents-rows.md index 77e64a68f..a454f7c6f 100644 --- a/.changeset/477-claude-hooks-schema-agents-rows.md +++ b/.changeset/477-claude-hooks-schema-agents-rows.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Re-pin the Claude Code `hooks.schema.json` to the documented hook handler contract so a `claude.nativeHooks` document may use every handler type (`command`, `http`, `mcp_tool`, `prompt`, `agent`), the per-type fields (`args`, `async`, `asyncRewake`, `shell`, `url`, `headers`, `allowedEnvVars`, `server`, `tool`, `input`, `prompt`, `model`, `continueOnBlock`), the common `if`, `once`, `statusMessage`, and `timeout` fields, and all 33 documented events, each closed to the handler types the reference allows for it; the compiler still emits shell-form `command` handlers only. Update the `claude` host's agents capability rows with `color`, `initialPrompt`, and `experimental.cacheTtl`, record that `permissionMode`, `mcpServers`, and `hooks` are ignored for plugin subagents, pin the anchored `^:$` `agent_type` matcher note, and bump the Claude `adapterRevision` to `1.26.0`. (#488) +Re-pin the Claude Code `hooks.schema.json` to the documented hook handler contract so a `claude.nativeHooks` document may use every handler type (`command`, `http`, `mcp_tool`, `prompt`, `agent`), the per-type fields (`args`, `async`, `asyncRewake`, `shell`, `url`, `headers`, `allowedEnvVars`, `server`, `tool`, `input`, `prompt`, `model`, `continueOnBlock`), the common `if`, `once`, `statusMessage`, and `timeout` fields, and all 33 documented events, each closed to the handler types the reference allows for it; the compiler still emits shell-form `command` handlers only. Update the `claude` host's agents capability rows with `color`, `initialPrompt`, and `experimental.cacheTtl`, record that `permissionMode`, `mcpServers`, and `hooks` are ignored for plugin subagents, pin the anchored `^:$` `agent_type` matcher note, and bump the Claude `adapterRevision` to `1.26.0`. (#496) From f3c1271a871fed76b507105ff54a68292bcab499 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 01:18:43 +0000 Subject: [PATCH 3/3] fix(claude): cross-check the hooks schema against the pinned 2.1.250 CLI too; omit the 2.1.251 model-switch events until the re-pin --- .../477-claude-hooks-schema-agents-rows.md | 2 +- .../adapters/capabilities/claude-2.1.250.json | 2 +- .../adapters/schemas/claude/PROVENANCE.json | 6 +- .../adapters/schemas/claude/hooks.schema.json | 2 - .../tests/adapter-metadata.test.ts | 2 +- .../tests/claude-hooks-schema.test.ts | 78 +++++++++++++------ ...ted-events.json => all-pinned-events.json} | 20 ----- .../cases/model-switch-events.json | 24 ++++++ .../reports/2.1.250/agent-missing-prompt.json | 28 +++++++ .../2.1.250/agent-on-session-start.json | 14 ++++ .../reports/2.1.250/all-pinned-events.json | 14 ++++ .../2.1.250/allowed-env-vars-not-array.json | 28 +++++++ .../reports/2.1.250/args-not-strings.json | 28 +++++++ .../reports/2.1.250/args-on-http.json | 14 ++++ .../reports/2.1.250/args-string.json | 28 +++++++ .../reports/2.1.250/async-on-http.json | 14 ++++ .../reports/2.1.250/async-on-prompt.json | 14 ++++ .../reports/2.1.250/async-rewake-on-mcp.json | 14 ++++ .../reports/2.1.250/async-string.json | 28 +++++++ .../2.1.250/continue-on-block-on-agent.json | 14 ++++ .../2.1.250/continue-on-block-on-command.json | 14 ++++ .../reports/2.1.250/description-number.json | 28 +++++++ .../reports/2.1.250/empty-command.json | 14 ++++ .../reports/2.1.250/empty-hooks-array.json | 14 ++++ .../reports/2.1.250/full.json | 14 ++++ .../reports/2.1.250/group-missing-hooks.json | 28 +++++++ .../reports/2.1.250/headers-not-object.json | 28 +++++++ .../reports/2.1.250/headers-value-number.json | 28 +++++++ .../reports/2.1.250/hooks-empty-object.json | 14 ++++ .../reports/2.1.250/http-missing-url.json | 28 +++++++ .../2.1.250/http-on-session-start.json | 14 ++++ .../reports/2.1.250/if-number.json | 28 +++++++ .../reports/2.1.250/input-not-object.json | 28 +++++++ .../reports/2.1.250/matcher-number.json | 28 +++++++ .../2.1.250/mcp-tool-missing-tool.json | 28 +++++++ .../reports/2.1.250/missing-command.json | 28 +++++++ .../reports/2.1.250/missing-type.json | 28 +++++++ .../reports/2.1.250/model-on-command.json | 14 ++++ .../reports/2.1.250/model-switch-events.json | 33 ++++++++ .../reports/2.1.250/no-hooks-key.json | 28 +++++++ .../2.1.250/once-false-async-false.json | 14 ++++ .../reports/2.1.250/once-string.json | 28 +++++++ .../reports/2.1.250/prompt-empty.json | 14 ++++ .../2.1.250/prompt-missing-prompt.json | 28 +++++++ .../2.1.250/prompt-on-session-start.json | 14 ++++ .../2.1.250/prompt-on-subagent-start.json | 14 ++++ .../reports/2.1.250/server-empty.json | 14 ++++ .../reports/2.1.250/shell-invalid.json | 28 +++++++ .../reports/2.1.250/shell-on-mcp.json | 14 ++++ .../2.1.250/status-message-number.json | 28 +++++++ .../reports/2.1.250/timeout-float.json | 14 ++++ .../reports/2.1.250/timeout-string.json | 28 +++++++ .../reports/2.1.250/timeout-zero.json | 33 ++++++++ .../reports/2.1.250/unknown-event.json | 28 +++++++ .../reports/2.1.250/unknown-group-field.json | 14 ++++ .../2.1.250/unknown-handler-field.json | 14 ++++ .../2.1.250/unknown-top-level-field.json | 14 ++++ .../reports/2.1.250/unknown-type.json | 28 +++++++ .../reports/2.1.250/url-empty.json | 33 ++++++++ .../reports/2.1.250/url-on-command.json | 14 ++++ ...ted-events.json => all-pinned-events.json} | 0 .../reports/2.1.260/model-switch-events.json | 13 ++++ .../record-claude-hooks-schema-fixtures.mjs | 71 ++++++++++++++--- website/docs/en/guide/authoring/index.mdx | 3 +- website/docs/zh/guide/authoring/index.mdx | 3 +- 65 files changed, 1286 insertions(+), 61 deletions(-) rename packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/{all-documented-events.json => all-pinned-events.json} (93%) create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-switch-events.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/allowed-env-vars-not-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-not-strings.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-http.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-rewake-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-agent.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/description-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-hooks-array.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/full.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/group-missing-hooks.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-value-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/hooks-empty-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-missing-url.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/if-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/input-not-object.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/matcher-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/mcp-tool-missing-tool.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-on-command.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-switch-events.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/no-hooks-key.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-false-async-false.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-missing-prompt.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-session-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-subagent-start.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/server-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-invalid.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-on-mcp.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/status-message-number.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-float.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-string.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-zero.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-event.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-group-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-handler-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-top-level-field.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-type.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-empty.json create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-on-command.json rename packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/{all-documented-events.json => all-pinned-events.json} (100%) create mode 100644 packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-switch-events.json diff --git a/.changeset/477-claude-hooks-schema-agents-rows.md b/.changeset/477-claude-hooks-schema-agents-rows.md index a454f7c6f..d8863f84c 100644 --- a/.changeset/477-claude-hooks-schema-agents-rows.md +++ b/.changeset/477-claude-hooks-schema-agents-rows.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Re-pin the Claude Code `hooks.schema.json` to the documented hook handler contract so a `claude.nativeHooks` document may use every handler type (`command`, `http`, `mcp_tool`, `prompt`, `agent`), the per-type fields (`args`, `async`, `asyncRewake`, `shell`, `url`, `headers`, `allowedEnvVars`, `server`, `tool`, `input`, `prompt`, `model`, `continueOnBlock`), the common `if`, `once`, `statusMessage`, and `timeout` fields, and all 33 documented events, each closed to the handler types the reference allows for it; the compiler still emits shell-form `command` handlers only. Update the `claude` host's agents capability rows with `color`, `initialPrompt`, and `experimental.cacheTtl`, record that `permissionMode`, `mcpServers`, and `hooks` are ignored for plugin subagents, pin the anchored `^:$` `agent_type` matcher note, and bump the Claude `adapterRevision` to `1.26.0`. (#496) +Re-pin the Claude Code `hooks.schema.json` to the documented hook handler contract so a `claude.nativeHooks` document may use every handler type (`command`, `http`, `mcp_tool`, `prompt`, `agent`), the per-type fields (`args`, `async`, `asyncRewake`, `shell`, `url`, `headers`, `allowedEnvVars`, `server`, `tool`, `input`, `prompt`, `model`, `continueOnBlock`), the common `if`, `once`, `statusMessage`, and `timeout` fields, and every documented event the pinned Claude Code 2.1.250 host knows (`PreModelSwitch` and `PostModelSwitch` require 2.1.251 and wait for the re-pin), each closed to the handler types the reference allows for it; the compiler still emits shell-form `command` handlers only. Update the `claude` host's agents capability rows with `color`, `initialPrompt`, and `experimental.cacheTtl`, record that `permissionMode`, `mcpServers`, and `hooks` are ignored for plugin subagents, pin the anchored `^:$` `agent_type` matcher note, and bump the Claude `adapterRevision` to `1.26.0`. (#496) diff --git a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json index ca5f38d16..fb671258e 100644 --- a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json +++ b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json @@ -143,7 +143,7 @@ "emitted": { "command": ["command", "timeout", "type"] }, "evidence": [ "uploaded 2026-09-03: Claude Code docs hooks reference (hooks-2.md, \"Hook handler fields\", \"Common fields\", \"Command hook fields\", \"Exec form and shell form\", \"HTTP hook fields\", \"MCP tool hook fields\", \"Prompt and agent hook fields\", \"Prompt-based hooks\", \"Agent hook configuration\", \"Run hooks in the background\") documents the five handler types, the common if/timeout/statusMessage/once fields, and the per-type fields pinned in schemas/claude/hooks.schema.json (#477); `async` and `args` belong to command hooks only, and agent hooks have no continueOnBlock.", - "2026-09-03: Claude Code 2.1.260 `claude plugin validate --strict --json` accepts a hooks/hooks.json using every documented type and field (tests/fixtures/claude-hooks-schema/cases/full.json) and rejects under --strict a missing per-type required field, an unknown handler type, an unknown event, and each wrong-typed field; it ignores unknown hooks.json keys and cross-type fields without a warning, which the closed pinned schema rejects instead (schemas/claude/PROVENANCE.json hooksSchemaNotes).", + "2026-09-03: the pinned Claude Code 2.1.250 (`claude plugin validate --strict`, text reporter) and 2.1.260 (`--strict --json`) both accept a hooks/hooks.json using every documented type and field (tests/fixtures/claude-hooks-schema/cases/full.json) and both reject under --strict a missing per-type required field, an unknown handler type, an unknown event, and each wrong-typed field; both ignore unknown hooks.json keys and cross-type fields without a warning, which the closed pinned schema rejects instead. They disagree only on PreModelSwitch and PostModelSwitch, which the reference gates to v2.1.251 or later and 2.1.250 rejects as unknown keys, so the schema omits them until the re-pin (schemas/claude/PROVENANCE.json hooksSchemaNotes).", "2026-09-03: the compiler emits only shell-form command handlers carrying type, command, and timeout (hook-contract planHooks); args (exec form, the reference's recommendation for path placeholders such as ${CLAUDE_PLUGIN_ROOT}), async, asyncRewake, shell, if, once, statusMessage, and the http/mcp_tool/prompt/agent handlers reach a bundle only through an authored `claude.nativeHooks` document, validated against the pinned schema and merged after the generated groups." ] }, diff --git a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json index 02d499e4d..f55e1ce6a 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json @@ -11,11 +11,11 @@ "sourceMatrixNotes": "Source-matrix addendum (retrieved 2026-09-02): marketplace.schema.json now admits the documented relative, github, git URL, git-subdir, npm, archive, and command plugin sources as closed alternatives. Agent Bundle tightens relative paths and git-subdir paths against lexical escapes; GitHub shorthand to one owner/repo pair; git URLs to HTTPS or git@ forms; npm names to npm's lowercase package grammar and npm versions to exact, ^, or ~ semantic versions; registry URLs to HTTP(S); archive URLs to HTTPS with lexical rejection for localhost, IPv4/IPv6 loopback, 169.254/16 and fe80::/10 link-local addresses, and known AWS, Azure, and Google metadata hostnames; git sha to 40 hex; archive sha256 to 64 case-insensitive hex; command text to 1-500 printable ASCII characters without four-space runs; timeout to integer seconds from 1 through 600; and mode to copy or link. Entry headers remain a nonempty record of nonempty string values, and headersHelper requires a nonempty command plus strict:false. These checks do not resolve DNS, follow redirects, contact registries or repositories, download archives, verify archive bytes, execute command sources, inspect produced directories, or attempt to predict Windows link-mode support; those remain native install-time behavior.", "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment. Drift addendum (#478, uploaded 2026-09-03 sub-agents reference sub-agents-3.md, \"Supported frontmatter fields\"): the contract gained color (red, blue, green, yellow, purple, orange, pink, cyan), initialPrompt (main-session agents via --agent or the agent setting), and experimental.cacheTtl (5m or 1h, Claude Code 2.1.248 or later, read only from subagent files); permissionMode, mcpServers, and hooks are each documented as \"Ignored for plugin subagents\"; name may not contain a colon (reserved for plugin-scoped identifiers, unloaded since 2.1.218) and hooks receive it as agent_type. \"Project-level hooks for subagent events\" and the hooks reference's SubagentStart matcher row give the plugin-scoped matcher form ^:$, anchored because the colon puts it on the unanchored regular-expression path. claude-2.1.250.json plugin.agents and hooks.agentTypeMatcher record these rows; the component stays deferred.", "hooksManifestNotes": "Manifest `hooks` addendum (2026-09-03, Claude Code plugins reference \"Plugin manifest schema\" > \"Component path fields\" and \"Plugin directory structure\" > \"File locations reference\"; #462/#463, adapter fix in #470): the default hooks location is `hooks/hooks.json`, which Claude Code loads on its own, and the manifest `hooks` field (string|array|object) is documented as \"Hook config paths or inline config\" with the example `./my-extra-hooks.json`, i.e. additional hook files. Claude Code 2.1.250, 2.1.251, 2.1.257, and 2.1.259 (`claude --plugin-dir plugin list --json`, observed 2026-09-03) all refuse a plugin whose manifest names the auto-loaded file again with `errors: [\"Hook load failed: Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file /hooks/hooks.json. The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files.\"]`; `claude plugin validate --strict` and `claude plugin details` accept that manifest, so only the list verb exposes the refusal. plugin.schema.json therefore admits the documented `./`-prefixed path, path array, and inline object forms for `hooks` but rejects the literal `./hooks/hooks.json`, and the Claude and unified `plugin` adapters emit the hook document at the default location without a manifest `hooks` key (#470). The same 2.1.259 binary lists exactly one hook set for a plugin shipping `hooks/hooks.json` beside `hooks/hooks-cursor.json` and no manifest `hooks` key, so the unified bundle needs no pointer to keep Claude Code off the Cursor document.", - "hooksSchemaNotes": "hooks.schema.json was re-pinned (uploaded 2026-09-03 hooks reference hooks-2.md, #477) from the single shell-form command handler to the documented handler union: \"Hook handler fields\" names five types (command, http, mcp_tool, prompt, agent); \"Common fields\" gives every type if, timeout, statusMessage, and once; \"Command hook fields\" adds command, args (exec form), async, asyncRewake, and shell (bash or powershell); \"HTTP hook fields\" adds url, headers, and allowedEnvVars; \"MCP tool hook fields\" adds server, tool, and input; \"Prompt and agent hook fields\" adds prompt and model, with continueOnBlock on prompt hooks only (\"Agent hook configuration\": agent hooks have no continueOnBlock field). Each handler carries the documented required fields (command; url; server and tool; prompt) and is closed. The `hooks` map is closed to the 33 events under \"Hook events\", and \"Prompt-based hooks\" partitions them: 13 events admit all five types, 18 admit command, http, and mcp_tool, and SessionStart and Setup admit command and mcp_tool only. Cross-check against Claude Code 2.1.260 `claude plugin validate --strict --json` (tests/fixtures/claude-hooks-schema, recorded by scripts/record-claude-hooks-schema-fixtures.mjs): the CLI accepts a document using every type and field, knows all 33 events, and rejects under --strict an unknown event, an unknown handler type, a missing type, each missing required field, and each wrong-typed field (timeout 0 or a string, shell other than bash/powershell, non-string args, non-object headers or input, non-array allowedEnvVars, non-boolean async/once, non-string if/statusMessage/matcher, a non-string description, and a document without `hooks`), all of which the schema rejects too. Agent Bundle tightenings the CLI does not share: unknown keys on the document, a matcher group, or a handler are rejected (plugins-reference \"plugin validate\" documents unrecognized manifest fields as warnings that --strict promotes to errors, but 2.1.260 --strict ignores unknown hooks.json keys silently, so the closed pin is the only guard against a misspelled field); a field of another handler type (args or async on http, continueOnBlock on agent, model or url on command, shell or asyncRewake on mcp_tool) is rejected rather than silently ignored; a prompt, agent, or http handler on an event the reference excludes it from is rejected although the CLI accepts it; and the pre-existing minLength 1 on command (now also server, tool, prompt, url, model), integer timeout (the hook contract lowers timeoutMs to whole seconds; the CLI accepts 1.5), and minItems 1 on a group's hooks array stay, where the CLI accepts empty strings, fractional timeouts, and empty groups. The undocumented top-level `modules` key the 2.1.260 validator mentions is not admitted. The compiler still emits only shell-form command handlers with command and timeout; exec form, async, if, and statusMessage are pass-through fields for authored nativeHooks documents until a separate emission increment.", + "hooksSchemaNotes": "hooks.schema.json was re-pinned (uploaded 2026-09-03 hooks reference hooks-2.md, #477) from the single shell-form command handler to the documented handler union: \"Hook handler fields\" names five types (command, http, mcp_tool, prompt, agent); \"Common fields\" gives every type if, timeout, statusMessage, and once; \"Command hook fields\" adds command, args (exec form), async, asyncRewake, and shell (bash or powershell); \"HTTP hook fields\" adds url, headers, and allowedEnvVars; \"MCP tool hook fields\" adds server, tool, and input; \"Prompt and agent hook fields\" adds prompt and model, with continueOnBlock on prompt hooks only (\"Agent hook configuration\": agent hooks have no continueOnBlock field). Each handler carries the documented required fields (command; url; server and tool; prompt) and is closed. The `hooks` map is closed to the 31 events under \"Hook events\" that the pinned 2.1.250 host knows: PreModelSwitch and PostModelSwitch are documented there too but \"require Claude Code v2.1.251 or later\", 2.1.250 --strict rejects both keys (`Invalid key in record`), so they stay out until the observedCliVersion re-pin moves past that gate. \"Prompt-based hooks\" partitions the admitted events: 13 admit all five types, 16 admit command, http, and mcp_tool, and SessionStart and Setup admit command and mcp_tool only. Cross-check against the pinned Claude Code 2.1.250 (`claude plugin validate --strict`; that release has no --json, so its text reporter is recorded into the same report shape) and against 2.1.260 (`--strict --json`), both under tests/fixtures/claude-hooks-schema and recorded by scripts/record-claude-hooks-schema-fixtures.mjs: each CLI accepts a document using every type and field and a document naming all 31 admitted events, and each rejects under --strict an unknown event, an unknown handler type, a missing type, each missing required field, and each wrong-typed field (timeout 0 or a string, shell other than bash/powershell, non-string args, non-object headers or input, non-array allowedEnvVars, non-boolean async/once, non-string if/statusMessage/matcher, a non-string description, and a document without `hooks`), all of which the schema rejects too. The two CLIs disagree on exactly one case, the 2.1.251 model-switch events above. Agent Bundle tightenings neither CLI shares: unknown keys on the document, a matcher group, or a handler are rejected (plugins-reference \"plugin validate\" documents unrecognized manifest fields as warnings that --strict promotes to errors, but both 2.1.250 and 2.1.260 --strict ignore unknown hooks.json keys silently, so the closed pin is the only guard against a misspelled field); a field of another handler type (args or async on http, continueOnBlock on agent, model or url on command, shell or asyncRewake on mcp_tool) is rejected rather than silently ignored; a prompt, agent, or http handler on an event the reference excludes it from is rejected although the CLI accepts it; and the pre-existing minLength 1 on command (now also server, tool, prompt, url, model), integer timeout (the hook contract lowers timeoutMs to whole seconds; the CLI accepts 1.5), and minItems 1 on a group's hooks array stay, where both CLIs accept empty strings, fractional timeouts, and empty groups. The undocumented top-level `modules` key both validators mention is not admitted. The compiler still emits only shell-form command handlers with command and timeout; exec form, async, if, and statusMessage are pass-through fields for authored nativeHooks documents until a separate emission increment.", "schemas": { "hooks.schema.json": { - "bytes": 7229, - "sha256": "70cd71b42bf9f9e03e86dcffb82b50038f92605baadd2fbc12dda488502d5cd7", + "bytes": 7086, + "sha256": "7535806f86481b89b628a0ccf8e34261e6f93163f562c2146ca8dbfe4d95766e", "url": "https://code.claude.com/docs/en/hooks" }, "lsp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json index f14732c15..d1d2a0b1a 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/hooks.schema.json @@ -166,12 +166,10 @@ "PermissionDenied": { "$ref": "#/$defs/allHandlerGroups" }, "PermissionRequest": { "$ref": "#/$defs/allHandlerGroups" }, "PostCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, - "PostModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, "PostToolBatch": { "$ref": "#/$defs/allHandlerGroups" }, "PostToolUse": { "$ref": "#/$defs/allHandlerGroups" }, "PostToolUseFailure": { "$ref": "#/$defs/allHandlerGroups" }, "PreCompact": { "$ref": "#/$defs/nonModelHandlerGroups" }, - "PreModelSwitch": { "$ref": "#/$defs/nonModelHandlerGroups" }, "PreToolUse": { "$ref": "#/$defs/allHandlerGroups" }, "SessionEnd": { "$ref": "#/$defs/nonModelHandlerGroups" }, "SessionStart": { "$ref": "#/$defs/startupHandlerGroups" }, diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index afc90b339..d140e896a 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -105,7 +105,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'hooks', revision: '2.1.250', - sha256: '70cd71b42bf9f9e03e86dcffb82b50038f92605baadd2fbc12dda488502d5cd7', + sha256: '7535806f86481b89b628a0ccf8e34261e6f93163f562c2146ca8dbfe4d95766e', }, { name: 'lsp', diff --git a/packages/agent-bundle/tests/claude-hooks-schema.test.ts b/packages/agent-bundle/tests/claude-hooks-schema.test.ts index d72b93f47..723212ada 100644 --- a/packages/agent-bundle/tests/claude-hooks-schema.test.ts +++ b/packages/agent-bundle/tests/claude-hooks-schema.test.ts @@ -14,11 +14,19 @@ import { normalizeProject } from '../src/config/normalize.ts'; import type { LoadedConfig } from '../src/config/load.ts'; const fixtureRoot = new URL('./fixtures/claude-hooks-schema/', import.meta.url); -/** The Claude CLI whose `plugin validate --strict --json` verdicts are recorded beside the cases. */ -const recordedCliVersion = '2.1.260'; +/** + * The Claude CLI the schema is pinned to (PROVENANCE.json observedCliVersion, the + * exact version CI installs); its `plugin validate --strict` verdicts must be + * recorded beside the cases. Reports from later releases may sit beside them. + */ +const pinnedCliVersion = schemaProvenance.observedCliVersion; +/** The newest release recorded; the reference the schema is drawn from describes it. */ +const latestRecordedCliVersion = '2.1.260'; const validate = createAdapterValidator().compile(hooksSchema); interface RecordedReport { + /** Present when the report was parsed from the text reporter (a CLI without `--json`). */ + readonly reporter?: 'text'; readonly contents: readonly { readonly errors: readonly { readonly message: string }[]; readonly file: string; @@ -61,10 +69,20 @@ const deliberateTightenings: Readonly> = { 'url-on-command': 'closed per-type handler shape: `url` is an http-hook field.', }; +/** + * Cases the reference documents but the pinned host predates: the schema and the + * pinned CLI reject them, a later CLI accepts them, and admitting them waits for + * the re-pin that moves observedCliVersion past the gate. + */ +const newerThanPin: Readonly> = { + 'model-switch-events': 'hooks reference "PreModelSwitch" / "PostModelSwitch": both require Claude Code v2.1.251 or later; 2.1.250 rejects the keys.', +}; + +/** The reference's "Hook events" minus PreModelSwitch and PostModelSwitch, which require v2.1.251 (newerThanPin). */ const documentedEvents = [ 'ConfigChange', 'CwdChanged', 'DirectoryAdded', 'Elicitation', 'ElicitationResult', 'FileChanged', 'InstructionsLoaded', - 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', 'PostCompact', 'PostModelSwitch', 'PostToolBatch', - 'PostToolUse', 'PostToolUseFailure', 'PreCompact', 'PreModelSwitch', 'PreToolUse', 'SessionEnd', 'SessionStart', 'Setup', 'Stop', + 'MessageDisplay', 'Notification', 'PermissionDenied', 'PermissionRequest', 'PostCompact', 'PostToolBatch', + 'PostToolUse', 'PostToolUseFailure', 'PreCompact', 'PreToolUse', 'SessionEnd', 'SessionStart', 'Setup', 'Stop', 'StopFailure', 'SubagentStart', 'SubagentStop', 'TaskCompleted', 'TaskCreated', 'TeammateIdle', 'UserPromptExpansion', 'UserPromptSubmit', 'WorktreeCreate', 'WorktreeRemove', ] as const; @@ -77,24 +95,38 @@ const startupEvents = ['SessionStart', 'Setup'] as const; const handlerGroup = (handler: Readonly>): unknown => [{ hooks: [handler] }]; -it('agrees with the recorded Claude CLI verdict on every fixture except the recorded tightenings', async () => { +it('agrees with the recorded verdict of the pinned Claude CLI and every later one on each fixture, except the recorded tightenings', async () => { const names = await caseNames(); expect(names.length).toBeGreaterThan(40); - expect(await readdir(new URL('./reports/', fixtureRoot))).toEqual([recordedCliVersion]); - for (const tightening of Object.keys(deliberateTightenings)) expect(names).toContain(tightening); - - for (const name of names) { - const document = await readJson(`./cases/${name}.json`); - const report = await readJson(`./reports/${recordedCliVersion}/${name}.json`); - expect(report.strict, name).toBe(true); - const accepted = validate(document); - if (Object.hasOwn(deliberateTightenings, name)) { - expect(report.success, `${name}: a tightening must be a case Claude accepts`).toBe(true); - expect(accepted, `${name}: the schema must reject what the tightening describes`).toBe(false); - continue; + const recordedVersions = (await readdir(new URL('./reports/', fixtureRoot))).sort(); + expect(pinnedCliVersion).toBe('2.1.250'); + expect(recordedVersions).toEqual([pinnedCliVersion, latestRecordedCliVersion]); + for (const tightening of [...Object.keys(deliberateTightenings), ...Object.keys(newerThanPin)]) expect(names).toContain(tightening); + + for (const version of recordedVersions) { + const pinned = version === pinnedCliVersion; + expect(await readdir(new URL(`./reports/${version}/`, fixtureRoot)), version).toEqual(names.map((name) => `${name}.json`)); + for (const name of names) { + const document = await readJson(`./cases/${name}.json`); + const report = await readJson(`./reports/${version}/${name}.json`); + const label = `${name} @ ${version}`; + expect(report.strict, label).toBe(true); + // 2.1.250 has no `--json`; its verdicts come from the text reporter. + expect(report.reporter, label).toBe(pinned ? 'text' : undefined); + const accepted = validate(document); + if (Object.hasOwn(deliberateTightenings, name)) { + expect(report.success, `${label}: a tightening must be a case Claude accepts`).toBe(true); + expect(accepted, `${label}: the schema must reject what the tightening describes`).toBe(false); + continue; + } + if (Object.hasOwn(newerThanPin, name)) { + expect(accepted, `${label}: the schema must not admit what the pinned host rejects`).toBe(false); + expect(report.success, `${label}: newer-than-pin cases are rejected by the pin and accepted after it`).toBe(!pinned); + continue; + } + expect(accepted, `${label}: schema=${accepted} claude=${report.success} ${JSON.stringify(validate.errors ?? report.contents)}`) + .toBe(report.success); } - expect(accepted, `${name}: schema=${accepted} claude=${report.success} ${JSON.stringify(validate.errors ?? report.contents)}`) - .toBe(report.success); } }); @@ -112,7 +144,7 @@ it('accepts every documented handler type and field, and every documented event, expect(fields.has(field), field).toBe(true); } - const allEvents = await readJson<{ readonly hooks: Record }>('./cases/all-documented-events.json'); + const allEvents = await readJson<{ readonly hooks: Record }>('./cases/all-pinned-events.json'); expect(validate(allEvents), JSON.stringify(validate.errors)).toBe(true); expect(Object.keys(allEvents.hooks).sort()).toEqual([...documentedEvents]); expect(Object.keys(hooksSchema.properties.hooks.properties).sort()).toEqual([...documentedEvents]); @@ -169,13 +201,15 @@ it('keeps the pinned schema descriptor and the capability evidence in step with const descriptor = claude.metadata.schemas.find((schema) => schema.name === 'hooks'); expect(descriptor?.sha256).toBe(schemaProvenance.schemas['hooks.schema.json'].sha256); expect(schemaProvenance.hooksSchemaNotes).toContain('hooks.schema.json was re-pinned (uploaded 2026-09-03'); - expect(schemaProvenance.hooksSchemaNotes).toContain('Claude Code 2.1.260'); + expect(schemaProvenance.hooksSchemaNotes).toContain('Claude Code 2.1.250'); + expect(schemaProvenance.hooksSchemaNotes).toContain('2.1.260'); + expect(schemaProvenance.hooksSchemaNotes).toContain('PreModelSwitch'); const handlerContract = claudeCapabilityTable.hooks.handlerContract; expect(handlerContract.types).toEqual(['agent', 'command', 'http', 'mcp_tool', 'prompt']); expect(handlerContract.emitted).toEqual({ command: ['command', 'timeout', 'type'] }); expect(handlerContract.evidence.some((line) => line.startsWith('uploaded 2026-09-03:') && line.includes('hooks-2.md'))).toBe(true); - expect(handlerContract.evidence.some((line) => line.includes('Claude Code 2.1.260'))).toBe(true); + expect(handlerContract.evidence.some((line) => line.includes('Claude Code 2.1.250') && line.includes('2.1.260'))).toBe(true); for (const feature of ['timeout', 'toolMatchers'] as const) { expect(claudeCapabilityTable.hooks.features[feature].evidence.some((line) => line.includes('every handler'))).toBe(true); } diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json similarity index 93% rename from packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json rename to packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json index addc2cf74..1aefbf9b5 100644 --- a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-documented-events.json +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/all-pinned-events.json @@ -280,26 +280,6 @@ ] } ], - "PreModelSwitch": [ - { - "hooks": [ - { - "type": "command", - "command": "echo PreModelSwitch" - } - ] - } - ], - "PostModelSwitch": [ - { - "hooks": [ - { - "type": "command", - "command": "echo PostModelSwitch" - } - ] - } - ], "SessionEnd": [ { "hooks": [ diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-switch-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-switch-events.json new file mode 100644 index 000000000..cb3c8b20f --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/cases/model-switch-events.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "PreModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PreModelSwitch" + } + ] + } + ], + "PostModelSwitch": [ + { + "hooks": [ + { + "type": "command", + "command": "echo PostModelSwitch" + } + ] + } + ] + } +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-missing-prompt.json new file mode 100644 index 000000000..b1806338a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-missing-prompt.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.prompt", + "message": "Invalid input: expected string, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-on-session-start.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/agent-on-session-start.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/all-pinned-events.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/allowed-env-vars-not-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/allowed-env-vars-not-array.json new file mode 100644 index 000000000..cb9d68be4 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/allowed-env-vars-not-array.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.allowedEnvVars", + "message": "Invalid input: expected array, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-not-strings.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-not-strings.json new file mode 100644 index 000000000..e29624e68 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-not-strings.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.args.0", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-on-http.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-on-http.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-string.json new file mode 100644 index 000000000..148cea561 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/args-string.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.args", + "message": "Invalid input: expected array, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-http.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-http.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-http.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-prompt.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-on-prompt.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-rewake-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-rewake-on-mcp.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-rewake-on-mcp.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-string.json new file mode 100644 index 000000000..4625dc978 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/async-string.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.async", + "message": "Invalid input: expected boolean, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-agent.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-agent.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-agent.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-command.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/continue-on-block-on-command.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/description-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/description-number.json new file mode 100644 index 000000000..0b73d9c2e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/description-number.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "description", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-command.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-command.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-hooks-array.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-hooks-array.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/empty-hooks-array.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/full.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/full.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/full.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/group-missing-hooks.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/group-missing-hooks.json new file mode 100644 index 000000000..f6f57790f --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/group-missing-hooks.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks", + "message": "Invalid input: expected array, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-not-object.json new file mode 100644 index 000000000..2d5433068 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-not-object.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.headers", + "message": "Invalid input: expected record, received array", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-value-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-value-number.json new file mode 100644 index 000000000..a016322e9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/headers-value-number.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.headers.a", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/hooks-empty-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/hooks-empty-object.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/hooks-empty-object.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-missing-url.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-missing-url.json new file mode 100644 index 000000000..d2c86a100 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-missing-url.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.url", + "message": "Invalid input: expected string, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-on-session-start.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/http-on-session-start.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/if-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/if-number.json new file mode 100644 index 000000000..8d5bd8d58 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/if-number.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.if", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/input-not-object.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/input-not-object.json new file mode 100644 index 000000000..0c9a30d4e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/input-not-object.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.input", + "message": "Invalid input: expected record, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/matcher-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/matcher-number.json new file mode 100644 index 000000000..2dd73538c --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/matcher-number.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.matcher", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/mcp-tool-missing-tool.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/mcp-tool-missing-tool.json new file mode 100644 index 000000000..64bd2f11e --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/mcp-tool-missing-tool.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.tool", + "message": "Invalid input: expected string, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-command.json new file mode 100644 index 000000000..bcd050875 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-command.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.command", + "message": "Invalid input: expected string, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-type.json new file mode 100644 index 000000000..8748d2db8 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/missing-type.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.type", + "message": "Invalid input", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-on-command.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-on-command.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-switch-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-switch-events.json new file mode 100644 index 000000000..7d92e8404 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/model-switch-events.json @@ -0,0 +1,33 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.PreModelSwitch", + "message": "Invalid key in record", + "code": null + }, + { + "path": "hooks.PostModelSwitch", + "message": "Invalid key in record", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/no-hooks-key.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/no-hooks-key.json new file mode 100644 index 000000000..1de031da8 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/no-hooks-key.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "root", + "message": "hooks.json must have `hooks` (the hook matchers) or `modules` (hooks modules), or both", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-false-async-false.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-false-async-false.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-false-async-false.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-string.json new file mode 100644 index 000000000..ddd5a13f7 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/once-string.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.once", + "message": "Invalid input: expected boolean, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-empty.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-empty.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-missing-prompt.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-missing-prompt.json new file mode 100644 index 000000000..b1806338a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-missing-prompt.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.prompt", + "message": "Invalid input: expected string, received undefined", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-session-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-session-start.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-session-start.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-subagent-start.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-subagent-start.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/prompt-on-subagent-start.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/server-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/server-empty.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/server-empty.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-invalid.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-invalid.json new file mode 100644 index 000000000..e12468305 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-invalid.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.shell", + "message": "Invalid option: expected one of \"bash\"|\"powershell\"", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-on-mcp.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-on-mcp.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/shell-on-mcp.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/status-message-number.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/status-message-number.json new file mode 100644 index 000000000..0f68d0c82 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/status-message-number.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.statusMessage", + "message": "Invalid input: expected string, received number", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-float.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-float.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-float.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-string.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-string.json new file mode 100644 index 000000000..1014cc4fc --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-string.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.timeout", + "message": "Invalid input: expected number, received string", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-zero.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-zero.json new file mode 100644 index 000000000..7b7a4337d --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/timeout-zero.json @@ -0,0 +1,33 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.timeout", + "message": "Too small: expected number to be >0", + "code": null + }, + { + "path": "root", + "message": "hooks.json must have `hooks` (the hook matchers) or `modules` (hooks modules), or both", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-event.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-event.json new file mode 100644 index 000000000..0019216b2 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-event.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.FooEvent", + "message": "Invalid key in record", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-group-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-group-field.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-group-field.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-handler-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-handler-field.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-handler-field.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-top-level-field.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-top-level-field.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-top-level-field.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-type.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-type.json new file mode 100644 index 000000000..8748d2db8 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/unknown-type.json @@ -0,0 +1,28 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.type", + "message": "Invalid input", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-empty.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-empty.json new file mode 100644 index 000000000..9fb81c5d9 --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-empty.json @@ -0,0 +1,33 @@ +{ + "success": false, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [ + { + "file": "/bundle/claude/hooks/hooks.json", + "type": "hooks", + "errors": [ + { + "path": "hooks.Stop.0.hooks.0.url", + "message": "Invalid URL", + "code": null + }, + { + "path": "root", + "message": "hooks.json must have `hooks` (the hook matchers) or `modules` (hooks modules), or both", + "code": null + } + ], + "warnings": [], + "notes": [] + } + ] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-on-command.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-on-command.json new file mode 100644 index 000000000..bab52146a --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.250/url-on-command.json @@ -0,0 +1,14 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "reporter": "text", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-pinned-events.json similarity index 100% rename from packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-documented-events.json rename to packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/all-pinned-events.json diff --git a/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-switch-events.json b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-switch-events.json new file mode 100644 index 000000000..725ce46ec --- /dev/null +++ b/packages/agent-bundle/tests/fixtures/claude-hooks-schema/reports/2.1.260/model-switch-events.json @@ -0,0 +1,13 @@ +{ + "success": true, + "strict": true, + "target": "/bundle/claude/.claude-plugin/plugin.json", + "manifest": { + "file": "/bundle/claude/.claude-plugin/plugin.json", + "type": "plugin", + "errors": [], + "warnings": [], + "notes": [] + }, + "contents": [] +} diff --git a/scripts/record-claude-hooks-schema-fixtures.mjs b/scripts/record-claude-hooks-schema-fixtures.mjs index c2dfe06b3..4666a4f1a 100644 --- a/scripts/record-claude-hooks-schema-fixtures.mjs +++ b/scripts/record-claude-hooks-schema-fixtures.mjs @@ -7,10 +7,17 @@ * `claude plugin validate /.claude-plugin/plugin.json --strict --json`; * the JSON report lands in `reports//.json` with the * temporary plugin path replaced by `/bundle/claude`, the placeholder the other - * recorded Claude reports under tests/fixtures use. The run needs no login: it - * points CLAUDE_CONFIG_DIR at an empty directory. + * recorded Claude reports under tests/fixtures use. A CLI without `--json` + * (the pinned 2.1.250 has only the text reporter) is recorded from its text + * output into the same shape, marked `"reporter": "text"`. The run needs no + * login: it points CLAUDE_CONFIG_DIR at an empty directory. * * node scripts/record-claude-hooks-schema-fixtures.mjs + * + * CLAUDE_BIN selects the binary (default: `claude` on PATH), so the pinned + * PROVENANCE.json observedCliVersion can be recorded from a scratch install + * (`npm install --prefix @anthropic-ai/claude-code@`) beside a newer + * release without touching the global installation. */ import { execFile as executeFile } from 'node:child_process'; import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; @@ -23,6 +30,7 @@ const execFile = promisify(executeFile); const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const fixtureRoot = join(repositoryRoot, 'packages/agent-bundle/tests/fixtures/claude-hooks-schema'); const pluginPathPlaceholder = '/bundle/claude'; +const claudeBinary = process.env.CLAUDE_BIN ?? 'claude'; /** The manifest every case shares; `author` keeps `--strict` from failing on the attribution warning. */ const manifest = { @@ -34,7 +42,7 @@ const manifest = { const claude = async (args, env) => { try { - const { stdout, stderr } = await execFile('claude', args, { encoding: 'utf8', env }); + const { stdout, stderr } = await execFile(claudeBinary, args, { encoding: 'utf8', env }); return { exitCode: 0, stderr, stdout }; } catch (error) { if (typeof error?.code !== 'number') throw error; @@ -42,6 +50,54 @@ const claude = async (args, env) => { } }; +/** `--json` where the CLI has it; otherwise the text reporter, parsed into the JSON reporter's shape. */ +let jsonReporter = true; +const validateReport = async (manifestPath, pluginRoot, env) => { + const normalize = (text) => text.replaceAll(pluginRoot, pluginPathPlaceholder); + if (jsonReporter) { + const result = await claude(['plugin', 'validate', manifestPath, '--strict', '--json'], env); + if (!/unknown option '--json'/u.test(result.stderr)) { + if (result.exitCode === 2 || result.stdout.trim().length === 0) { + throw new Error(`claude plugin validate did not produce a report for ${manifestPath} (exit ${result.exitCode}): ${result.stderr}`); + } + return { ...JSON.parse(normalize(result.stdout)), exitCodeSuccess: result.exitCode === 0 }; + } + jsonReporter = false; + } + const result = await claude(['plugin', 'validate', manifestPath, '--strict'], env); + const text = normalize(`${result.stdout}\n${result.stderr}`); + if (!/Validation (passed|failed)/u.test(text)) { + throw new Error(`claude plugin validate did not produce a verdict for ${manifestPath} (exit ${result.exitCode}): ${text}`); + } + const success = /Validation passed/u.test(text); + // Findings are " ❯ : " lines under "Found N error(s)" / "Found N warning(s)". + const contents = []; + let current; + for (const line of text.split('\n')) { + const validating = /^Validating (\w+): (.+)$/u.exec(line.trim()); + if (validating) { + current = { file: validating[2], type: validating[1] === 'plugin' ? 'plugin' : validating[1], errors: [], warnings: [], notes: [] }; + contents.push(current); + continue; + } + const heading = /Found \d+ (error|warning)s?:/u.exec(line); + if (heading) { current.findings = heading[1] === 'error' ? current.errors : current.warnings; continue; } + const finding = /^\s*❯ (\S+): (.+)$/u.exec(line); + if (finding && current?.findings) current.findings.push({ path: finding[1], message: finding[2], code: null }); + } + for (const content of contents) delete content.findings; + const manifest = contents.find((content) => content.type === 'plugin') ?? { file: normalize(manifestPath), type: 'plugin', errors: [], warnings: [], notes: [] }; + return { + success, + strict: true, + target: normalize(manifestPath), + reporter: 'text', + manifest, + contents: contents.filter((content) => content !== manifest), + exitCodeSuccess: result.exitCode === 0, + }; +}; + const scratch = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-hooks-schema-')); try { const env = { ...process.env, CLAUDE_CONFIG_DIR: join(scratch, 'config') }; @@ -62,12 +118,9 @@ try { await writeFile(join(pluginRoot, 'hooks', 'hooks.json'), await readFile(join(fixtureRoot, 'cases', file), 'utf8')); const manifestPath = join(pluginRoot, '.claude-plugin', 'plugin.json'); - const result = await claude(['plugin', 'validate', manifestPath, '--strict', '--json'], env); - if (result.exitCode === 2 || result.stdout.trim().length === 0) { - throw new Error(`claude plugin validate did not produce a report for ${name} (exit ${result.exitCode}): ${result.stderr}`); - } - const report = JSON.parse(result.stdout.replaceAll(pluginRoot, pluginPathPlaceholder)); - if (report.success !== (result.exitCode === 0)) disagreements += 1; + const report = await validateReport(manifestPath, pluginRoot, env); + if (report.success !== report.exitCodeSuccess) disagreements += 1; + delete report.exitCodeSuccess; await writeFile(join(reportRoot, file), `${JSON.stringify(report, null, 2)}\n`); const findings = (report.contents ?? []).flatMap((content) => [ ...(content.errors ?? []).map((finding) => `error ${finding.path}: ${finding.message}`), diff --git a/website/docs/en/guide/authoring/index.mdx b/website/docs/en/guide/authoring/index.mdx index 5ae432941..b285e775c 100644 --- a/website/docs/en/guide/authoring/index.mdx +++ b/website/docs/en/guide/authoring/index.mdx @@ -84,7 +84,8 @@ Every host key is optional and every field inside it is optional. `claude` and ` the shared `AgentBundleHostConfig`, whose single field `nativeHooks` names a project-authored, target-native hook document (`hooks.json`) the adapter validates and merges beside the compiled hooks. For Claude Code the pinned schema admits the documented handler contract: every hook event -the hooks reference lists, the five handler types (`command`, `http`, `mcp_tool`, `prompt`, +the pinned 2.1.250 host knows (the reference's list minus `PreModelSwitch` and `PostModelSwitch`, +which require 2.1.251), the five handler types (`command`, `http`, `mcp_tool`, `prompt`, `agent`), their per-type fields (`args`, `async`, `shell`, `url`, `headers`, `server`, `tool`, `input`, `prompt`, `model`, …), and the common `if`, `once`, `statusMessage`, and `timeout` fields; unknown keys, a field of another handler type, and a prompt/agent/http handler on an event diff --git a/website/docs/zh/guide/authoring/index.mdx b/website/docs/zh/guide/authoring/index.mdx index b557f662b..653c119a7 100644 --- a/website/docs/zh/guide/authoring/index.mdx +++ b/website/docs/zh/guide/authoring/index.mdx @@ -79,7 +79,8 @@ export default defineConfig({ 每个宿主键都是可选的,其中的每个字段也都是可选的。`claude` 与 `codex` 扩展自共享的 `AgentBundleHostConfig`,它唯一的字段 `nativeHooks` 指向一份由项目编写、target 原生的钩子文档 (`hooks.json`),适配器会校验它并与编译出的钩子合并。对 Claude Code 而言,固定的 schema 接纳文档化的 -处理器契约:hooks 参考列出的每个钩子事件、五种处理器类型(`command`、`http`、`mcp_tool`、`prompt`、 +处理器契约:固定的 2.1.250 宿主认识的每个钩子事件(即参考文档列出的事件,去掉需要 2.1.251 的 +`PreModelSwitch` 与 `PostModelSwitch`)、五种处理器类型(`command`、`http`、`mcp_tool`、`prompt`、 `agent`)、各类型自己的字段(`args`、`async`、`shell`、`url`、`headers`、`server`、`tool`、`input`、 `prompt`、`model` 等)以及公共字段 `if`、`once`、`statusMessage` 与 `timeout`;未知键、属于另一种处理器 类型的字段,以及参考文档明确排除的事件上的 prompt/agent/http 处理器都会被拒绝。编译器自身仍只输出