Skip to content

fix(plugins): use Claude Code hooks schema in plugin hook manifests - #1

Open
rangulvers wants to merge 1 commit into
mnott:mainfrom
rangulvers:fix/claude-code-hooks-schema
Open

fix(plugins): use Claude Code hooks schema in plugin hook manifests#1
rangulvers wants to merge 1 commit into
mnott:mainfrom
rangulvers:fix/claude-code-hooks-schema

Conversation

@rangulvers

Copy link
Copy Markdown

Problem

Installing PAI via Claude Code's plugin marketplace (or any other consumer that follows .claude-plugin/plugin.json / pai-plugin.json) currently produces a settings validation error on every Claude Code start:

Settings Error
~/.claude/settings.json
 └ hooks
   ├ PostToolUse
   │ ├ 0
   │ │ └ hooks: Expected array, but received undefined
   │ ├ 1
   │ │ └ hooks: Expected array, but received undefined
   │   ...

with three forced choices: Fix with Claude / Exit and fix manually / Continue without these settings. "Continue without these settings" silently disables every PAI hook, so PAI looks installed but is effectively dead.

Root cause

.claude-plugin/plugin.json (and pai-plugin.json) reference the per-module hook manifests:

{ "hooks": "plugins/core/hooks/hooks.json", ... }

Those files were written in PAI's internal manifest shape:

{
  "module": "core",
  "description": "Essential lifecycle hooks ...",
  "hooks": [
    { "event": "SessionStart", "command": "${PAI_DIR}/Hooks/load-core-context.mjs", "description": "..." },
    { "event": "PreToolUse", "matcher": "Bash", "command": "${PAI_DIR}/Hooks/security-validator.mjs", "description": "..." }
  ]
}

Claude Code's plugin loader expects hooks.json to use the same schema as ~/.claude/settings.json hooks — an object keyed by event name, where each rule is { matcher?, hooks: [{ type: "command", command }] }. When the loader encounters PAI's shape it partially merges the entries into settings.json without the inner hooks array, producing the broken state above.

The pai setup CLI path was unaffected because mergeHooks() in src/cli/commands/settings-manager.ts already builds the correct shape — only the marketplace/plugin-manifest path was broken.

Fix

Convert all plugins/*/hooks/hooks.json to Claude Code's expected schema. Same hooks, same matchers, same commands — just wrapped correctly:

{
  "SessionStart": [
    { "hooks": [ { "type": "command", "command": "${PAI_DIR}/Hooks/load-core-context.mjs" } ] }
  ],
  "PreToolUse": [
    { "matcher": "Bash", "hooks": [ { "type": "command", "command": "${PAI_DIR}/Hooks/security-validator.mjs" } ] }
  ]
}

5 files touched: plugins/{core,productivity,ui,context-preservation,observability}/hooks/hooks.json. 26 hook rules total, all validated against the schema (object keyed by event → array of rules → each with hooks: [{type:"command", command:string}]).

Validation

  • ✅ JSON parses cleanly in every file
  • ✅ Every rule conforms to {matcher?, hooks: [{type:"command", command:string}]}
  • ✅ All event names are valid Claude Code hook events (SessionStart, SessionEnd, PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact)
  • ✅ No commands, matchers, or matchers-empty-string semantics were changed
  • ✅ Manually verified that the resulting ~/.claude/settings.json no longer triggers the validation panel and that claude starts cleanly

Notes

  • The per-hook description strings inside the old format were not preserved — Claude Code's schema has no slot for them. The hook script files are self-documenting and the higher-level mapping is already covered in PLUGIN-ARCHITECTURE.md ("Hook Distribution" section). Happy to add them as a sidecar *.meta.json or as JSON Schema-aware comments if you prefer.
  • A possible follow-up: add a one-shot migration that detects already-installed users with the broken shape and rewrites their ~/.claude/settings.json in place, since otherwise existing installations continue to fail until the user nukes and reinstalls.

Each plugins/<module>/hooks/hooks.json was using PAI's internal manifest
shape with 'module', 'description', and a flat list of
{event, matcher, command, description} entries. Claude Code's plugin
loader expects hooks.json to follow the same schema as settings.json:
an object keyed by event name, where each rule is
{ matcher?, hooks: [ { type: 'command', command } ] }.

When users installed PAI via Claude Code's plugin marketplace (which
references these files from .claude-plugin/plugin.json and
pai-plugin.json), the loader silently flattened the entries into a
broken shape ({matcher, command} without the inner 'hooks' array),
producing the validation panel:

    Settings Error
    /Users/<user>/.claude/settings.json
    └ hooks
      ├ PostToolUse
      │ └ 0 → hooks: Expected array, but received undefined
      ...

After conversion every event-keyed list contains valid
{matcher?, hooks: [{type: 'command', command}]} entries that Claude Code
accepts on first start. The 'pai setup' CLI path was already producing
the correct shape via mergeHooks() in settings-manager.ts; this brings
the marketplace path in line.

Touches: plugins/{core,productivity,ui,context-preservation,observability}/hooks/hooks.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant