{"record":{"id":"ab0448cf98f3122d","repo":"larksuite/cli","slug":"q-has-an-empty-relative-path","errorCode":null,"errorMessage":"%q has an empty relative path","messagePattern":"%q has an empty relative path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/skillref/ref.go","lineNumber":35,"sourceCode":"type Ref struct {\n\tSkill string\n\tPath  string\n}\n\n// Parse parses the \"name[/relative/path]\" form accepted by `skills read`.\nfunc Parse(raw string) (Ref, error) {\n\tif raw == \"\" {\n\t\treturn Ref{}, fmt.Errorf(\"skill reference is empty\")\n\t}\n\tskill, path, _ := strings.Cut(raw, \"/\")\n\tif !ValidSkillName(skill) {\n\t\treturn Ref{}, fmt.Errorf(\"%q has invalid skill name %q\", raw, skill)\n\t}\n\tif path != \"\" && (!fs.ValidPath(path) || path == \".\" || strings.Contains(path, `\\`)) {\n\t\treturn Ref{}, fmt.Errorf(\"%q has invalid relative path %q\", raw, path)\n\t}\n\tif path == \"\" && strings.HasSuffix(raw, \"/\") {\n\t\treturn Ref{}, fmt.Errorf(\"%q has an empty relative path\", raw)\n\t}\n\treturn Ref{Skill: skill, Path: path}, nil\n}\n\n// ValidSkillName reports whether name can identify a top-level skill\n// directory. Keep this rule aligned with the skill-tree manifest validator.\nfunc ValidSkillName(name string) bool {\n\treturn name != \"\" && name != \".\" && name != \"..\" && !strings.ContainsAny(name, `/\\`)\n}\n\n// String returns the canonical \"name[/relative/path]\" form.\nfunc (r Ref) String() string {\n\tif r.Path == \"\" {\n\t\treturn r.Skill\n\t}\n\treturn r.Skill + \"/\" + r.Path\n}\n","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/skillref/ref.go#L17-L53","documentation":"A trailing slash (e.g. \"auth/\") leaves the path component empty after strings.Cut, which would ambiguously mean \"the skill's SKILL.md\" while the user clearly asked for a path. Parse rejects this exact shape rather than silently treating it as no path.","triggerScenarios":"Parse(\"auth/\") — trailing slash with no path after it. Common when a base path variable ends with \"/\" and is concatenated with an empty suffix.","commonSituations":"Shell scripts building \"${SKILL}/${SUBPATH}\" where SUBPATH is empty; tab-completion or copy of a directory URL/path including the trailing slash.","solutions":["Drop the trailing slash: use \"auth\" (for SKILL.md) or \"auth/references/tokens.md\" (for a file).","Trim trailing \"/\" with strings.TrimRight(raw, \"/\") before Parse when input is assembled.","If the intent was the skill root, omit the slash entirely — empty Path already means SKILL.md.","Fix the templating/variable so an empty subpath component is skipped instead of leaving a dangling slash."],"exampleFix":"// before\nref, err := skillref.Parse(fmt.Sprintf(\"%s/%s\", skill, subpath)) // \"auth/\"\n// after\nraw := skill\nif subpath != \"\" {\n    raw = skill + \"/\" + subpath\n}\nref, err := skillref.Parse(raw)","handlingStrategy":"validation","validationCode":"raw = strings.TrimRight(raw, \"/\")\nif raw == \"\" { return errors.New(\"empty reference\") }\nref, err := skillref.Parse(raw)","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"empty relative path\") {\n    ref, err = skillref.Parse(strings.TrimRight(raw, \"/\"))\n}","preventionTips":["Skip empty subpath components when templating \"<skill>/<subpath>\"","Trim trailing slashes from path inputs at the CLI boundary","Remember empty Path already means the skill's SKILL.md — no trailing slash needed"],"tags":["go","skillref","input-validation","path-format"],"backgroundTag":"invalid-relative-path","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}