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
- Add a "hooks" array to each hook group containing at least one {"command": "..."} object
- If "hooks" is a single object, wrap it in an array
- 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
- Every hook group requires the inner "hooks" key holding an array
- Do not flatten the inner array to a single object when only one hook exists
- Copy hook-group examples verbatim rather than abbreviating
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
- hooks must be an object
- hooks. must be an array
- hooks. [] must be an object
- hooks. [].hooks[] must be an object
- mcpServers must be an object
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)