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

  1. Rewrite the path as a clean slash-separated relative path, e.g. "references/tokens.md".
  2. Use path.Clean on the relative portion and reject/strip ".." and "." segments before Parse.
  3. Convert backslashes with strings.ReplaceAll(p, "\\", "/") on Windows-origin input.
  4. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/b75921b1d7a31185. Report an issue: GitHub.