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 := SourceUserView on GitHub (pinned to 7944b8e522)
Solutions
- Verify the file at skill.SkillFilePath exists and is readable (ls -l / cat the path)
- Re-run skill discovery so SkillFilePath is refreshed to the current location
- Restore or fix permissions on the SKILL.md file
- 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
- Re-run discovery after any file move/rename inside skill directories
- Check file permissions when running under different users or containers
- Avoid deleting skill directories while a session is running
- Use errors.As with *fs.PathError to surface the exact failing path
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
- skill not found
- path does not exist: %s
- %w: %s
- name is required
- name must be alphanumeric with hyphens, no leading/trailing/
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/fb68d58e24094203.
Report an issue: GitHub.