siyuan-note/siyuan · error
user skill is read-only: %s
Error message
user skill is read-only: %s
What it means
RemoveSkill only deletes skills that live in the workspace SkillsDir(). If the name matches a skill found under UserSkillsDir() (bundled/installed skills shipped with the application), the library refuses to delete it and reports 'user skill is read-only'. User skills are treated as managed content, not user-editable data.
Source
Thrown at kernel/util/skill.go:444
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
skillDir := filepath.Join(dir, name)
if err := os.MkdirAll(skillDir, 0755); err != nil {
return err
}
skillMdPath := filepath.Join(skillDir, "SKILL.md")
return filelock.WriteFile(skillMdPath, []byte(content))
}
func RemoveSkill(name string) error {
if err := validateSkillName(name); err != nil {
return err
}
record, ok := findSkillRecord(readSkillRecords(SkillsDir()), name)
if !ok {
if _, found := findSkillRecord(readSkillRecords(UserSkillsDir()), name); found {
return fmt.Errorf("user skill is read-only: %s", name)
}
return fmt.Errorf("skill not found: %s", name)
}
skillDir := filepath.Join(record.Root, record.DirName)
return os.RemoveAll(skillDir)
}
func RenameSkill(oldName, newName string) error {
if err := validateSkillName(oldName); err != nil {
return err
}
if err := validateSkillName(newName); err != nil {
return err
}
dir := SkillsDir()
record, ok := findSkillRecord(readSkillRecords(dir), oldName)
if !ok {
if _, found := findSkillRecord(readSkillRecords(UserSkillsDir()), oldName); found {View on GitHub (pinned to 8641553a1f)
Solutions
- Do not remove user skills — they are read-only by design; leave them enabled/disabled instead
- If you want an equivalent workspace copy you can manage, create a new skill in the workspace with the desired content (SaveSkill) rather than deleting the user skill
- Filter user-skill names out of any bulk-delete routine before calling RemoveSkill
- Disable the user skill in settings if it should not be visible, instead of deleting it
Example fix
// before
for _, name := range allVisibleSkillNames {
RemoveSkill(name) // errors on user skills
}
// after
for _, name := range workspaceOnlySkillNames {
RemoveSkill(name) // only skills under SkillsDir()
} Defensive patterns
Strategy: try-catch
Validate before calling
// Go: distinguish roots before deleting
if _, ok := findSkillRecord(readSkillRecords(UserSkillsDir()), name); ok {
return fmt.Errorf("%s is a read-only user skill; skipping", name)
} Try / catch
if err := RemoveSkill(name); err != nil {
if strings.Contains(err.Error(), "user skill is read-only") {
log.Printf("skipped built-in skill %s", name)
} else {
return err
}
} Prevention
- Treat UserSkillsDir() content as immutable; only manage SkillsDir() content
- Filter user-skill names out of bulk-delete jobs
- Use enable/disable settings instead of deletion for built-in skills
When it happens
Trigger: RemoveSkill(name) / removeSkill / skillRemove called on a skill whose record resolves from UserSkillsDir() rather than SkillsDir() — i.e. attempting to delete a built-in/installed skill that exists only in the user-skills directory.
Common situations: Trying to clean up 'unused' skills that are actually shipped defaults; scripts iterating all visible skill names and deleting them; confusion between workspace skills and installed user skills after upgrading the app.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- read session file failed: %w
- create session dir failed: %w
- %w: read Vault config directory: %v
- create inline styles directory failed: %w
- This operation is not supported in read-only mode
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/4dcc061e77c67a99.
Report an issue: GitHub.