siyuan-note/siyuan · error

create a skill before creating its subdirectories

Error message

create a skill before creating its subdirectories

What it means

The "mkdir" action only creates subdirectories inside an existing skill; a path without "/" would be a top-level skill name. This error tells the caller that skills themselves must be created with the "create" action before their subdirectories can be made.

Solutions

  1. Create the skill first with Action "create", then call mkdir with a path that includes the skill prefix
  2. Verify request.Path contains at least one "/" before calling mkdir

Example fix

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

Strategy: validation

Validate before calling

if !strings.Contains(req.Path, "/") {
  // create the skill first with Action "create", then mkdir "<skill>/<sub>"
}

Type guard

func isSkillSubpath(p string) bool { return strings.Contains(p, "/") }

Try / catch

if err := ManageSkillFiles(req); err != nil && strings.Contains(err.Error(), "create a skill before creating") {
  // create the skill, then retry mkdir
}

Prevention

When it happens

Trigger: ManageSkillFiles{Action:"mkdir"} with a slash-free request.Path such as "my-skill" instead of "my-skill/subdir"; trying to create a new skill directory via mkdir.

Common situations: Confusing "create" (new skill + SKILL.md) with "mkdir" (subdirectory); generic file-management code calling mkdir for every directory level including the top level.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at kernel/util/skill_manage.go:356

		}
		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)))
		return ret, nil
	}
	if request.Action == "mkdir" {
		if !strings.Contains(request.Path, "/") {
			return ret, errors.New("create a skill before creating its subdirectories")
		}
		return ret, root.Mkdir(request.Path, 0755)
	}
	if statErr != nil && !(request.Action == "write" && request.Revision == "" && errors.Is(statErr, os.ErrNotExist)) {
		return ret, statErr
	}
	if request.Action == "read" {
		if !info.IsDir() && strings.Contains(request.Path, "/") {
			content, readErr := readManagedSkillSource(root, request.Path)
			if readErr == nil {
				ret.Content = &content
				ret.Revision = fmt.Sprintf("%x", sha256.Sum256([]byte(content)))
				return ret, nil
			}
			ret.ReadOnlyReason = skillSourceReadOnlyReason(readErr)
			if ret.ReadOnlyReason == "" {
				return ret, readErr
			}

View on GitHub (pinned to 9f775e8a12)