{"record":{"id":"e793678f59f3e961","repo":"xai-org/grok-build","slug":"hooks-is-not-a-json-object","errorCode":null,"errorMessage":"{}: hooks is not a JSON object","messagePattern":"(.+?): hooks is not a JSON object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-shell/src/claude_import.rs","lineNumber":1074,"sourceCode":"            warn!(\n                path = %target.display(),\n                error = %e,\n                \"Existing imported-from-claude.json is malformed; replacing with fresh content. \\\n                 Manual edits in the malformed file will be lost.\"\n            );\n            serde_json::json!({})\n        }),\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => serde_json::json!({}),\n        Err(e) => return Err(e.into()),\n    };\n    let root_obj = root\n        .as_object_mut()\n        .ok_or_else(|| anyhow::anyhow!(\"{}: root is not a JSON object\", target.display()))?;\n    let hooks_obj = root_obj\n        .entry(\"hooks\".to_string())\n        .or_insert_with(|| serde_json::json!({}))\n        .as_object_mut()\n        .ok_or_else(|| anyhow::anyhow!(\"{}: hooks is not a JSON object\", target.display()))?;\n\n    let mut count = 0usize;\n    // `dirty` tracks whether we mutated the JSON in any way (including\n    // in-place timeout refreshes that don't add new entries). The file is\n    // re-written iff dirty, even when count == 0.\n    let mut dirty = false;\n    for item in new_hooks {\n        let ImportableItem::Hook {\n            event,\n            matcher,\n            command,\n            timeout,\n        } = item\n        else {\n            continue;\n        };\n\n        let groups = hooks_obj","sourceCodeStart":1056,"sourceCodeEnd":1092,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-shell/src/claude_import.rs#L1056-L1092","documentation":"After ensuring the root is an object, apply_hooks_to_dir fetches or creates the root's 'hooks' key and requires it to be a JSON object whose keys are event names. If 'hooks' exists but is an array, string, or other non-object, merging hook groups is impossible and this error is thrown.","triggerScenarios":"apply_import encounters a settings.json where \"hooks\" is set to a non-object value, e.g. \"hooks\": [] or \"hooks\": \"none\".","commonSituations":"Hand-edited settings.json with wrong hooks shape; older config format that stored hooks as a list; copy-paste from outdated documentation.","solutions":["Edit settings.json so \"hooks\" is an object keyed by event name, e.g. \"hooks\": {\"PreToolUse\": []}","Remove the bad \"hooks\" key and re-run the import so it is recreated as {}","Validate settings.json against the expected hooks schema before importing"],"exampleFix":"// before\n{\"hooks\": []}\n// after\n{\"hooks\": {\"PreToolUse\": []}}","handlingStrategy":"type-guard","validationCode":"let v: serde_json::Value = serde_json::from_str(&raw)?;\nif !v.get(\"hooks\").map_or(true, |h| h.is_object()) {\n    return Err(\"hooks must be a JSON object\".into());\n}","typeGuard":"fn hooks_is_object(v: &serde_json::Value) -> bool {\n    v.get(\"hooks\").map_or(true, serde_json::Value::is_object)\n}","tryCatchPattern":"match apply_import(...) {\n    Err(e) if e.to_string().contains(\"hooks is not a JSON object\") => {\n        // repair: force hooks to {}\n        let mut v: serde_json::Value = serde_json::from_str(&raw)?;\n        v[\"hooks\"] = serde_json::json!({});\n        serde_json::to_writer_pretty(&mut file, &v)?;\n        apply_import(...) // retry\n    }\n    r => r,\n}","preventionTips":["Keep hooks as an object keyed by event name","Check settings.json shape after upgrading tool versions","Validate against the hooks schema before import"],"tags":["json","config","schema"],"backgroundTag":"json-root-not-object","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}