{"record":{"id":"8cee06b861e2b8f5","repo":"plandex-ai/plandex","slug":"failed-to-read-s-s","errorCode":null,"errorMessage":"failed to read %s: %s","messagePattern":"failed to read (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/cli/lib/apply.go","lineNumber":660,"sourceCode":"\t\t\tvar mode os.FileMode\n\t\t\tinfo, err := os.Stat(dstPath)\n\t\t\tif err == nil {\n\t\t\t\texists = true\n\t\t\t\tmode = info.Mode()\n\t\t\t} else {\n\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\texists = false\n\t\t\t\t} else {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to check if %s exists: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif exists {\n\t\t\t\t// read file content\n\t\t\t\tbytes, err := os.ReadFile(dstPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to read %s: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Check if the file has changed\n\t\t\t\tif string(bytes) == content {\n\t\t\t\t\t// log.Println(\"File is unchanged, skipping\")\n\t\t\t\t\terrCh <- nil\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tupdatedFiles = append(updatedFiles, path)\n\t\t\t\t\ttoRevert[dstPath] = types.ApplyReversion{Content: string(bytes), Mode: mode}\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmu.Lock()\n\t\t\t\tupdatedFiles = append(updatedFiles, path)\n\t\t\t\ttoRemoveOnRollback = append(toRemoveOnRollback, dstPath)\n\t\t\t\tmu.Unlock()","sourceCodeStart":642,"sourceCodeEnd":678,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/cli/lib/apply.go#L642-L678","documentation":"When the destination file exists, ApplyFiles reads its content to compare against the plan's content and record a reversion snapshot. If os.ReadFile fails (despite Stat succeeding), the goroutine emits this error and the apply aborts.","triggerScenarios":"os.ReadFile(dstPath) errors on an existing file — read permission denied, the 'file' is actually a directory, or the file was deleted/changed between Stat and Read by a concurrent process.","commonSituations":"Files owned by another user with no read bit; path is a directory that Stat succeeded on; concurrent builds/tools deleting files mid-apply; special files that can't be read as bytes.","solutions":["Check the file's read permissions (chmod u+r) or fix ownership","Verify the path is a regular file, not a directory","Re-run the apply if a concurrent process was racing with it; ensure no other tool is mutating the same files","Exclude unreadable/special files from the plan"],"exampleFix":"// before\n$ ls -l src/config.go  # -rw------- root root\n// after\n$ sudo chown $USER src/config.go\n$ plandex apply","handlingStrategy":"validation","validationCode":"info, err := os.Stat(path)\nif err != nil || !info.Mode().IsRegular() {\n  return fmt.Errorf(\"%s is not a readable regular file\", path)\n}\nf, err := os.OpenFile(path, os.O_RDONLY, 0)\nif err != nil { return fmt.Errorf(\"not readable: %w\", err) }\nf.Close()","typeGuard":"func isReadableRegularFile(path string) bool {\n  info, err := os.Stat(path)\n  return err == nil && info.Mode().IsRegular() && info.Mode().Perm()&0o444 != 0\n}","tryCatchPattern":"bytes, err := os.ReadFile(dstPath)\nif err != nil {\n  return fmt.Errorf(\"read %s: %w\", dstPath, err)\n}","preventionTips":["Chown project files to the user running Plandex","Ensure plan paths point at files, not directories","Stop concurrent watchers/tools mutating the tree during apply","Watch for TOCTOU races between Stat and Read"],"tags":["go","filesystem","permissions"],"backgroundTag":"file-read-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}