Kuberwastaken/claurst · error · anyhow::Error

hooks. [] must be an object

Error message

hooks.{event_name}[] must be an object

What it means

In parse_hooks, every element of an event's array must be a hook-group JSON object with optional "matcher" and required "hooks". A non-object element (string, null, number) in the array raises this error naming the event.

Solutions

  1. Convert each array element into an object with a "hooks" array of {"command": ...} entries
  2. Remove non-object elements from the event array
  3. Keep the documented shape: event -> [ { matcher, hooks: [ { command } ] } ]

Example fix

// before
{ "hooks": { "PreToolUse": ["npx lint"] } }
// after
{ "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "command": "npx lint" } ] } ] } }
Defensive patterns

Strategy: type-guard

Validate before calling

for (const groups of Object.values(cfg.hooks ?? {})) {
  for (const g of groups) if (typeof g !== 'object' || g === null || Array.isArray(g)) throw new Error('hook group entries must be objects');
}

Type guard

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

Try / catch

try { importConfig(path) } catch (e) { if (/hooks\.\S+\[\] must be an object/.test(String(e))) { console.error('Wrap each hook entry in an object with matcher/hooks'); } else { throw e; } }

Prevention

When it happens

Trigger: Importing config where the event array contains raw strings or nulls, e.g. "hooks": {"PreToolUse": ["npx lint"]} or [null].

Common situations: Copy-pasted hooks where entries lost their object wrapping, JSON edited by scripts that pushed plain strings, or an intentionally disabled hook left as null.

Related errors


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

Appendix: source

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

    Ok(servers)
}

fn parse_hooks(value: &Value) -> Result<HashMap<HookEvent, Vec<HookEntry>>> {
    let Some(obj) = value.as_object() else {
        return Err(anyhow!("hooks must be an object"));
    };
    let mut out = HashMap::new();
    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();

View on GitHub (pinned to b0637c97ec)