Kuberwastaken/claurst · error · anyhow::Error

hooks. [].hooks[] must be an object

Error message

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

What it means

In an imported hooks config, an element of a hook event array is not a JSON object. Each entry must be an object like { "matcher": "...", "hooks": [...] }; a non-object element (string, number) triggers this error.

Solutions

  1. Wrap each command string in an object: {"command": "<shell command>"}
  2. Remove null or otherwise non-object elements from the inner hooks array
  3. Follow the documented inner shape: hooks: [ { "command": "..." } ]

Example fix

// before
{ "hooks": { "Stop": [ { "matcher": "*", "hooks": ["echo hi"] } ] } }
// after
{ "hooks": { "Stop": [ { "matcher": "*", "hooks": [ { "command": "echo hi" } ] } ] } }
Defensive patterns

Strategy: type-guard

Validate before calling

for (const groups of Object.values(cfg.hooks ?? {})) {
  for (const g of groups) for (const h of g.hooks ?? []) if (typeof h !== 'object' || h === null) throw new Error('each hook must be an object with a command');
}

Type guard

const isHookObj = (h) => typeof h === 'object' && h !== null && !Array.isArray(h) && typeof h.command === 'string';

Try / catch

try { importConfig(path) } catch (e) { if (/hooks\[\] must be an object/.test(String(e))) { wrapInnerHooksAsObjects(path); } else { throw e; } }

Prevention

When it happens

Trigger: Importing a group like {"hooks": ["echo hi"]} where the inner array holds raw strings or nulls instead of objects.

Common situations: Concise hook syntax copied from blog posts or other tools, sed/script-generated config pushing strings, or JSON where the object braces were lost during editing.

Related errors


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

Appendix: source

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

            .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);
    }
    Ok(out)
}

fn parse_hook_event(name: &str) -> Result<HookEvent> {

View on GitHub (pinned to b0637c97ec)