JuliusBrussee/caveman · error · Error
hooks. must be an array; refusing to overwrite it
Error message
${path} hooks.${event} must be an array; refusing to overwrite it What it means
Thrown when an agent config file defines `hooks.<event>` as a non-array value for an event name the CLI needs to manage (SessionStart, Stop, etc.). Hooks events must map to arrays of matchers; the CLI refuses to overwrite a malformed event value rather than destroying user config.
Solutions
- Open the config file and find `hooks.<event>` with the non-array value
- Wrap the value in an array or convert an object of matchers into the expected array form, or delete the key to let the CLI regenerate it
- Validate the resulting JSON parses and hooks events are all arrays
- Re-run the setup command
Example fix
// before
{ "hooks": { "SessionStart": { "command": "x" } } }
// after
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "x" } ] } ] } } Defensive patterns
Strategy: validation
Validate before calling
const doc = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
for (const [event, value] of Object.entries(doc.hooks ?? {})) {
if (value !== undefined && !Array.isArray(value)) console.error(`hooks.${event} must be an array`);
} Type guard
function isHookEventArray(v: unknown): v is unknown[] {
return Array.isArray(v);
} Try / catch
try {
runSetup();
} catch (e) {
if (String(e.message).includes("must be an array")) {
console.error(`Convert the named hooks event value to an array in the config file, then retry.`);
}
} Prevention
- Follow the documented hooks schema: each event maps to an array of matchers
- Run a JSON schema check on settings files after manual edits
- Regenerate hooks config from templates instead of hand-writing structures
When it happens
Trigger: assertNativeHooksShape iterates the expected event names from nativeHooksDocument(agentId, true) and finds hooks[event] defined but not an array.
Common situations: Config produced by another tool or hand edit where hooks.PreToolUse is an object or string instead of the expected array; schema drift between tool versions.
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 a JSON object; 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/381e6ed73f0b24a6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:6607
}
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;
const expected = nativeHooksDocument(agentId, nativeShrinkEnabled()).hooks as Record<string, unknown>;
const required = Object.entries(expected).every(([event, expectedRaw]) => {
const actual = Array.isArray(hooks[event]) ? hooks[event] as Array<Record<string, unknown>> : [];
const actualEntries = new Set(actual.map(canonicalManagedHookEntry).filter(Boolean));
return (expectedRaw as Array<Record<string, unknown>>).every((entry) => {
const canonical = canonicalManagedHookEntry(entry);
return canonical !== undefined && actualEntries.has(canonical);
});
});View on GitHub (pinned to 3ee70a1026)