xai-org/grok-build · error

{}: root is not a JSON object

Error message

{}: root is not a JSON object

What it means

apply_hooks_to_dir reads the target settings.json, falling back to an empty JSON object when the file is absent, then requires the parsed root to be a JSON object so 'hooks' can be inserted. If the file's top-level JSON value is an array, string, number, or null, the mutation cannot proceed and this error is thrown.

Source

Thrown at crates/codegen/xai-grok-shell/src/claude_import.rs:1069

    let target = hooks_dir.join("imported-from-claude.json");

    // Read existing JSON if present.
    let mut root: serde_json::Value = match std::fs::read_to_string(&target) {
        Ok(s) => serde_json::from_str(&s).unwrap_or_else(|e| {
            warn!(
                path = %target.display(),
                error = %e,
                "Existing imported-from-claude.json is malformed; replacing with fresh content. \
                 Manual edits in the malformed file will be lost."
            );
            serde_json::json!({})
        }),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => serde_json::json!({}),
        Err(e) => return Err(e.into()),
    };
    let root_obj = root
        .as_object_mut()
        .ok_or_else(|| anyhow::anyhow!("{}: root is not a JSON object", target.display()))?;
    let hooks_obj = root_obj
        .entry("hooks".to_string())
        .or_insert_with(|| serde_json::json!({}))
        .as_object_mut()
        .ok_or_else(|| anyhow::anyhow!("{}: hooks is not a JSON object", target.display()))?;

    let mut count = 0usize;
    // `dirty` tracks whether we mutated the JSON in any way (including
    // in-place timeout refreshes that don't add new entries). The file is
    // re-written iff dirty, even when count == 0.
    let mut dirty = false;
    for item in new_hooks {
        let ImportableItem::Hook {
            event,
            matcher,
            command,
            timeout,
        } = item

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Inspect the target file and wrap the content in an object, e.g. {} or {"hooks": {}}
  2. Back up and replace the malformed settings.json with an empty JSON object, then re-run the import
  3. Fix whatever tool is writing a non-object root to settings.json

Example fix

// before (settings.json)
[]
// after
{}
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight check
let raw = std::fs::read_to_string(path)?;
let v: serde_json::Value = serde_json::from_str(&raw)?;
if !v.is_object() {
    return Err(format!("{}: root is not an object", path.display()).into());
}

Type guard

fn is_json_object(v: &serde_json::Value) -> bool {
    v.is_object()
}

Try / catch

match apply_import(...) {
    Err(e) if e.to_string().contains("root is not a JSON object") => {
        std::fs::copy(&target, "settings.json.bak")?;
        std::fs::write(&target, "{}")?;
        apply_import(...) // retry
    }
    r => r,
}

Prevention

When it happens

Trigger: Running apply_import when ~/.claude/settings.json (or the matched dir target) contains valid JSON whose top level is not an object, e.g. the file is '[]' or '"settings"'.

Common situations: Manually edited or corrupted settings.json; a tool that wrote a JSON array; an empty-but-invalid placeholder file; a truncated write from a crash.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/4c6d10f04b9332a0. Report an issue: GitHub.