ultraworkers/claw-code · error · std::io::Error

credentials file must contain a JSON object

Error message

credentials file must contain a JSON object

What it means

Thrown by read_credentials_root() when ~/.claw/credentials.json exists and parses as valid JSON, but the top-level value is not an object (e.g. an array, string, or number). The credentials store is a map of provider-name to credential entries, so a non-object root is treated as corrupt data. It is returned as io::Error with ErrorKind::InvalidData, wrapping the serde_json failure only when JSON parsing itself fails; this specific message means parsing succeeded but the shape was wrong.

Source

Thrown at rust/crates/runtime/src/oauth.rs:360

                "HOME is not set (on Windows, set USERPROFILE or HOME, \
                 or use CLAW_CONFIG_HOME to point directly at the config directory)",
            )
        })?;
    Ok(PathBuf::from(home).join(".claw"))
}

fn read_credentials_root(path: &PathBuf) -> io::Result<Map<String, Value>> {
    match fs::read_to_string(path) {
        Ok(contents) => {
            if contents.trim().is_empty() {
                return Ok(Map::new());
            }
            serde_json::from_str::<Value>(&contents)
                .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
                .as_object()
                .cloned()
                .ok_or_else(|| {
                    io::Error::new(
                        io::ErrorKind::InvalidData,
                        "credentials file must contain a JSON object",
                    )
                })
        }
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Map::new()),
        Err(error) => Err(error),
    }
}

fn write_credentials_root(path: &PathBuf, root: &Map<String, Value>) -> io::Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let rendered = serde_json::to_string_pretty(&Value::Object(root.clone()))
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    let temp_path = path.with_extension("json.tmp");
    fs::write(&temp_path, format!("{rendered}\n"))?;

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Fix the file so the top level is a JSON object: {"anthropic": { ... }} instead of [ ... ] or "..."
  2. If the file was mangled beyond repair, move it aside (mv ~/.claw/credentials.json ~/.claw/credentials.json.bak) — read_credentials_root() treats NotFound/empty as an empty map and OAuth login will recreate it
  3. Validate with jq before saving: jq -e 'type == "object"' ~/.claw/credentials.json
  4. If you generate the file programmatically, serialize a serde_json::Map<String, Value>, never a Vec or String

Example fix

// before — array root, triggers "credentials file must contain a JSON object"
[
  { "anthropic": { "access_token": "..." } }
]

// after — object root keyed by provider
{
  "anthropic": { "access_token": "...", "refresh_token": "..." }
}
Defensive patterns

Strategy: validation

Validate before calling

fn credentials_file_is_object(path: &std::path::Path) -> bool {
    let Ok(contents) = std::fs::read_to_string(path) else { return true }; // missing/empty = OK (empty map)
    serde_json::from_str::<serde_json::Value>(&contents)
        .map(|v| v.is_object())
        .unwrap_or(false)
}

if !credentials_file_is_object(credentials_path.as_ref()) {
    return Err("credentials.json root must be a JSON object".into());
}

Type guard

fn is_credentials_object(value: &serde_json::Value) -> bool {
    value.is_object()
}

Try / catch

match read_credentials() {
    Ok(c) => c,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        // rename the corrupt file aside; an empty map is recreated on next login
        let _ = std::fs::rename(&creds_path, creds_path.with_extension("json.bak"));
        serde_json::Map::new()
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling any credentials read/modify path (oauth.rs:271, 285, 296 — load, store, update) after the credentials file was hand-edited or written by another tool. Examples: someone pasted an array of tokens ([{"anthropic": ...}]) instead of an object, an editor truncated/rewrote the file, or a JSON-lines dump was saved over credentials.json.

Common situations: Manually provisioning OAuth tokens in CI by scripting credentials.json with the wrong top-level shape; a previous version of the tool or a different machine writing a different schema; file synced from a notes app or template that wrapped it in an array; partial manual migration from another credential store.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/71e3d0dad109d7cf. Report an issue: GitHub.