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
- Open the config file and inspect the `hooks` key
- Change `hooks` to a JSON object (e.g. `{}`) or remove the key so the CLI can populate it
- Back up the file before editing: `cp settings.json settings.json.bak`
- 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
- Keep hooks as a plain object in agent settings files
- Validate settings.json with a schema after editing it by hand
- Back up config files before running third-party tools that may rewrite them
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
- hooks. must be an array; refusing to overwrite it
- is not a JSON object
- assembly slot content is not JSON-serializable
- cave_breaker_retry_backoff_invalid
- cave_breaker_retry_requires_budget
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)