larksuite/cli · error

: path 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.

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.

Appendix: 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)