[BUG] `/reload-plugins` crashes when a marketplace entry uses `"hooks": "./hooks/hooks.json"`
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 isstring | 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-leveldescriptionplus ahooksobject:
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
- Add the local marketplace:
/plugin marketplace add ./repro-market
- Install the plugin:
/plugin install repro-plugin@repro-market
- 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
- Plugin marketplaces: https://code.claude.com/docs/en/plugin-marketplaces
- Plugins reference: https://code.claude.com/docs/en/plugins-reference
- Hooks reference: https://code.claude.com/docs/en/hooks
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗