{"record":{"id":"381e6ed73f0b24a6","repo":"JuliusBrussee/caveman","slug":"path-hooks-event-must-be-an-array-refusing","errorCode":null,"errorMessage":"${path} hooks.${event} must be an array; refusing to overwrite it","messagePattern":"(.+?) hooks\\.(.+?) must be an array; refusing to overwrite it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":5743,"sourceCode":"    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]) => {\n    const actual = Array.isArray(hooks[event]) ? hooks[event] as Array<Record<string, unknown>> : [];\n    const actualEntries = new Set(actual.map((entry) => JSON.stringify(entry)));\n    return (expectedRaw as Array<Record<string, unknown>>).every((entry) => actualEntries.has(JSON.stringify(entry)));\n  });\n}\n\nfunction buildCodexEphemeralHome(","sourceCodeStart":5725,"sourceCodeEnd":5761,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L5725-L5761","documentation":"Per-event check in assertNativeHooksShape: for each hook event the native integration manages (from nativeHooksDocument for claude/codex/gemini, e.g. UserPromptSubmit), the existing value under hooks.<event> must be an array if present. A non-array (object, string, number) means an incompatible schema that merge would corrupt, so it refuses.","triggerScenarios":"Installing native hooks when hooks.UserPromptSubmit (or another managed event) holds an object or scalar instead of the array-of-matchers form the agent expects.","commonSituations":"Settings migrated from a different agent whose hook schema nests by field; user hand-wrote a single hook object instead of a one-element array; older caveman versions used a different shape.","solutions":["Rewrite the event entry named in the message as an array: \"hooks\": { \"UserPromptSubmit\": [ { ...matcher } ] }","Remove the malformed event key and re-run setup to regenerate the native entry","Check the agent's hooks documentation for the array-of-matchers shape and conform existing entries"],"exampleFix":"// before\n{ \"hooks\": { \"UserPromptSubmit\": { \"command\": \"...\" } } }\n\n// after\n{ \"hooks\": { \"UserPromptSubmit\": [ { \"hooks\": [ { \"type\": \"command\", \"command\": \"...\" } ] } ] } }","handlingStrategy":"type-guard","validationCode":"const EVENTS = [\"UserPromptSubmit\", \"Stop\", \"PostToolUse\"];  // events caveman manages\nfor (const event of EVENTS) {\n  const v = root.hooks?.[event];\n  if (v !== undefined && !Array.isArray(v)) {\n    throw new Error(`${settingsPath}: hooks.${event} must be an array`);\n  }\n}","typeGuard":"function isHookEventArray(v: unknown): v is Array<Record<string, unknown>> {\n  return Array.isArray(v) && v.every((e) => typeof e === \"object\" && e !== null && !Array.isArray(e));\n}","tryCatchPattern":"try {\n  installNativeHooks(agent);\n} catch (error) {\n  if (/hooks\\..+ must be an array/.test((error as Error).message)) {\n    // schema mismatch in user data: repair the named event entry, then retry setup\n    repairHooksEvent(settingsPath, eventFromMessage(error));\n    await installNativeHooks(agent);\n  } else throw error;\n}","preventionTips":["Write each hook event as an array of matcher objects, even for a single hook","Copy hook snippets from the agent's official docs rather than hand-nesting","Lint agent settings files in dotfile management (chezmoi/stow) for array shapes"],"tags":["hooks","json-schema","settings","refuse-overwrite"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}