{"record":{"id":"5e257eff0e2e4dce","repo":"siyuan-note/siyuan","slug":"invalid-skill-name-s","errorCode":null,"errorMessage":"invalid skill name: %s","messagePattern":"invalid skill name: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":398,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"skill resource not found: %s/%s\", skillName, resource)\n\t}\n\tdefer file.Close()\n\tdata, err := io.ReadAll(io.LimitReader(file, maxSkillResourceBytes+1))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"skill resource read failed: %s/%s\", skillName, resource)\n\t}\n\tif len(data) > maxSkillResourceBytes {\n\t\treturn \"\", fmt.Errorf(\"skill resource exceeds the %d byte limit: %s/%s\", maxSkillResourceBytes, skillName, resource)\n\t}\n\tif !utf8.Valid(data) {\n\t\treturn \"\", fmt.Errorf(\"skill resource is not valid UTF-8: %s/%s\", skillName, resource)\n\t}\n\treturn string(data), nil\n}\n\nfunc validateSkillName(name string) error {\n\tif name == \"\" || name == \".\" || name == \"..\" {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\tif strings.ContainsAny(name, `/\\`) {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\tdir := SkillsDir()\n\tabs := filepath.Join(dir, name)\n\tif !gulu.File.IsSubPath(dir, abs) {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\treturn nil\n}\n\nfunc ReadSkill(name string, enabledUserSkills []string) (string, error) {\n\tif err := validateSkillName(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tif record, ok := findSkillRecord(resolveSkillRecords(enabledUserSkills), name); ok {\n\t\treturn record.Content, nil","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill.go#L380-L416","documentation":"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.","triggerScenarios":"ReadSkill, SaveSkill, RemoveSkill, RenameSkill, installSkillDirs, or installFromSingleSkillMD is called with an empty name, \".\", or \"..\".","commonSituations":"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.","solutions":["Provide a non-empty skill name before calling the API","Filter out '.' and '..' when iterating directory entries to build skill names","Trim and validate the name at the caller boundary (e.g. strings.TrimSpace, reject if empty)"],"exampleFix":"// before\nentries, _ := os.ReadDir(util.SkillsDir())\nfor _, e := range entries {\n    util.SaveSkill(e.Name(), content) // panics on \".\" / \"..\"\n}\n// after\nfor _, e := range entries {\n    if e.IsDir() && e.Name() != \".\" && e.Name() != \"..\" {\n        util.SaveSkill(e.Name(), content)\n    }\n}","handlingStrategy":"validation","validationCode":"func validName(n string) bool {\n    return n != \"\" && n != \".\" && n != \"..\"\n}","typeGuard":null,"tryCatchPattern":"if err := util.ValidateSkillName(name); err != nil { /* reject before calling SaveSkill */ }","preventionTips":["Trim and require non-empty names at the caller boundary","Filter '.' and '..' when iterating directory entries","Validate names before any skill API call"],"tags":["skill","validation","argument"],"backgroundTag":"invalid-argument-value","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}