{"record":{"id":"842b0fcd0acd099e","repo":"affaan-m/ECC","slug":"invalid-label-expected-hooks-event-to-be-an-array","errorCode":null,"errorMessage":"Invalid ${label}: expected hooks.${event} to be an array","messagePattern":"Invalid (.+?): expected hooks\\.(.+?) to be an array","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install/claude-settings.js","lineNumber":229,"sourceCode":"      }\n      seenIds.add(entry.id);\n    });\n  }\n  return cloneValue(managedHooks);\n}\n\nfunction validateSettings(settings, label = 'Claude settings') {\n  if (!isJsonObject(settings)) {\n    throw new Error(`Invalid ${label}: expected a JSON object`);\n  }\n\n  if (Object.prototype.hasOwnProperty.call(settings, 'hooks')) {\n    if (!isJsonObject(settings.hooks)) {\n      throw new Error(`Invalid ${label}: expected \"hooks\" to be a JSON object`);\n    }\n    for (const [event, entries] of Object.entries(settings.hooks)) {\n      if (!Array.isArray(entries)) {\n        throw new Error(`Invalid ${label}: expected hooks.${event} to be an array`);\n      }\n    }\n  }\n\n  return cloneValue(settings);\n}\n\nfunction replacePluginRootPlaceholders(value, pluginRoot) {\n  if (typeof pluginRoot !== 'string') {\n    throw new Error('Invalid Claude plugin root: expected a string');\n  }\n  if (typeof value === 'string') {\n    return value.split(PLUGIN_ROOT_PLACEHOLDER).join(pluginRoot);\n  }\n  if (Array.isArray(value)) {\n    return value.map(item => replacePluginRootPlaceholders(item, pluginRoot));\n  }\n  if (isJsonObject(value)) {","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install/claude-settings.js#L211-L247","documentation":"Each value inside settings.hooks must be an array of hook entries. After confirming hooks is an object, the validator iterates its event keys and throws this error if any event's value is not an array. This keeps the hooks structure consistent with Claude's settings schema.","triggerScenarios":"Calling parseSettings/validatedSettings/validateSettings where settings.hooks[event] is a string, object, or number instead of an array — e.g. \"hooks\": { \"Stop\": { \"command\": ... } }.","commonSituations":"A user flattened a single hook into an object instead of wrapping it in an array, a merge tool replaced an array with a keyed object, or hand-editing removed the [ ] brackets around entries.","solutions":["Wrap the event's hook entries in an array","Remove the malformed event key if it holds no valid hooks","Re-run validation after fixing to catch any other malformed events","Compare against a known-good settings.json hooks block"],"exampleFix":"// before\n\"Stop\": { \"type\": \"command\", \"command\": \"x\" }\n// after\n\"Stop\": [ { \"type\": \"command\", \"command\": \"x\" } ]","handlingStrategy":"validation","validationCode":"const allArrays = (hooks) => Object.values(hooks).every(Array.isArray);","typeGuard":"const isHookArray = (v) => Array.isArray(v);","tryCatchPattern":"try { return parseSettings(s); } catch (e) { const m = /hooks\\.(\\S+) to be an array/.exec(e.message); if (m) { console.error('Event', m[1], 'must be an array'); } throw e; }","preventionTips":["Always wrap hook entries in arrays, even single hooks","Validate hooks blocks after any programmatic merge","Lint hooks.json with a JSON schema","Avoid flattening tools that rewrite hooks objects"],"tags":["validation","settings","hooks"],"backgroundTag":"config-type-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}