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

  1. Do not remove user skills — they are read-only by design; leave them enabled/disabled instead
  2. 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
  3. Filter user-skill names out of any bulk-delete routine before calling RemoveSkill
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/4dcc061e77c67a99. Report an issue: GitHub.