larksuite/cli · error
%q has invalid relative path %q
Error message
%q has invalid relative path %q
What it means
If the path portion after the skill name is non-empty, it must be a valid io/fs path: fs.ValidPath, not ".", and not containing backslashes. This prevents traversal-prone or host-path-shaped references from entering the resolver. The error echoes the full raw reference and the bad path.
Source
Thrown at internal/skillref/ref.go:32
// Ref is one exact canonical or runtime skill reference. Path is relative to
// the named skill; an empty Path denotes the skill's SKILL.md.
type Ref struct {
Skill string
Path string
}
// Parse parses the "name[/relative/path]" form accepted by `skills read`.
func Parse(raw string) (Ref, error) {
if raw == "" {
return Ref{}, fmt.Errorf("skill reference is empty")
}
skill, path, _ := strings.Cut(raw, "/")
if !ValidSkillName(skill) {
return Ref{}, fmt.Errorf("%q has invalid skill name %q", raw, skill)
}
if path != "" && (!fs.ValidPath(path) || path == "." || strings.Contains(path, `\`)) {
return Ref{}, fmt.Errorf("%q has invalid relative path %q", raw, path)
}
if path == "" && strings.HasSuffix(raw, "/") {
return Ref{}, fmt.Errorf("%q has an empty relative path", raw)
}
return Ref{Skill: skill, Path: path}, nil
}
// ValidSkillName reports whether name can identify a top-level skill
// directory. Keep this rule aligned with the skill-tree manifest validator.
func ValidSkillName(name string) bool {
return name != "" && name != "." && name != ".." && !strings.ContainsAny(name, `/\`)
}
// String returns the canonical "name[/relative/path]" form.
func (r Ref) String() string {
if r.Path == "" {
return r.Skill
}View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Rewrite the path as a clean slash-separated relative path, e.g. "references/tokens.md".
- Use path.Clean on the relative portion and reject/strip ".." and "." segments before Parse.
- Convert backslashes with strings.ReplaceAll(p, "\\", "/") on Windows-origin input.
- Verify against the actual files shipped inside the skill directory (embedded FS layout), not host disk layout.
Example fix
// before
ref, err := skillref.Parse("auth/..\\references\\tokens.md")
// after
ref, err := skillref.Parse("auth/references/tokens.md") Defensive patterns
Strategy: validation
Validate before calling
if path != "" && (!fs.ValidPath(path) || path == "." || strings.Contains(path, `\\`)) {
return fmt.Errorf("invalid relative path %q; use a clean slash-separated relative path", path)
} Type guard
func validRelPath(p string) bool {
return p == "" || (fs.ValidPath(p) && p != "." && !strings.Contains(p, `\\`))
} Try / catch
if _, err := skillref.Parse(raw); err != nil {
var pe *fs.PathError
_ = pe
return fmt.Errorf("clean the path with path.CutPrefix/path.Clean and retry: %w", err)
} Prevention
- Apply path.Clean and reject any ".." segment before parsing user-supplied paths
- Convert backslashes on Windows-origin input before concatenation
- Verify the path matches files actually shipped inside the embedded skill directory
When it happens
Trigger: Parse("auth/./TOKENS.md"), Parse("auth/../secret"), Parse("auth/C:\\x"), Parse("auth/a\\b.md"), any path with leading "/" or empty segments like "auth//x".
Common situations: Copying a Windows file path into the reference; joining paths with filepath.Join on Windows before parsing; injecting "../" from user input; passing an absolute host path expecting it to resolve into the embedded tree.
Related errors
- %s %q resolves outside the current working directory (hint:
- %s: path must be absolute, got %q
- %s: path %q is a directory, not a file
- %w: source %q: %w
- %w: target %q: %w
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/b75921b1d7a31185.
Report an issue: GitHub.