{"record":{"id":"b377662aacb12e99","repo":"Hmbown/CodeWhale","slug":"failed-to-write-mcp-config","errorCode":null,"errorMessage":"Failed to write MCP config {}: {}","messagePattern":"Failed to write MCP config (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/lib.rs","lineNumber":8886,"sourceCode":"    }\n\n    McpServerDoctorStatus::Ok(format!(\n        \"stdio server configured (command omitted; {} argument(s), {} environment binding(s))\",\n        server.args.len(),\n        server.env.len()\n    ))\n}\n\nfn save_mcp_config(path: &Path, cfg: &McpConfig) -> Result<()> {\n    if let Some(parent) = path.parent() {\n        std::fs::create_dir_all(parent).with_context(|| {\n            format!(\"Failed to create MCP config directory {}\", parent.display())\n        })?;\n    }\n    let rendered = serde_json::to_string_pretty(cfg)\n        .map_err(|e| anyhow!(\"Failed to serialize MCP config: {e}\"))?;\n    crate::utils::write_atomic(path, rendered.as_bytes())\n        .map_err(|e| anyhow!(\"Failed to write MCP config {}: {}\", path.display(), e))?;\n    Ok(())\n}\n\nfn run_sandbox_command(args: SandboxArgs) -> Result<()> {\n    use crate::sandbox::{CommandSpec, SandboxManager};\n\n    let SandboxCommand::Run {\n        policy,\n        network,\n        writable_root,\n        exclude_tmpdir,\n        exclude_slash_tmp,\n        cwd,\n        timeout_ms,\n        command,\n    } = args.command;\n\n    let policy = parse_sandbox_policy(","sourceCodeStart":8868,"sourceCodeEnd":8904,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/lib.rs#L8868-L8904","documentation":"Raised by save_mcp_config when utils::write_atomic fails to persist the serialized MCP config. write_atomic creates a temp file next to the target, writes, fsyncs, and renames, so the error covers temp-file creation, write, fsync, or rename failures (the parent directory is created first, so directory creation rarely fails). The message includes the target path and the underlying io::Error. Note write_atomic applies Private (0600-style) permissions, which some filesystems reject.","triggerScenarios":"Saving MCP config to a path whose parent exists but is not writable (EACCES), a path that names an existing directory, a full filesystem during the temp write/fsync, a read-only mount, or a filesystem that cannot apply private permission bits (FAT/exFAT, some network mounts).","commonSituations":"Config directory owned by root or another user, XDG config dir on a read-only mount, disk exhausted during `mcp add`, path colliding with an existing directory, macOS TCC or Windows controlled-folder-access blocking the config dir.","solutions":["Check ownership and permissions of the config directory named in the message (ls -ld) and chown/chmod it for the current user","Verify the target path is not an existing directory and remove stale temp siblings left by failed writes","Free disk space and retry the command that saves the config","If the directory cannot be made writable, move the config path to a writable location (set XDG_CONFIG_HOME or the equivalent override) or rerun as the owning user"],"exampleFix":"# before: config dir owned by root, write fails\nsudo codewhale mcp add my-server -- s npx -y server.js\n# after: fix ownership, run as the user\nsudo chown -R \"$USER\" ~/.config/codewhale\ncodewhale mcp add my-server -- npx -y server.js","handlingStrategy":"validation","validationCode":"// Rust: verify the destination is writable before triggering the save\nfn mcp_config_writable(path: &std::path::Path) -> bool {\n    let parent = match path.parent() {\n        Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),\n        _ => std::path::PathBuf::from(\".\"),\n    };\n    if std::fs::create_dir_all(&parent).is_err() || path.is_dir() {\n        return false;\n    }\n    let probe = parent.join(format!(\".mcp-write-probe-{}\", std::process::id()));\n    match std::fs::File::create(&probe) {\n        Ok(_) => { let _ = std::fs::remove_file(&probe); true }\n        Err(_) => false,\n    }\n}","typeGuard":null,"tryCatchPattern":"// Branch on the underlying io error kind for recovery\nif let Err(report) = save_mcp_config(&path, &cfg) {\n    let io = report.chain().find_map(|c| c.downcast_ref::<std::io::Error>());\n    match io.map(|e| e.kind()) {\n        Some(std::io::ErrorKind::PermissionDenied) => { /* fix dir ownership, retry once */ }\n        Some(std::io::ErrorKind::StorageFull) | Some(std::io::ErrorKind::WriteZero) => { /* free space, retry */ }\n        _ => return Err(report),\n    }\n}","preventionTips":["Keep the MCP config directory owned by the invoking user","Pre-flight check free space and write access before batch mcp operations","Never point the MCP config path at a directory or a read-only mount","Treat leftover temp siblings of the config file as evidence of a prior failed save"],"tags":["filesystem","mcp","config","io","permissions"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}