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

  1. Check and fix permissions on ~/.claude/plugins/installed_plugins.json (chmod u+r, ensure ownership by your user)
  2. If the path is a directory or corrupted, reinstall/restart the Claude plugin so it rewrites the file
  3. Verify ~/.claude is a directory (file ~/.claude/plugins) and remove the conflicting file
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3b07a40d9be000a3. Report an issue: GitHub.