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

  1. Open the config file and find `hooks.<event>` with the non-array value
  2. 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
  3. Validate the resulting JSON parses and hooks events are all arrays
  4. 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

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


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)