JuliusBrussee/caveman · error · Error

hooks must be a JSON object; refusing to overwrite it

Error message

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

What it means

Thrown when merging native hooks into an agent config file (settings.json for claude/codex/gemini) whose top-level `hooks` key exists but is not a plain JSON object (string, number, boolean, or array). The CLI refuses to overwrite a value it cannot safely merge, protecting user data.

Solutions

  1. Open the config file and inspect the `hooks` key
  2. Change `hooks` to a JSON object (e.g. `{}`) or remove the key so the CLI can populate it
  3. Back up the file before editing: `cp settings.json settings.json.bak`
  4. Re-run the setup/install command

Example fix

// before (settings.json)
{ "hooks": "enabled" }
// after
{ "hooks": {} }
Defensive patterns

Strategy: validation

Validate before calling

const doc = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
if (doc.hooks !== undefined && (typeof doc.hooks !== "object" || doc.hooks === null || Array.isArray(doc.hooks))) {
  console.error(`${settingsPath}: hooks must be an object`);
}

Type guard

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

Try / catch

try {
  runSetup();
} catch (e) {
  if (String(e.message).includes("hooks must be a JSON object")) {
    console.error(`Fix the hooks key in the named config file to be an object, then retry.`);
  }
}

Prevention

When it happens

Trigger: Calling the hooks-install/setup flow against a config file whose parsed JSON has `hooks` set to a non-object non-null value; detected by assertNativeHooksShape before writing.

Common situations: Hand-edited or third-party-tool-written settings.json where hooks was written as a string or array; a corrupted file from a partial write by another tool.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/90773ada03d269ca. Report an issue: GitHub.

Appendix: source

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

    }
    // The lifecycle loop above always writes this event, so the key exists either way.
    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, agentId));
    }
    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;
  // `true` on purpose, unlike the writers: this only reads the event NAMES to
  // refuse a non-array, so the superset is the safer set to check.
  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;

View on GitHub (pinned to 3ee70a1026)