{"record":{"id":"3b07a40d9be000a3","repo":"gastownhall/beads","slug":"unable-to-read-plugin-file-w","errorCode":null,"errorMessage":"unable to read plugin file: %w","messagePattern":"unable to read plugin file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/doctor/claude.go","lineNumber":550,"sourceCode":"\n// GetClaudePluginVersion returns the installed beads Claude plugin version.\nfunc GetClaudePluginVersion() (version string, installed bool, err error) {\n\t// Get user home directory (cross-platform)\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"unable to determine home directory: %w\", err)\n\t}\n\n\t// Path to installed_plugins.json\n\tpluginPath := filepath.Join(homeDir, \".claude\", \"plugins\", \"installed_plugins.json\")\n\n\t// Read plugin file\n\tdata, err := os.ReadFile(pluginPath) // #nosec G304 - path is controlled\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn \"\", false, nil\n\t\t}\n\t\treturn \"\", false, fmt.Errorf(\"unable to read plugin file: %w\", err)\n\t}\n\n\t// First, determine the format version\n\tvar versionCheck struct {\n\t\tVersion int `json:\"version\"`\n\t}\n\tif err := json.Unmarshal(data, &versionCheck); err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"unable to parse plugin file: %w\", err)\n\t}\n\n\t// Handle version 2 format (GH#741): plugins map contains arrays\n\tif versionCheck.Version == 2 {\n\t\tvar pluginDataV2 struct {\n\t\t\tPlugins map[string][]struct {\n\t\t\t\tVersion string `json:\"version\"`\n\t\t\t\tScope   string `json:\"scope\"`\n\t\t\t} `json:\"plugins\"`\n\t\t}","sourceCodeStart":532,"sourceCodeEnd":568,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/doctor/claude.go#L532-L568","documentation":"GetClaudePluginVersion reads ~/.claude/plugins/installed_plugins.json to find the beads plugin version. A missing file is treated as 'not installed' (no error); any other read failure (permissions, I/O error, path is a directory) is wrapped here.","triggerScenarios":"GetClaudePluginVersion (via CheckClaudePlugin) when os.ReadFile on installed_plugins.json fails with an error other than NotExist — e.g. permission denied, EIO, or the path exists as a directory — at cmd/bd/doctor/claude.go:550.","commonSituations":"installed_plugins.json created by another user (root) with restrictive modes; ~/.claude/plugins being a file instead of a directory after a bad install; disk I/O errors; partially-deleted Claude installation.","solutions":["Check and fix permissions on ~/.claude/plugins/installed_plugins.json (chmod u+r, ensure ownership by your user)","If the path is a directory or corrupted, reinstall/restart the Claude plugin so it rewrites the file","Verify ~/.claude is a directory (file ~/.claude/plugins) and remove the conflicting file","If you intend no plugin to be installed, remove the broken file so doctor reports 'not installed' instead of an error"],"exampleFix":"# before\nls -l ~/.claude/plugins/installed_plugins.json  # owned by root\n# after\nsudo chown $USER ~/.claude/plugins/installed_plugins.json && chmod u+rw ~/.claude/plugins/installed_plugins.json","handlingStrategy":"type-guard","validationCode":"import \"os\"\nfunc pluginFileReadable(home string) error {\n\tp := filepath.Join(home, \".claude\", \"plugins\", \"installed_plugins.json\")\n\tst, err := os.Stat(p)\n\tif err != nil {\n\t\tif os.IsNotExist(err) { return nil } // treat as not installed\n\t\treturn err\n\t}\n\tif st.IsDir() { return fmt.Errorf(\"%s is a directory\", p) }\n\tf, err := os.Open(p)\n\tif err != nil { return err }\n\treturn f.Close()\n}","typeGuard":null,"tryCatchPattern":"_, installed, err := GetClaudePluginVersion()\nif err != nil {\n\tvar pe *fs.PathError\n\tif errors.As(err, &pe) && errors.Is(pe.Err, fs.ErrPermission) {\n\t\tlog.Printf(\"cannot read %s: fix ownership/permissions\", pe.Path)\n\t\treturn nil\n\t}\n\treturn err\n}","preventionTips":["Run Claude and bd as the same user so plugin files keep consistent ownership","Never replace ~/.claude/plugins with a file; keep it a directory","Watch for interrupted installs that leave partial manifests","Chown files back after running tools with sudo in your home directory"],"tags":["filesystem","permissions","claude-plugin","doctor"],"backgroundTag":"file-read-permission-denied","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}