larksuite/cli · error

%s: path %q is a directory, not a file

Error message

%s: path %q is a directory, not a file

What it means

lstatNonDir succeeded but the Lstat info shows the audited path is a directory, and the audit requires a regular file. The security audit deliberately refuses directories so bindings reference actual files.

Source

Thrown at internal/binding/audit.go:88

// orthogonal concern and the audit is intentionally Go-stdlib strict here.
// Callers that accept user-authored config (e.g. resolveFileRef) must
// pre-resolve any such shortcuts before passing the path in.
func requireAbsolutePath(target, label string) error {
	if !filepath.IsAbs(target) {
		return fmt.Errorf("%s: path must be absolute, got %q", label, target)
	}
	return nil
}

// lstatNonDir stats the path without following symlinks, rejecting
// directories. Returns the stat info for downstream steps to reuse.
func lstatNonDir(target, label string) (fs.FileInfo, error) {
	info, err := vfs.Lstat(target)
	if err != nil {
		return nil, fmt.Errorf("%s: cannot stat %q: %w", label, target, err)
	}
	if info.IsDir() {
		return nil, fmt.Errorf("%s: path %q is a directory, not a file", label, target)
	}
	return info, nil
}

// resolveSymlinkIfAllowed resolves a symlink to its target when
// params.AllowSymlinkPath is true, or rejects it otherwise. When the input
// is not a symlink, target is returned unchanged. A symlink that points to
// another symlink is rejected so callers only deal with a single hop.
func resolveSymlinkIfAllowed(target string, linfo fs.FileInfo, params AuditParams) (string, error) {
	if linfo.Mode()&os.ModeSymlink == 0 {
		return target, nil
	}
	if !params.AllowSymlinkPath {
		return "", fmt.Errorf("%s: path %q is a symlink (not allowed)", params.Label, target)
	}
	resolved, err := vfs.EvalSymlinks(target)
	if err != nil {
		return "", fmt.Errorf("%s: cannot resolve symlink %q: %w", params.Label, target, err)

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Point the binding at the concrete file inside the directory (append the filename).
  2. Verify with 'ls -la' what exists at the path and correct the configured path.
  3. If a directory is legitimately expected, this is the wrong audit/API — use a directory-aware path instead of AssertSecurePath for files.
  4. Recreate the layout so the path is a regular file (e.g. remove the mistakenly created directory and restore the file).

Example fix

// before
path := "/home/u/.config/myapp"      // directory
// after
path := "/home/u/.config/myapp/config.yaml"
Defensive patterns

Strategy: validation

Validate before calling

// preflight: reject directories before the audit
if info, err := os.Lstat(p); err == nil && info.IsDir() {
    return fmt.Errorf("%q is a directory; point the binding at a concrete file inside it", p)
}

Type guard

func isDirErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "is a directory, not a file")
}

Try / catch

if err := binding.AssertSecurePath(p, params); err != nil {
    if isDirErr(err) {
        return fmt.Errorf("append the filename to the configured path: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: AssertSecurePath is called with a path that Lstat reports IsDir() — a directory was configured where a file is required (e.g. a directory named like the expected config file, or the user passed a folder path).

Common situations: Config value points at a directory (e.g. ~/.config/myapp/ instead of ~/.config/myapp/config.yaml); workspace layout change turned the expected file into a directory; accidental mkdir with the file's name.

Related errors


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