siyuan-note/siyuan · error

cannot write a skill directory

Error message

cannot write a skill directory

What it means

ManageSkillFiles' 'write' action refuses to overwrite a path that currently exists as a directory. The skill workspace treats directories and files as distinct managed resources, so a write to a directory path is rejected instead of silently truncating or replacing the directory. This protects skill content from accidental destruction.

Solutions

  1. Use the 'move' action or remove the directory before writing to that path
  2. Write to a file path inside the directory instead (e.g. skills/foo/SKILL.md)
  3. Check existence/type of the target path first and pick a distinct file name

Example fix

// before: Path = "skills/foo" where foo is a directory
root.ManageSkillFiles("write", "skills/foo", content, rev)
// after
root.ManageSkillFiles("write", "skills/foo/SKILL.md", content, rev)
Defensive patterns

Strategy: validation

Validate before calling

info, err := root.Lstat(p); if err == nil && info.IsDir() { /* choose a file path or move/remove first */ }

Try / catch

if err := ManageSkillFiles(req); err != nil { if strings.Contains(err.Error(), "cannot write a skill directory") { /* adjust path */ } }

Prevention

When it happens

Trigger: Calling ManageSkillFiles with request.Action == "write" where request.Path resolves (via Lstat) to an existing directory inside the skill root.

Common situations: A path previously created as a directory (e.g. a skill folder) is targeted for write; stale directory left from a failed earlier operation; a path computed with a missing file component like 'skills/foo' when 'foo' is a directory.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at kernel/util/skill_manage.go:404

			}
		}
	}
	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
		}
		ret.Revision = fmt.Sprintf("%x", sha256.Sum256([]byte(request.Content)))
		return ret, nil
	case "move":
		if err = checkManagedSkillPath(root, request.Target); err != nil {
			return ret, err
		}
		if path.Dir(request.Path) != path.Dir(request.Target) {
			return ret, errors.New("skills and text files can only be renamed in the same directory")
		}
		if _, err = root.Lstat(request.Target); !errors.Is(err, os.ErrNotExist) {
			return ret, errors.New("skill destination already exists or is inaccessible")
		}
		return ret, root.Rename(request.Path, request.Target)
	case "remove":

View on GitHub (pinned to 9f775e8a12)