Kuberwastaken/claurst · error

hooks must be an object

Error message

hooks must be an object

What it means

The imported config's top-level `hooks` value is not a JSON object (e.g. it is an array, string, or scalar). parse_hooks pattern-matches value.as_object() and rejects anything else; the hook map shape expected is { "PreToolUse": [...], ... }.

Solutions

  1. Make "hooks" a JSON object keyed by event name (PreToolUse, PostToolUse, Stop, PostModelTurn, UserPromptSubmit, Notification)
  2. Remove the hooks key if no hooks should be imported
  3. Convert array-style hook lists into the event-keyed object shape

Example fix

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

Strategy: validation

Validate before calling

if (cfg.hooks !== undefined && (typeof cfg.hooks !== 'object' || Array.isArray(cfg.hooks) || cfg.hooks === null)) {
  throw new Error('hooks must be a JSON object keyed by event name');
}

Type guard

const isHooksMap = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every(Array.isArray);

Try / catch

try { importConfig(path) } catch (e) { if (String(e).includes('hooks must be an object')) { fixHooksShape(path); } else { throw e; } }

Prevention

When it happens

Trigger: Importing a settings file where "hooks" is null, a string, or an array instead of an object like {"PreToolUse": [...]}.

Common situations: A hooks key set to null to disable hooks before import, config fragments copied from a tool that stores hooks as a flat list, or JSON corruption from manual edits.

Related errors


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

Appendix: source

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

        servers.push(McpServerConfig {
            name: name.clone(),
            command,
            args,
            env,
            url,
            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

View on GitHub (pinned to b0637c97ec)