charmbracelet/crush · error

read skill %q: %w

Error message

read skill %q: %w

What it means

ReadContent falls back to os.ReadFile(skill.SkillFilePath) for non-builtin skills and wraps any OS-level read failure with the skill ID for context. This is the standard file-read error path: the SKILL.md path recorded during discovery no longer exists, is unreadable, or is a directory. The underlying *fs.PathError is preserved via %w.

Source

Thrown at internal/skills/catalog.go:101

	result := SkillReadResult{
		Name:        skill.Name,
		Description: skill.Description,
		Source:      source,
		Builtin:     skill.Builtin,
	}

	if skill.Builtin {
		embeddedPath := "builtin/" + strings.TrimPrefix(skill.SkillFilePath, BuiltinPrefix)
		content, err := BuiltinFS().ReadFile(embeddedPath)
		if err != nil {
			return nil, SkillReadResult{}, fmt.Errorf("read builtin skill %q: %w", skillID, err)
		}
		return content, result, nil
	}

	content, err := os.ReadFile(skill.SkillFilePath)
	if err != nil {
		return nil, SkillReadResult{}, fmt.Errorf("read skill %q: %w", skillID, err)
	}
	return content, result, nil
}

func skillLabel(skillPaths []string, workingDir string, skill *Skill) (string, SourceType) {
	if skill.Builtin {
		return string(SourceSystem) + ":" + skill.Name, SourceSystem
	}

	cleanFile := filepath.Clean(skill.SkillFilePath)
	for _, base := range skillPaths {
		cleanBase := filepath.Clean(base)
		rel, err := filepath.Rel(cleanBase, cleanFile)
		if err != nil || escapesParent(rel) {
			continue
		}

		source := SourceUser

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Verify the file at skill.SkillFilePath exists and is readable (ls -l / cat the path)
  2. Re-run skill discovery so SkillFilePath is refreshed to the current location
  3. Restore or fix permissions on the SKILL.md file
  4. Remove or repair the broken skill directory so it stops being discovered

Example fix

// before
content, err := skills.ReadContent(active, paths, dir, skillID)

// after — check the skill file first
if info, err := os.Stat(skillFilePath); err != nil || info.IsDir() {
    return fmt.Errorf("skill file %s unavailable", skillFilePath)
}
content, err := skills.ReadContent(active, paths, dir, skillID)
Defensive patterns

Strategy: try-catch

Validate before calling

func readable(path string) error {
    info, err := os.Stat(path)
    if err != nil { return err }
    if info.IsDir() { return fmt.Errorf("%s is a directory", path) }
    f, err := os.Open(path)
    if err != nil { return err }
    return f.Close()
}

Try / catch

content, _, err := skills.ReadContent(active, paths, dir, skillID)
if err != nil {
    var pathErr *fs.PathError
    if errors.As(err, &pathErr) && errors.Is(pathErr, fs.ErrNotExist) {
        return fmt.Errorf("skill file %s was moved or deleted; rediscover skills", pathErr.Path)
    }
    return err
}

Prevention

When it happens

Trigger: Calling skills.ReadContent for a user/project skill whose SkillFilePath points to a deleted, moved, renamed, or permission-denied file; skill discovered on a mount that was later unmounted; file removed between discovery and read (TOCTOU).

Common situations: Skill directory deleted or moved after Crush started; project checked out to a different path; file permissions changed (e.g. chmod 000, root-owned file); .local skill file cleaned by a build.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/fb68d58e24094203. Report an issue: GitHub.