gastownhall/beads · error
unable to read plugin file: %w
Error message
unable to read plugin file: %w
What it means
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.
Source
Thrown at cmd/bd/doctor/claude.go:550
// GetClaudePluginVersion returns the installed beads Claude plugin version.
func GetClaudePluginVersion() (version string, installed bool, err error) {
// Get user home directory (cross-platform)
homeDir, err := os.UserHomeDir()
if err != nil {
return "", false, fmt.Errorf("unable to determine home directory: %w", err)
}
// Path to installed_plugins.json
pluginPath := filepath.Join(homeDir, ".claude", "plugins", "installed_plugins.json")
// Read plugin file
data, err := os.ReadFile(pluginPath) // #nosec G304 - path is controlled
if err != nil {
if os.IsNotExist(err) {
return "", false, nil
}
return "", false, fmt.Errorf("unable to read plugin file: %w", err)
}
// First, determine the format version
var versionCheck struct {
Version int `json:"version"`
}
if err := json.Unmarshal(data, &versionCheck); err != nil {
return "", false, fmt.Errorf("unable to parse plugin file: %w", err)
}
// Handle version 2 format (GH#741): plugins map contains arrays
if versionCheck.Version == 2 {
var pluginDataV2 struct {
Plugins map[string][]struct {
Version string `json:"version"`
Scope string `json:"scope"`
} `json:"plugins"`
}View on GitHub (pinned to 71377f2769)
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
Example fix
# before ls -l ~/.claude/plugins/installed_plugins.json # owned by root # after sudo chown $USER ~/.claude/plugins/installed_plugins.json && chmod u+rw ~/.claude/plugins/installed_plugins.json
Defensive patterns
Strategy: type-guard
Validate before calling
import "os"
func pluginFileReadable(home string) error {
p := filepath.Join(home, ".claude", "plugins", "installed_plugins.json")
st, err := os.Stat(p)
if err != nil {
if os.IsNotExist(err) { return nil } // treat as not installed
return err
}
if st.IsDir() { return fmt.Errorf("%s is a directory", p) }
f, err := os.Open(p)
if err != nil { return err }
return f.Close()
} Try / catch
_, installed, err := GetClaudePluginVersion()
if err != nil {
var pe *fs.PathError
if errors.As(err, &pe) && errors.Is(pe.Err, fs.ErrPermission) {
log.Printf("cannot read %s: fix ownership/permissions", pe.Path)
return nil
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- creating .bd-dolt-ok marker: %w
- read .beads/.gitignore: %w
- dolt path is not executable
- failed to create backup directory: %w
- failed to create temp file: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/3b07a40d9be000a3.
Report an issue: GitHub.