fix(claude): drop the manifest hooks pointer Claude Code reports as a duplicate; pin every Claude hook_event_name - #470
Merged
Conversation
…k_event_name Claude Code loads `hooks/hooks.json` on its own; `manifest.hooks` is only for additional documents. The `claude` target has named `./hooks/hooks.json` there since b256d44 and #450 added the same pointer to the unified `plugin` bundle. Claude Code 2.1.259 records a `hook-load-failed` plugin error for it: "Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded file ... The standard hooks/hooks.json is loaded automatically, so manifest.hooks should only reference additional hook files" (reproduced with an isolated CLAUDE_CONFIG_DIR against a built Claude pack). #450's stated root cause does not hold: Claude Code never scans `hooks/` for other documents, so `hooks/hooks-cursor.json` is invisible to it. The live "native hook_event_name must equal postToolUse" errors came from the Claude wrapper path (`event-route-tool-after.mjs`, per the session transcript) during a rebuild/reinstall window of a directory marketplace, whose `${CLAUDE_PLUGIN_ROOT}` is the build output itself; only a Cursor-built wrapper bakes that camelCase constant. - claude.ts / plugin.ts: emit no `hooks` field on `.claude-plugin/plugin.json`. - tests/claude-hook-event-name.test.ts: for every supported Claude event route, the planned wrapper bakes the pinned PascalCase name, the hooks document is keyed by it, and a real Claude envelope passes validateNativeEventEnvelope; the live PostToolUse:Bash envelope is accepted by the Claude wrapper and rejected with the exact observed message only under a Cursor-baked validation; the unified bundle keeps `.mjs`/`.cursor.mjs` spellings apart; no Claude manifest pointer. - docs (en+zh hooks.mdx, installation.mdx) state the loader behavior.
🦋 Changeset detectedLatest commit: fd94af0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This was referenced Sep 3, 2026
ScriptedAlchemy
added a commit
that referenced
this pull request
Sep 4, 2026
…stall (AB7325, AB7006); reject the auto-loaded hooks path in the Claude manifest schema (#479) * fix(install): surface Claude plugin list --json errors in doctor and install; reject the auto-loaded hooks path in the manifest schema Claude Code lists a plugin it refused to load with an `errors` array on its `plugin list --json` row (the row stays `enabled: true`; healthy rows omit the key). `doctor` and `install` read only id/installPath/scope/version from that row, so a refused plugin — the cargo-hauler#48 case, where the manifest `hooks` pointer at the auto-loaded `hooks/hooks.json` made Claude Code drop every hook, MCP server, and skill — was reported `installed` / `current` (#464). - install.ts: `claudePluginRowErrors` reads the array into `PublicHostInstalledEntry.errors`; `installBundle` throws `AB7006` when the byte-identical existing copy carries errors (reinstalling cannot help) and re-reads the listing after `claude plugin install` (which exits 0 for a plugin Claude Code then refuses) to fail the same way. - doctor.ts: inventory entries with errors are `failed`; the `--from` comparison is `load-failed` (`AB7325`, error) instead of `current`/`stale`; the `--plugin-dir` registration proof is `failed` with the host's text. - cli.ts: text report prints the `load-failed` comparison. - schemas/claude/plugin.schema.json: `hooks` admits the documented additional-hook-file forms (`./` path, array, inline object) and rejects the literal `./hooks/hooks.json`; PROVENANCE and the Claude capability table record the evidence (refused on 2.1.250, 2.1.251, 2.1.257, 2.1.259; accepted by `plugin validate --strict`). Claude adapterRevision 1.25.0. - tests: doctor/install fixtures use the verbatim 2.1.259 row shape; claude-plugin-validate-acceptance.test.ts emits the cargo-hauler shape and, when `claude` is on PATH, runs `plugin validate --strict --json` and `--plugin-dir plugin list --json` under an isolated CLAUDE_CONFIG_DIR. - docs: diagnostics.md (`AB7006`, `AB7325`, comparison matrix), en/zh cli, installation, and validation pages. The adapter half (#462/#463) landed in #470 (5775351). * chore(changeset): reference #479 * fix(review): gate the Claude acceptance test in CI, version-gate validate --json, reject alias paths in the schema - package.json: `test:host-install` (the CI job with the pinned Claude CLI on PATH) now runs claude-plugin-validate-acceptance.test.ts, so the real-host load verdict gates CI instead of skipping in the Verify pool. - The acceptance test reads `claude --version` and uses `plugin validate --strict --json` only from 2.1.259; the pinned 2.1.250 rejects the flag, so older binaries run the textual `--strict` form and are held to its exit code and "Validation passed" line. Verified against 2.1.250, 2.1.259, and no binary (skips with the missing-evidence title). - plugin.schema.json `additionalHooksPath`: the pattern admits only normalized `./`-prefixed paths (no `.`, `..`, or empty segments), so aliases of the auto-loaded file such as `./hooks/./hooks.json` or `./hooks/../hooks/hooks.json` fail the segment rule rather than slipping past the literal exclusion; PROVENANCE/metadata pins re-hashed, tests cover the aliases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
The maintainer's real Claude Code session (2.1.257/2.1.259) failed every hook between 2026-09-03 20:47:53Z and 20:49:29Z:
(and
preToolUseforPreToolUse:Bash,stopforStop). #450 landed under this lane with the explanation that Claude Code was "discoveringhooks/hooks-cursor.json" and invoking the Cursor wrappers, and fixed it by naming./hooks/hooks.jsonin.claude-plugin/plugin.json.Root cause
Two findings, both verified against the installed Claude Code binary rather than inferred:
Claude Code never scans
hooks/. Its plugin loader (~/.local/share/claude/versions/2.1.259) reads exactlyhooks/hooks.jsonfrom the plugin root, then walksmanifest.hooksfor additional files.hooks/hooks-cursor.jsonis invisible to it, so fix(plugin): point Claude plugin.json at hooks/hooks.json so Cursor wrappers are not loaded #450's root cause cannot be what happened. The session transcript (~/.claude/projects/-fast-projects-agent-bundle/36af94c3-….jsonl,hook_non_blocking_errorattachments) confirms the failing command was the Claude wrapper,node "${CLAUDE_PLUGIN_ROOT}/hooks/event-route-tool-after.mjs", not a.cursor.mjsfile. The errors are confined to the window in which thecargo-haulerplugin was being migrated from the compositeplugintarget to per-host packs and re-registered in the real~/.claude(marketplace re-added 20:46:02Z, cache written 20:49:25Z); 182 hook successes precede the window and none fail after it. For adirectorymarketplace Claude Code resolves${CLAUDE_PLUGIN_ROOT}to the build output directory itself (reproduced below), so the wrapper file that ran mid-rebuild is gone — but only a wrapper compiled for thecursortarget bakesconst nativeEvent = "postToolUse". The agent-bundle event contract for Claude was and is correct:capabilities/claude-2.1.250.jsonmaps all 19 supported routes to Claude's PascalCase names andplanHooksbakes exactly those.The
hookspointer fix(plugin): point Claude plugin.json at hooks/hooks.json so Cursor wrappers are not loaded #450 added (and theclaudetarget has emitted since b256d44) is itself a Claude Code plugin error. Reproduced with an isolatedCLAUDE_CONFIG_DIR/HOME, installing a freshly built Claude pack and startingclaude --debug-file:Hooks still register (the auto-loaded copy wins) but the plugin carries a
hook-load-failederror on every start under the defaultstrictmarketplace entry. The official plugins reference documentshooksas"./my-extra-hooks.json"— extra files only.Fix
adapters/claude.ts,adapters/plugin.ts: emit nohooksfield on.claude-plugin/plugin.json(removes fix(plugin): point Claude plugin.json at hooks/hooks.json so Cursor wrappers are not loaded #450'sattachClaudeHookManifestand the pre-existingclaude-target pointer).hooks/hooks.jsonis still emitted and is what Claude Code loads. Codex/Cursor manifests are untouched..changeset/claude-hook-manifest-pointer.md(fix(plugin): point Claude plugin.json at hooks/hooks.json so Cursor wrappers are not loaded #450, unreleased) is removed since its release note describes behavior this PR reverts; one new patch changeset replaces it.guide/authoring/hooks.mdx,guide/distribution/installation.mdx): state the loader behavior, that the Claude manifest carries no pointer, whatmust equal postToolUseon a Claude session actually identifies, and that adirectorymarketplace resolves${CLAUDE_PLUGIN_ROOT}to the directory itself.Tests
New
tests/claude-hook-event-name.test.ts(unit pool):state: "supported"route inclaude-2.1.250.json(19);claude-target wrapper'snativeEvent, its bakedconst nativeEvent = "…", and thehooks/hooks.jsonkey are the pinned PascalCase name, and a real Claude envelope (15 fromfixtures/events/claude-*.json, plus inlineSessionStart/Stop/PreToolUse/PostToolUse) passesvalidateNativeEventEnvelopeunder that wrapper's validation;PostToolUse:Bashenvelope is accepted by the Claude wrapper and rejected with exactlyAgent Bundle event route error: native hook_event_name must equal postToolUseonly under a Cursor-baked validation (andpreToolUseforPreToolUse);pluginbundle keeps.mjs(PostToolUse) and.cursor.mjs(postToolUse) apart and its two hook documents keyed by their own host spellings;claudenor theplugintarget putshookson the Claude manifest; Cursor's manifest still names./hooks/hooks-cursor.json.Adjusted:
plugin-bundle.test.ts,hooks.test.ts(Claude manifest has nohooks), removedplugin-claude-hook-manifest.test.ts(superseded).Evidence
pnpm typecheck— exit 0 (afterpnpm build).pnpm lint— 0 errors, 0 warnings.pnpm test:unit— 3030 passed, 0 failed, 5 skipped.rstest --config rstest.integration.config.ts plugin-bundle.test.ts hooks.test.ts target-hook-contract.test.ts— 58 passed, 0 failed.pnpm docs:site:build— build, dead-link, and language-parity checks pass.hooks/hooks.jsonloaded from the marketplace directory; 4 hooks registered;Duplicate hooks file detectedrecorded for the manifest pointer (the condition this PR removes).Follow-up (not in this PR)
The maintainer's installed
cargo-haulerpack was rebuilt against agent-bundle886b192and is correct today (nativeEvent = "PostToolUse"baked inartifact/claude/hooks/*.mjs); it will stop reporting the duplicate-hooks error once rebuilt on a release containing this change.