[BUG] `/reload-plugins` crashes when a marketplace entry uses `"hooks": "./hooks/hooks.json"`

Open 💬 3 comments Opened Apr 1, 2026 by LukasKubec

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

/reload-plugins crashes after installing a plugin from a marketplace when the marketplace entry declares hooks using the documented string-path form:

"hooks": "./hooks/hooks.json"

Observed error:

TypeError: J?.reduce is not a function. (In 'J?.reduce((X,G)=>X+G.hooks.length,0)', 'J?.reduce' is undefined)

This is a Claude Code bug, not a malformed marketplace/plugin config.

Why This Looks Like a Claude Code Bug

The docs explicitly say this configuration is supported:

  • Marketplace plugin entries may include hooks, and its type is string | object:

https://code.claude.com/docs/en/plugin-marketplaces

  • Plugin manifest/reference says hooks can be string | array | object:

https://code.claude.com/docs/en/plugins-reference

  • Hooks docs say plugin hooks live in hooks/hooks.json, and plugin hook files may include an optional top-level description plus a hooks object:

https://code.claude.com/docs/en/hooks

So a marketplace entry pointing to ./hooks/hooks.json should be accepted and reloaded safely.

Root Cause (traced from source)

The bug is in cbY (marketplace plugin loader in cli.js). When a marketplace plugin has no .claude-plugin/plugin.json (common for marketplace-only plugins), two things happen:

Step 1 — Hooks are loaded correctly by Ao4()

Ao4() auto-discovers hooks/hooks.json in the plugin directory and loads it via ar4():

// In Ao4():
let P = join(pluginPath, "hooks", "hooks.json");
if (fs.existsSync(P))
    M = ar4(P, manifest.name);  // reads, JSON-parses, Zod-validates, returns .hooks
// ...
if (M) plugin.hooksConfig = M;  // correctly set

ar4() reads the file, parses JSON, validates against the QJ8 Zod schema, and returns the .hooks object. At this point plugin.hooksConfig is a properly parsed hooks config object.

Step 2 — cbY() overwrites with raw string

Back in cbY(), when no plugin.json exists:

// In cbY():
let pluginJsonExists = fs.existsSync(join(cachedPath, ".claude-plugin", "plugin.json"));
let {plugin: J} = Ao4(cachedPath, ...);
// J.hooksConfig is now correctly parsed from auto-discovery

if (!pluginJsonExists) {
    J.manifest = {...marketplaceEntry, id: undefined, source: undefined, strict: undefined};
    J.name = J.manifest.name;
    // ... handles commands, agents, skills ...
    if (marketplaceEntry.hooks) J.hooksConfig = marketplaceEntry.hooks;
    //                          ^^^ OVERWRITES with raw string "./hooks/hooks.json"
}

The correctly-loaded hooks config is replaced by the raw string "./hooks/hooks.json" from the marketplace entry.

Step 3 — Crash during hook registration

In bU (the hook registration function), the code counts total hooks:

let K = Object.values(q).reduce(
    (Y, z) => Y + z.reduce((w, H) => w + H.hooks.length, 0),
    0
);

With a string hooksConfig, the processing in Zg9() should technically degrade gracefully (string character entries don't match event-name keys). However, in the compiled binary the reduce crashes — likely due to the global registeredHooks accumulator (n6.registeredHooks) getting contaminated across the reload cycle, or a minor difference between the npm source and the compiled binary's code path.

What Should Happen?

Claude Code should reload successfully and either:

  • Load the hooks from ./hooks/hooks.json (resolving the string path), or
  • Show a validation/load error tied to the hook config

It should not crash the reload flow with a runtime reduce is not a function exception.

Actual Behavior

/reload-plugins crashes with:

TypeError: J?.reduce is not a function. (In 'J?.reduce((X,G)=>X+G.hooks.length,0)', 'J?.reduce' is undefined)

Error Messages/Logs

TypeError: J?.reduce is not a function. (In 'J?.reduce((X,G)=>X+G.hooks.length,0)', 'J?.reduce' is undefined)

Steps to Reproduce

Directory layout:

repro-market/
  .claude-plugin/marketplace.json
  repro-plugin/
    hooks/hooks.json

repro-market/.claude-plugin/marketplace.json:

{
  "name": "repro-market",
  "owner": { "name": "Example" },
  "plugins": [
    {
      "name": "repro-plugin",
      "version": "0.1.0",
      "source": "./repro-plugin",
      "description": "Minimal reproduction for hooks reload bug",
      "hooks": "./hooks/hooks.json"
    }
  ]
}

repro-market/repro-plugin/hooks/hooks.json:

{
  "description": "Repro hook config",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo repro"
          }
        ]
      }
    ]
  }
}

Steps to Reproduce

  1. Add the local marketplace:

/plugin marketplace add ./repro-market

  1. Install the plugin:

/plugin install repro-plugin@repro-market

  1. Run:

/reload-plugins

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.89 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Suggested Fix (in Claude Code)

In cbY(), the no-plugin.json branch should resolve string hook paths the same way Ao4() does — by reading and parsing the referenced file via ar4() — rather than assigning the raw string:

// Current (broken):
if (A.hooks) J.hooksConfig = A.hooks;

// Should be something like:
if (A.hooks) {
    if (typeof A.hooks === 'string') {
        const hooksPath = join(cachedPath, A.hooks);
        if (fs.existsSync(hooksPath)) {
            J.hooksConfig = ar4(hooksPath, A.name);
        }
    } else {
        J.hooksConfig = A.hooks;
    }
}

Or, simpler: skip the assignment entirely when Ao4() already loaded hooks from auto-discovery:

if (A.hooks && !J.hooksConfig) J.hooksConfig = A.hooks;

Relevant Source Locations (npm package, cli.js)

| Function | Byte offset | Purpose |
|----------|-------------|---------|
| ar4 | ~9027103 | Reads + Zod-validates hooks.json files |
| Ao4 | ~9027300 | Builds plugin object, auto-discovers hooks/hooks.json |
| cbY | ~9035000 | Loads marketplace plugins, contains the overwrite bug |
| Zg9 | ~6290889 | Processes plugin hooksConfig into categorized hooks |
| bU | ~6291500 | Registers all plugin hooks, contains the crashing reduce |

Relevant Docs

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗