{"record":{"id":"fb375e6905ba4687","repo":"affaan-m/ECC","slug":"invalid-hooks-config-at-hookssourcepath-expect","errorCode":null,"errorMessage":"Invalid hooks config at ${hooksSourcePath}: expected \"hooks\" to be a JSON object","messagePattern":"Invalid hooks config at (.+?): expected \"hooks\" to be a JSON object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/install/apply.js","lineNumber":247,"sourceCode":"  if (!plan.adapter || (plan.adapter.target !== 'claude' && plan.adapter.target !== 'claude-project')) {\n    return null;\n  }\n\n  const pluginRoot = plan.targetRoot;\n  const hooksDestinationPath = path.join(plan.targetRoot, 'hooks', 'hooks.json');\n  const hooksOperation = findHooksOperation(plan, hooksDestinationPath);\n  if (!hooksOperation) {\n    return null;\n  }\n  const hooksSourcePath = hooksOperation.sourcePath;\n  if (!fs.existsSync(hooksSourcePath)) {\n    return null;\n  }\n\n  const hooksConfig = readJsonObject(hooksSourcePath, 'hooks config');\n  const resolvedHooks = replacePluginRootPlaceholders(hooksConfig.hooks, pluginRoot);\n  if (!resolvedHooks || typeof resolvedHooks !== 'object' || Array.isArray(resolvedHooks)) {\n    throw new Error(`Invalid hooks config at ${hooksSourcePath}: expected \"hooks\" to be a JSON object`);\n  }\n\n  return {\n    hooksOperation,\n    hooksDestinationPath,\n    resolvedHooksConfig: {\n      ...hooksConfig,\n      hooks: resolvedHooks,\n    },\n  };\n}\n\nfunction previewInstallPlan(plan) {\n  const migration = prepareClaudeSkillMigration(plan);\n  return {\n    ...plan,\n    statePreview: migration.finalState,\n    plannedOperations: [...plan.operations],","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/install/apply.js#L229-L265","documentation":"Thrown by buildResolvedClaudeHooks in scripts/lib/install/apply.js. This function runs only when plan.adapter.target is 'claude' or 'claude-project'. It reads the source hooks.json, substitutes ${CLAUDE_PLUGIN_ROOT} placeholders via replacePluginRootPlaceholders, then requires the resulting hooks field to be a non-null, non-array object (matching Claude Code's hooks schema). If not, the installer refuses to write the hooks file.","triggerScenarios":"ECC's bundled hooks/hooks.json (or a fork's equivalent) has its hooks field set to an array, null, or undefined after placeholder substitution; or a downstream fork restructured hooks as a list.","commonSituations":"Manual edit of the bundled hooks file; a bad merge that wrapped hooks in an array; a fork that switched to a list-based representation without updating the applier.","solutions":["Open the file at hooksSourcePath and verify the top level has \"hooks\": { ... } (object).","Restore the file from git: git checkout -- hooks/hooks.json.","Validate against Claude Code's hooks schema before re-running.","Confirm no pre-processor (e.g. placeholder substitution) is returning undefined when pluginRoot is empty."],"exampleFix":"// before (hooks/hooks.json)\n{\n  \"hooks\": [\n    { \"matcher\": \"Bash\", \"hooks\": [ { \"type\": \"command\", \"command\": \"echo hi\" } ] }\n  ]\n}\n\n// after\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      { \"matcher\": \"Bash\", \"hooks\": [ { \"type\": \"command\", \"command\": \"echo hi\" } ] }\n    ]\n  }\n}","handlingStrategy":"type-guard","validationCode":"const cfg = JSON.parse(fs.readFileSync('hooks/hooks.json', 'utf8'));\nif (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {\n  throw new Error('hooks.json top level must be an object');\n}\nif (!cfg.hooks || typeof cfg.hooks !== 'object' || Array.isArray(cfg.hooks)) {\n  throw new Error('hooks.json must have a \"hooks\" object');\n}","typeGuard":"function isHooksConfig(value) {\n  return Boolean(\n    value && typeof value === 'object' && !Array.isArray(value)\n    && value.hooks && typeof value.hooks === 'object' && !Array.isArray(value.hooks)\n  );\n}","tryCatchPattern":null,"preventionTips":["Treat hooks/hooks.json as schema-locked; never change its top-level shape.","Add a pre-commit hook that validates the hooks file structure.","Run git diff on hooks/hooks.json before any release."],"tags":["hooks","claude","config-validation","install-apply"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}