Kuberwastaken/claurst · error · anyhow::Error

hooks. [].hooks must be an array

Error message

hooks.{event_name}[].hooks must be an array

What it means

In an imported hooks config, the value under a hook event key (e.g. hooks.PreToolUse) is not a JSON array. Each event must map to an array of matcher entries; a non-array (object, string, number) triggers this error. Note the message text with `[].hooks` is slightly misleading — the fault is the event value itself, read from the user-imported config.

Solutions

  1. Add a "hooks" array to each hook group containing at least one {"command": "..."} object
  2. If "hooks" is a single object, wrap it in an array
  3. Restore the standard key name "hooks" if it was renamed

Example fix

// before
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "command": "notify.sh" } ] } }
// after
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "command": "notify.sh" } ] } ] } }
Defensive patterns

Strategy: validation

Validate before calling

for (const [event, groups] of Object.entries(cfg.hooks ?? {})) {
  for (const g of groups) if (!Array.isArray(g?.hooks)) throw new Error(`hooks.${event} group needs a "hooks" array`);
}

Type guard

const hasHooksArray = (g) => typeof g === 'object' && g !== null && Array.isArray(g.hooks);

Try / catch

try { importConfig(path) } catch (e) { if (/\[\]\.hooks must be an array/.test(String(e))) { console.error('Add "hooks": [ { "command": "..." } ] to each hook group'); } else { throw e; } }

Prevention

When it happens

Trigger: Importing a hook group like {"matcher": "Bash"} without "hooks", or {"matcher": "*", "hooks": {"command": "x"}} where hooks is an object instead of an array.

Common situations: Hand-written hook groups omitting the inner "hooks" key, flattening the inner array to a single object, or renaming the key (e.g. "commands").

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/5a001e7767ac7438. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/core/src/import_config.rs:729

    for (event_name, event_value) in obj {
        let event = parse_hook_event(event_name)?;
        let entries = event_value
            .as_array()
            .ok_or_else(|| anyhow!("hooks.{event_name} must be an array"))?;
        let mut hook_entries = Vec::new();
        for entry in entries {
            let entry_obj = entry
                .as_object()
                .ok_or_else(|| anyhow!("hooks.{event_name}[] must be an object"))?;
            let matcher = entry_obj
                .get("matcher")
                .and_then(Value::as_str)
                .unwrap_or("*")
                .to_string();
            let hooks = entry_obj
                .get("hooks")
                .and_then(Value::as_array)
                .ok_or_else(|| anyhow!("hooks.{event_name}[].hooks must be an array"))?;
            for hook in hooks {
                let hook_obj = hook
                    .as_object()
                    .ok_or_else(|| anyhow!("hooks.{event_name}[].hooks[] must be an object"))?;
                let command = hook_obj
                    .get("command")
                    .and_then(Value::as_str)
                    .ok_or_else(|| anyhow!("hooks.{event_name} hook is missing command"))?
                    .to_string();
                hook_entries.push(HookEntry {
                    command,
                    tool_filter: if matcher == "*" { None } else { Some(matcher.clone()) },
                    blocking: false,
                });
            }
        }
        out.insert(event, hook_entries);
    }

View on GitHub (pinned to b0637c97ec)