siyuan-note/siyuan · error

skill directory already exists or is invalid

Error message

skill directory already exists or is invalid

What it means

The "create" action makes a new top-level skill directory containing a SKILL.md manifest. This error is returned when the requested path already exists (Lstat finds anything, including a directory, file, or link) or when the path contains "/", since only top-level skill names can be created this way (subdirectories are made with "mkdir").

Solutions

  1. Check existence first (action "read" or "list") and use "write"/"mkdir" to modify the existing skill instead of "create"
  2. Pick a different skill name
  3. Remove the leftover directory before re-creating
  4. Use "mkdir" for subdirectory creation after the skill exists

Example fix

// before
ManageSkillFiles(SkillFileRequest{Action: "create", Path: "my-skill/docs"})
// after
ManageSkillFiles(SkillFileRequest{Action: "create", Path: "my-skill"})
ManageSkillFiles(SkillFileRequest{Action: "mkdir", Path: "my-skill/docs"})
Defensive patterns

Strategy: validation

Validate before calling

_, err := ManageSkillFiles(SkillFileRequest{Action: "read", Path: name})
if err == nil { // skill already exists — write or pick another name
}
if strings.Contains(name, "/") { // use create for top-level names only
}

Type guard

func canCreateSkill(name string) bool { return name != "" && !strings.Contains(name, "/") }

Try / catch

if err := ManageSkillFiles(createReq); err != nil && strings.Contains(err.Error(), "already exists or is invalid") {
  // fall back to read + write, or choose a new name
}

Prevention

When it happens

Trigger: ManageSkillFiles{Action:"create"} where request.Path contains a slash (e.g. "my-skill/sub"), or root.Lstat(request.Path) returns success — the skill already exists (possibly as a leftover directory).

Common situations: Re-running an idempotent setup script that creates skills on every run; trying to create nested paths with "create" instead of creating the skill first and using "mkdir"; a skill with the same name left behind after a failed delete.

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/5b008bafef037151. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill_manage.go:333

					_, err = readManagedSkillSource(root, p)
					editable = err == nil
				}
			}
			ret.Entries = append(ret.Entries, SkillFileEntry{Path: p, IsDir: entry.IsDir(), Editable: editable})
			return nil
		})
		return ret, err
	}
	if err = checkManagedSkillPath(root, request.Path); err != nil {
		return ret, err
	}
	abs := filepath.Join(root.Name(), filepath.FromSlash(request.Path))
	filelock.Lock(abs)
	defer filelock.Unlock(abs)
	info, statErr := root.Lstat(request.Path)
	if request.Action == "create" {
		if strings.Contains(request.Path, "/") || !errors.Is(statErr, os.ErrNotExist) {
			return ret, errors.New("skill directory already exists or is invalid")
		}
		content := request.Content
		if content == "" {
			content = "---\nname: " + request.Path + "\ndescription: \n---\n\n"
		}
		tmp := ".skill-" + ast.NewNodeID()
		if err = root.Mkdir(tmp, 0755); err != nil {
			return ret, err
		}
		defer root.RemoveAll(tmp)
		if err = writeManagedSkillSource(root, path.Join(tmp, "SKILL.md"), content, true, ""); err != nil {
			return ret, err
		}
		if err = root.Rename(tmp, request.Path); err != nil {
			return ret, err
		}
		ret.Content = &content
		ret.Revision = fmt.Sprintf("%x", sha256.Sum256([]byte(content)))

View on GitHub (pinned to 9f775e8a12)