Kuberwastaken/claurst · error · anyhow::Error

hooks. must be an array

Error message

hooks.{event_name} must be an array

What it means

In parse_hooks, each event value inside the hooks object must be an array of hook-group objects. If the value for an event key is not an array, this error names the event (hooks.{event_name}) and aborts import.

Solutions

  1. Wrap the hook group (or command) in a JSON array under the event key
  2. If it is a single hook, wrap the existing object in brackets
  3. Validate hooks shape against Claude Code's documented hooks schema before importing

Example fix

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

Strategy: type-guard

Validate before calling

for (const [event, v] of Object.entries(cfg.hooks ?? {})) {
  if (!Array.isArray(v)) throw new Error(`hooks.${event} must be an array of hook groups`);
}

Type guard

const isEventGroups = (v) => Array.isArray(v) && v.every((g) => typeof g === 'object' && g !== null);

Try / catch

try { importConfig(path) } catch (e) { const m = String(e).match(/hooks\.(\S+) must be an array/); if (m) { wrapEventInArray(path, m[1]); } else { throw e; } }

Prevention

When it happens

Trigger: Importing config where an event maps to a single object instead of an array, e.g. "hooks": {"Stop": {"matcher": "*", ...}} or {"Stop": "echo done"}.

Common situations: Users writing one hook directly under the event key without wrapping it in an array, or merging hooks from two files where one got collapsed to a single object.

Related errors


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

Appendix: source

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

            server_type,
            // Imported from a user-chosen config (e.g. Claude Desktop): trusted.
            origin: Default::default(),
        });
    }

    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"))?;

View on GitHub (pinned to b0637c97ec)