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
- Check existence first (action "read" or "list") and use "write"/"mkdir" to modify the existing skill instead of "create"
- Pick a different skill name
- Remove the leftover directory before re-creating
- 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
- List skills before creating to detect name collisions
- Use create only for top-level names; use mkdir for subdirectories
- Make creation flows idempotent (check-then-create)
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
- skill already exists
- skill directory contains a link or special file
- skill path is not a regular file or directory
- skill source is not a regular file
- 345
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)