{"record":{"id":"90773ada03d269ca","repo":"JuliusBrussee/caveman","slug":"path-hooks-must-be-a-json-object-refusing-to-o","errorCode":null,"errorMessage":"${path} hooks must be a JSON object; refusing to overwrite it","messagePattern":"(.+?) hooks must be a JSON object; refusing to overwrite it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":5736,"sourceCode":"        : nativeHookEntry(shrinkCommand));\n    }\n    hooks[shrinkEvent] = list;\n  }\n  if (agentId === \"claude\" && includeRecall) {\n    const list = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit as Array<Record<string, unknown>> : [];\n    const recallCommand = `${cavemanBinForHook()} mem recall-hook`;\n    if (!list.some((entry) => hookEntryCommand(entry) === recallCommand)) {\n      list.push(nativeHookEntry(recallCommand));\n    }\n    hooks.UserPromptSubmit = list;\n  }\n  root.hooks = hooks;\n  return root;\n}\n\nfunction assertNativeHooksShape(path: string, root: Record<string, unknown>, agentId: \"claude\" | \"codex\" | \"gemini\"): void {\n  if (root.hooks !== undefined && (typeof root.hooks !== \"object\" || root.hooks === null || Array.isArray(root.hooks))) {\n    throw new Error(`${path} hooks must be a JSON object; refusing to overwrite it`);\n  }\n  const hooks = root.hooks as Record<string, unknown> | undefined;\n  if (!hooks) return;\n  const expected = nativeHooksDocument(agentId, true).hooks as Record<string, unknown>;\n  for (const event of Object.keys(expected)) {\n    if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {\n      throw new Error(`${path} hooks.${event} must be an array; refusing to overwrite it`);\n    }\n  }\n}\n\nfunction nativeHookEntriesHealthy(root: Record<string, unknown>, agentId: \"claude\" | \"codex\" | \"gemini\"): boolean {\n  const hooks = root.hooks && typeof root.hooks === \"object\" && !Array.isArray(root.hooks)\n    ? root.hooks as Record<string, unknown>\n    : undefined;\n  if (!hooks) return false;\n  const expected = nativeHooksDocument(agentId, true).hooks as Record<string, unknown>;\n  return Object.entries(expected).every(([event, expectedRaw]) => {","sourceCodeStart":5718,"sourceCodeEnd":5754,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L5718-L5754","documentation":"assertNativeHooksShape validates an agent settings JSON (~/.claude/settings.json, codex/gemini equivalents) before native hooks are merged. If a \"hooks\" key exists and is not a plain JSON object (it is an array, a string, a number, or null), the merge would destroy user data, so the CLI refuses to overwrite it.","triggerScenarios":"Running `caveman setup --agent-native <agent>` (or hooks installation) when the agent's settings file has \"hooks\" set to an array/string/number/null instead of an object mapping event names to arrays.","commonSituations":"Hand-edited settings file where hooks was written as a list; migration from a tool that stored hooks differently; malformed merge by another installer.","solutions":["Edit the settings file named in the message so \"hooks\" is an object, e.g. {\"UserPromptSubmit\": []}, moving any list entries under the correct event key","Back up and remove the malformed \"hooks\" key entirely, then re-run setup to have caveman create it","Validate the file with `jq '.hooks | type'` expecting \"object\" before setup"],"exampleFix":"// before (.claude/settings.json)\n{ \"hooks\": [ { \"UserPromptSubmit\": [] } ] }\n\n// after\n{ \"hooks\": { \"UserPromptSubmit\": [] } }","handlingStrategy":"type-guard","validationCode":"import { readFileSync } from \"node:fs\";\nconst root = JSON.parse(readFileSync(settingsPath, \"utf8\"));\nif (root.hooks !== undefined && (typeof root.hooks !== \"object\" || root.hooks === null || Array.isArray(root.hooks))) {\n  throw new Error(`${settingsPath}: fix hooks to an object before running caveman setup`);\n}","typeGuard":"function isHooksObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  installNativeHooks(agent);\n} catch (error) {\n  if (/hooks must be a JSON object/.test((error as Error).message)) {\n    // refuse-to-overwrite guard: repair the file manually; do not force\n    promptUserToFix(settingsPathFromMessage(error));\n  } else throw error;\n}","preventionTips":["Keep \"hooks\" as an object keyed by event names in agent settings files","Validate settings with `jq '.hooks | type' == \"object\"` before setup","Never let external tools write arrays at the hooks key"],"tags":["hooks","json-schema","settings","refuse-overwrite"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}