siyuan-note/siyuan · error

skill already exists

Error message

skill already exists: %s

What it means

RenameSkill checks os.Stat(newDir) before renaming and returns 'skill already exists: %s' if a directory with the new name already exists in SkillsDir(). This prevents overwriting an existing skill via rename.

Solutions

  1. Pick a unique newName that does not collide with existing skills
  2. Remove or rename the conflicting skill first if the collision is intentional
  3. Generate a suffix-based unique name when automating renames
  4. Check existence of the target name before calling RenameSkill

Example fix

// before
RenameSkill("draft", "review") // "review" already exists
// after
newName := "review"
if skillExists(newName) {
    newName = "review-2"
}
err := RenameSkill("draft", newName)
Defensive patterns

Strategy: validation

Validate before calling

if (skillExists(newName)) {
  newName = uniquifyName(newName); // e.g. append -2, -3
}

Try / catch

try {
  await renameSkill(oldName, newName);
} catch (e) {
  if (String(e.message).startsWith("skill already exists")) {
    newName = uniquifyName(newName);
    await renameSkill(oldName, newName);
  }
}

Prevention

When it happens

Trigger: Calling RenameSkill(oldName, newName) when a skill directory named newName already exists in SkillsDir().

Common situations: Renaming to a name that collides with another installed skill; retrying a partially completed rename; case-only rename on a case-insensitive filesystem colliding with the original.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/02b5cdcba139010e. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill.go:476

	defer skillManagementLock.Unlock()
	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 {
			return fmt.Errorf("user skill is read-only: %s", oldName)
		}
		return fmt.Errorf("skill not found: %s", oldName)
	}
	oldDir := filepath.Join(dir, record.DirName)
	newDir := filepath.Join(dir, newName)
	if _, err := os.Stat(newDir); err == nil {
		return fmt.Errorf("skill already exists: %s", newName)
	}
	return os.Rename(oldDir, newDir)
}

func parseSkillFrontmatter(text string) (fm map[string]string, body string) {
	fm = map[string]string{}
	text = strings.TrimSpace(text)
	if !strings.HasPrefix(text, "---") {
		return fm, text
	}
	end := strings.Index(text[3:], "\n---")
	if end < 0 {
		return fm, text
	}
	raw := text[3 : 3+end]
	body = strings.TrimSpace(text[3+end+4:])
	for line := range strings.SplitSeq(raw, "\n") {
		line = strings.TrimSpace(line)

View on GitHub (pinned to 9f775e8a12)