{"record":{"id":"8114a39ac1750fbf","repo":"sipeed/picoclaw","slug":"failed-to-save-config-w-8114a3","errorCode":null,"errorMessage":"failed to save config: %w","messagePattern":"failed to save config: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/picoclaw/internal/mcp/helpers.go","lineNumber":123,"sourceCode":"\nfunc saveValidatedConfig(cfg *config.Config) error {\n\tif cfg == nil {\n\t\treturn fmt.Errorf(\"config is nil\")\n\t}\n\n\tnormalizedCfg := normalizedConfigForSave(cfg)\n\n\tdata, err := json.Marshal(normalizedCfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to serialize config: %w\", err)\n\t}\n\n\tif err := validateConfigDocument(data); err != nil {\n\t\treturn err\n\t}\n\n\tif err := config.SaveConfig(internal.GetConfigPath(), normalizedCfg); err != nil {\n\t\treturn fmt.Errorf(\"failed to save config: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc normalizedConfigForSave(cfg *config.Config) *config.Config {\n\tclone := *cfg\n\tif cfg.Tools.MCP.Servers == nil {\n\t\treturn &clone\n\t}\n\n\tclone.Tools = cfg.Tools\n\tclone.Tools.MCP = cfg.Tools.MCP\n\tclone.Tools.MCP.Servers = make(map[string]config.MCPServerConfig, len(cfg.Tools.MCP.Servers))\n\tfor name, server := range cfg.Tools.MCP.Servers {\n\t\tif server.Type != \"\" {\n\t\t\tserver.Type = config.NormalizeMCPTransportType(server.Type)\n\t\t}","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/cmd/picoclaw/internal/mcp/helpers.go#L105-L141","documentation":"The serialized document already passed schema validation, but config.SaveConfig could not persist it (helpers.go:122-124). This is a filesystem-level failure: permission denied on the config file or directory, missing parent directory, disk full, or a failed atomic temp-write/rename.","triggerScenarios":"Read-only $HOME or config dir; config file owned by root after earlier sudo use; disk exhausted; leftover temp file from a crashed write blocking rename.","commonSituations":"Running the CLI under sudo once so the config becomes root-owned; CI containers with read-only mounts; full disks in dev VMs.","solutions":["Check permissions and ownership of the config path and its directory; fix with chown/chmod","Confirm the filesystem is writable (mount | grep <path>) and disk has space (df -h)","Remove stale .tmp/.bak artifacts next to the config left by an interrupted save, then retry"],"exampleFix":"# before: config owned by root after sudo use\n# after\nsudo chown -R \"$USER\" ~/.config/picoclaw\npicoclaw mcp list","handlingStrategy":"try-catch","validationCode":"cfgDir=$(dirname \"$cfgPath\")\n[ -w \"$cfgDir\" ] || { echo \"config dir not writable: $cfgDir\" >&2; exit 1; }\ndf -h \"$cfgDir\" | tail -1   # confirm space","typeGuard":null,"tryCatchPattern":"if err := saveCmd.Execute(); err != nil {\n\tif strings.Contains(err.Error(), \"failed to save config\") {\n\t\t// underlying error is wrapped; check for fs errors\n\t\tif errors.Is(err, fs.ErrPermission) {\n\t\t\tfmt.Fprintln(os.Stderr, \"fix permissions on the config directory and retry\")\n\t\t}\n\t\tif errors.Is(err, fs.ErrSpace /* or ENOSPC via syscall */) {\n\t\t\tfmt.Fprintln(os.Stderr, \"disk full — free space and retry\")\n\t\t}\n\t}\n}","preventionTips":["Keep the config directory user-owned (avoid mixing sudo and normal runs)","Monitor disk space on machines that write configs frequently","Clean up stale temp files next to the config after crashed saves"],"tags":["filesystem","config","permissions","cli"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}