JuliusBrussee/caveman · error

${path} hooks must be a JSON object; refusing to overwrite i

Error message

${path} hooks must be a JSON object; refusing to overwrite it

What it means

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.

Source

Thrown at packages/cli/src/index.ts:5736

        : nativeHookEntry(shrinkCommand));
    }
    hooks[shrinkEvent] = list;
  }
  if (agentId === "claude" && includeRecall) {
    const list = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit as Array<Record<string, unknown>> : [];
    const recallCommand = `${cavemanBinForHook()} mem recall-hook`;
    if (!list.some((entry) => hookEntryCommand(entry) === recallCommand)) {
      list.push(nativeHookEntry(recallCommand));
    }
    hooks.UserPromptSubmit = list;
  }
  root.hooks = hooks;
  return root;
}

function assertNativeHooksShape(path: string, root: Record<string, unknown>, agentId: "claude" | "codex" | "gemini"): void {
  if (root.hooks !== undefined && (typeof root.hooks !== "object" || root.hooks === null || Array.isArray(root.hooks))) {
    throw new Error(`${path} hooks must be a JSON object; refusing to overwrite it`);
  }
  const hooks = root.hooks as Record<string, unknown> | undefined;
  if (!hooks) return;
  const expected = nativeHooksDocument(agentId, true).hooks as Record<string, unknown>;
  for (const event of Object.keys(expected)) {
    if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
      throw new Error(`${path} hooks.${event} must be an array; refusing to overwrite it`);
    }
  }
}

function nativeHookEntriesHealthy(root: Record<string, unknown>, agentId: "claude" | "codex" | "gemini"): boolean {
  const hooks = root.hooks && typeof root.hooks === "object" && !Array.isArray(root.hooks)
    ? root.hooks as Record<string, unknown>
    : undefined;
  if (!hooks) return false;
  const expected = nativeHooksDocument(agentId, true).hooks as Record<string, unknown>;
  return Object.entries(expected).every(([event, expectedRaw]) => {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. 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
  2. Back up and remove the malformed "hooks" key entirely, then re-run setup to have caveman create it
  3. Validate the file with `jq '.hooks | type'` expecting "object" before setup

Example fix

// before (.claude/settings.json)
{ "hooks": [ { "UserPromptSubmit": [] } ] }

// after
{ "hooks": { "UserPromptSubmit": [] } }
Defensive patterns

Strategy: type-guard

Validate before calling

import { readFileSync } from "node:fs";
const root = JSON.parse(readFileSync(settingsPath, "utf8"));
if (root.hooks !== undefined && (typeof root.hooks !== "object" || root.hooks === null || Array.isArray(root.hooks))) {
  throw new Error(`${settingsPath}: fix hooks to an object before running caveman setup`);
}

Type guard

function isHooksObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}

Try / catch

try {
  installNativeHooks(agent);
} catch (error) {
  if (/hooks must be a JSON object/.test((error as Error).message)) {
    // refuse-to-overwrite guard: repair the file manually; do not force
    promptUserToFix(settingsPathFromMessage(error));
  } else throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: Hand-edited settings file where hooks was written as a list; migration from a tool that stored hooks differently; malformed merge by another installer.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/90773ada03d269ca. Report an issue: GitHub.