{"record":{"id":"dc9d894b3e6ef4ae","repo":"siyuan-note/siyuan","slug":"reserved-skill-file-name","errorCode":null,"errorMessage":"reserved skill file name","messagePattern":"reserved skill file name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill_manage.go","lineNumber":68,"sourceCode":"\tEntries        []SkillFileEntry\n\tContent        *string\n\tRevision       string\n\tReadOnlyReason string\n}\n\n// 管理操作使用真实的相对路径，不使用技能正文中的名称作为文件标识。\nfunc validateManagedSkillPath(p string) error {\n\tif p == \"\" || !fs.ValidPath(p) || strings.ContainsAny(p, \"\\\\:\") {\n\t\treturn errors.New(\"invalid skill path\")\n\t}\n\tfor _, part := range strings.Split(p, \"/\") {\n\t\tif strings.TrimSpace(part) != part || strings.HasSuffix(part, \".\") ||\n\t\t\tstrings.ContainsAny(part, \"<>\\\"|?*~\") || strings.ContainsFunc(part, unicode.IsControl) {\n\t\t\treturn errors.New(\"invalid skill path component\")\n\t\t}\n\t\tdevice := strings.ToUpper(strings.TrimRight(strings.SplitN(part, \".\", 2)[0], \" .\"))\n\t\tif device == \"CON\" || device == \"PRN\" || device == \"AUX\" || device == \"NUL\" || device == \"CONIN$\" || device == \"CONOUT$\" {\n\t\t\treturn errors.New(\"reserved skill file name\")\n\t\t}\n\t\tif strings.HasPrefix(device, \"COM\") || strings.HasPrefix(device, \"LPT\") {\n\t\t\tnumber := strings.TrimPrefix(strings.TrimPrefix(device, \"COM\"), \"LPT\")\n\t\t\tif len([]rune(number)) == 1 && strings.ContainsAny(number, \"0123456789¹²³\") {\n\t\t\t\treturn errors.New(\"reserved skill file name\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// 从工作空间目录逐层打开根目录，拒绝把管理操作重定向到链接目标。\nfunc openManagedSkillsRoot() (*os.Root, error) {\n\tworkspace, err := os.OpenRoot(DataDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer workspace.Close()","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill_manage.go#L50-L86","documentation":"To avoid Windows reserved device names (CON, PRN, AUX, NUL, CONIN$, CONOUT$), validateManagedSkillPath rejects any path component whose name before the first dot equals one of these (case-insensitive, after trimming trailing spaces/dots). This prevents files that would be created successfully on Unix but break or misbehave on Windows and in sync targets.","triggerScenarios":"Creating, moving (renaming), or writing to a skill file named e.g. 'con.md', 'NUL', 'aux.txt', 'Conin$.sh' — the uppercase base name before '.' matches a reserved device.","commonSituations":"Test files named 'con' or 'nul' by convention; artifacts generated with device-ish names; zips authored on Linux containing such names.","solutions":["Rename the file to a non-reserved base name (e.g. 'console.md' instead of 'con.md')","Avoid CON/PRN/AUX/NUL/CONIN$/CONOUT$ (case-insensitive) as file base names","Sanitize generated filenames against the Windows reserved-name list before install"],"exampleFix":"// before\npath := \"my-skill/con.md\"\n\n// after\npath := \"my-skill/console.md\"\n","handlingStrategy":"validation","validationCode":"const RESERVED = ['con','prn','aux','nul','conin$','conout$'];\nfunction nameOk(part) {\n  return !RESERVED.includes(part.split('.')[0].replace(/[ .]+$/,'').toLowerCase());\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.manageSkillFiles({action: 'create', path});\n} catch (e) {\n  if (e.message === 'reserved skill file name') {\n    // pick a different base name and retry\n  }\n}","preventionTips":["Maintain a reserved-name denylist in any file-naming UI","Never name files CON/PRN/AUX/NUL even on macOS/Linux","Validate names in plugin code before calling write/create"],"tags":["filename","windows-compatibility","reserved-names","go"],"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"}