{"record":{"id":"71e3d0dad109d7cf","repo":"ultraworkers/claw-code","slug":"credentials-file-must-contain-a-json-object","errorCode":null,"errorMessage":"credentials file must contain a JSON object","messagePattern":"credentials file must contain a JSON object","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"rust/crates/runtime/src/oauth.rs","lineNumber":360,"sourceCode":"                \"HOME is not set (on Windows, set USERPROFILE or HOME, \\\n                 or use CLAW_CONFIG_HOME to point directly at the config directory)\",\n            )\n        })?;\n    Ok(PathBuf::from(home).join(\".claw\"))\n}\n\nfn read_credentials_root(path: &PathBuf) -> io::Result<Map<String, Value>> {\n    match fs::read_to_string(path) {\n        Ok(contents) => {\n            if contents.trim().is_empty() {\n                return Ok(Map::new());\n            }\n            serde_json::from_str::<Value>(&contents)\n                .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?\n                .as_object()\n                .cloned()\n                .ok_or_else(|| {\n                    io::Error::new(\n                        io::ErrorKind::InvalidData,\n                        \"credentials file must contain a JSON object\",\n                    )\n                })\n        }\n        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Map::new()),\n        Err(error) => Err(error),\n    }\n}\n\nfn write_credentials_root(path: &PathBuf, root: &Map<String, Value>) -> io::Result<()> {\n    if let Some(parent) = path.parent() {\n        fs::create_dir_all(parent)?;\n    }\n    let rendered = serde_json::to_string_pretty(&Value::Object(root.clone()))\n        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;\n    let temp_path = path.with_extension(\"json.tmp\");\n    fs::write(&temp_path, format!(\"{rendered}\\n\"))?;","sourceCodeStart":342,"sourceCodeEnd":378,"githubUrl":"https://github.com/ultraworkers/claw-code/blob/08106b0c3771ef5b4a5aa176acccd460e88b7325/rust/crates/runtime/src/oauth.rs#L342-L378","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fix the file so the top level is a JSON object: {\"anthropic\": { ... }} instead of [ ... ] or \"...\"","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","Validate with jq before saving: jq -e 'type == \"object\"' ~/.claw/credentials.json","If you generate the file programmatically, serialize a serde_json::Map<String, Value>, never a Vec or String"],"exampleFix":"// before — array root, triggers \"credentials file must contain a JSON object\"\n[\n  { \"anthropic\": { \"access_token\": \"...\" } }\n]\n\n// after — object root keyed by provider\n{\n  \"anthropic\": { \"access_token\": \"...\", \"refresh_token\": \"...\" }\n}","handlingStrategy":"validation","validationCode":"fn credentials_file_is_object(path: &std::path::Path) -> bool {\n    let Ok(contents) = std::fs::read_to_string(path) else { return true }; // missing/empty = OK (empty map)\n    serde_json::from_str::<serde_json::Value>(&contents)\n        .map(|v| v.is_object())\n        .unwrap_or(false)\n}\n\nif !credentials_file_is_object(credentials_path.as_ref()) {\n    return Err(\"credentials.json root must be a JSON object\".into());\n}","typeGuard":"fn is_credentials_object(value: &serde_json::Value) -> bool {\n    value.is_object()\n}","tryCatchPattern":"match read_credentials() {\n    Ok(c) => c,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {\n        // rename the corrupt file aside; an empty map is recreated on next login\n        let _ = std::fs::rename(&creds_path, creds_path.with_extension(\"json.bak\"));\n        serde_json::Map::new()\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Never hand-edit credentials.json into arrays or wrapped strings — the root must be { \"provider\": { ... } }","Validate provisioning scripts with jq -e 'type == \"object\"' before writing the file","Treat InvalidData from the credentials path as corrupt-file signal: back it up and re-login rather than retrying"],"tags":["oauth","credentials","json","invalid-data","rust"],"backgroundTag":"config-file-invalid-json","analyzedSha":"08106b0c3771ef5b4a5aa176acccd460e88b7325","analyzedAt":"2026-08-18T00:29:38.590Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}