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.

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.

Appendix: 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)