siyuan-note/siyuan · error

rename or delete the skill directory instead of SKILL.md

Error message

rename or delete the skill directory instead of SKILL.md

What it means

SKILL.md at the top level of a skill directory is the manifest that defines the skill. It cannot be moved or deleted on its own; the whole skill directory must be renamed (move with the directory path) or removed. This keeps the manifest and directory lifecycle atomic.

Solutions

  1. To rename a skill, move the directory path: Action "move", Path "my-skill", Target "new-name"
  2. To delete a skill, remove the directory: Action "remove", Path "my-skill"
  3. Edit the manifest's frontmatter (name/description) with action "write" instead of moving the file

Example fix

// before
ManageSkillFiles(SkillFileRequest{Action: "remove", Path: "my-skill/SKILL.md", Revision: rev})
// after
ManageSkillFiles(SkillFileRequest{Action: "remove", Path: "my-skill", Revision: dirRev})
Defensive patterns

Strategy: validation

Validate before calling

func isManifest(p string) bool {
  return strings.Count(p, "/") == 1 && strings.EqualFold(path.Base(p), "SKILL.md")
}
if (req.Action == "move" || req.Action == "remove") && isManifest(req.Path) {
  // redirect to the directory path instead
}

Type guard

func isSkillDirPath(p string) bool { return strings.Count(p, "/") == 0 }

Try / catch

if err := ManageSkillFiles(req); err != nil && strings.Contains(err.Error(), "instead of SKILL.md") {
  // retry against the parent directory path
}

Prevention

When it happens

Trigger: ManageSkillFiles action "move" or "remove" where isManagedSkillManifest(request.Path) is true — i.e. the path has exactly one slash and its base is "SKILL.md" (case-insensitive), e.g. "my-skill/SKILL.md".

Common situations: Client UI let users select SKILL.md and issue delete/rename; scripts deleting manifest files to 'reset' a skill; refactoring code that renames manifest files to change a skill's display name.

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/28da84a677194076. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill_manage.go:390

			if ret.ReadOnlyReason == "" {
				return ret, readErr
			}
		}
		ret.Revision, err = managedSkillRevision(root, request.Path)
		return ret, err
	}
	if info == nil || !info.IsDir() {
		if !strings.Contains(request.Path, "/") {
			return ret, errors.New("only text files inside a skill can be edited")
		}
		if info != nil {
			if _, err = readManagedSkillSource(root, request.Path); err != nil {
				return ret, err
			}
		}
	}
	if (request.Action == "move" || request.Action == "remove") && isManagedSkillManifest(request.Path) {
		return ret, errors.New("rename or delete the skill directory instead of SKILL.md")
	}
	if info != nil {
		revision, revisionErr := managedSkillRevision(root, request.Path)
		if revisionErr != nil {
			return ret, revisionErr
		}
		if request.Revision == "" || request.Revision != revision {
			return ret, errors.New("skill changed; reload it before saving, renaming or deleting")
		}
	}
	switch request.Action {
	case "write":
		if info != nil && info.IsDir() {
			return ret, errors.New("cannot write a skill directory")
		}
		if err = writeManagedSkillSource(root, request.Path, request.Content, info == nil, request.Revision); err != nil {
			return ret, err
		}

View on GitHub (pinned to 9f775e8a12)