siyuan-note/siyuan · error

invalid skill name

Error message

invalid skill name: %s

What it means

validateSkillName rejects skill names that are empty, '.' or '..'. It is the first gate for all skill write operations (Read/Save/Remove/Rename/install), preventing degenerate names from reaching filesystem joins.

Solutions

  1. Provide a non-empty skill name before calling the API
  2. Filter out '.' and '..' when iterating directory entries to build skill names
  3. Trim and validate the name at the caller boundary (e.g. strings.TrimSpace, reject if empty)

Example fix

// before
entries, _ := os.ReadDir(util.SkillsDir())
for _, e := range entries {
    util.SaveSkill(e.Name(), content) // panics on "." / ".."
}
// after
for _, e := range entries {
    if e.IsDir() && e.Name() != "." && e.Name() != ".." {
        util.SaveSkill(e.Name(), content)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

func validName(n string) bool {
    return n != "" && n != "." && n != ".."
}

Try / catch

if err := util.ValidateSkillName(name); err != nil { /* reject before calling SaveSkill */ }

Prevention

When it happens

Trigger: ReadSkill, SaveSkill, RemoveSkill, RenameSkill, installSkillDirs, or installFromSingleSkillMD is called with an empty name, ".", or "..".

Common situations: A name variable left unset before calling SaveSkill; a loop iterating directory entries that passes '.'/'..' entries through unfiltered; an API caller omitting the skill name field.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at kernel/util/skill.go:398

		return "", fmt.Errorf("skill resource not found: %s/%s", skillName, resource)
	}
	defer file.Close()
	data, err := io.ReadAll(io.LimitReader(file, maxSkillResourceBytes+1))
	if err != nil {
		return "", fmt.Errorf("skill resource read failed: %s/%s", skillName, resource)
	}
	if len(data) > maxSkillResourceBytes {
		return "", fmt.Errorf("skill resource exceeds the %d byte limit: %s/%s", maxSkillResourceBytes, skillName, resource)
	}
	if !utf8.Valid(data) {
		return "", fmt.Errorf("skill resource is not valid UTF-8: %s/%s", skillName, resource)
	}
	return string(data), nil
}

func validateSkillName(name string) error {
	if name == "" || name == "." || name == ".." {
		return fmt.Errorf("invalid skill name: %s", name)
	}
	if strings.ContainsAny(name, `/\`) {
		return fmt.Errorf("invalid skill name: %s", name)
	}
	dir := SkillsDir()
	abs := filepath.Join(dir, name)
	if !gulu.File.IsSubPath(dir, abs) {
		return fmt.Errorf("invalid skill name: %s", name)
	}
	return nil
}

func ReadSkill(name string, enabledUserSkills []string) (string, error) {
	if err := validateSkillName(name); err != nil {
		return "", err
	}
	if record, ok := findSkillRecord(resolveSkillRecords(enabledUserSkills), name); ok {
		return record.Content, nil

View on GitHub (pinned to 9f775e8a12)