larksuite/cli · error
%s: path %q is a symlink (not allowed)
Error message
%s: path %q is a symlink (not allowed)
What it means
resolveSymlinkIfAllowed rejects the audited path because it is a symlink and AuditParams.AllowSymlinkPath is false. The secure-path audit refuses symlink indirection by default to prevent path-traversal/link-swap attacks; even when allowed, only a single hop is resolved and a symlink chain is rejected.
Source
Thrown at internal/binding/audit.go:102
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)
}
rinfo, err := vfs.Lstat(resolved)
if err != nil {
return "", fmt.Errorf("%s: cannot stat resolved path %q: %w", params.Label, resolved, err)
}
if rinfo.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("%s: resolved path %q is still a symlink", params.Label, resolved)
}
return resolved, nil
}
// requireInTrustedDirs enforces that effectivePath lives under one of the
// caller-declared trusted directories, if any were declared. An empty
// trustedDirs list disables the check.View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Set AllowSymlinkPath: true in AuditParams if symlinked configs are acceptable in your environment.
- Replace the symlink with a real file copy (cp --remove-destination or rm the link and cp the target).
- Point the binding directly at the symlink's real target path so no link is audited.
- If AllowSymlinkPath is true but it still fails, check the resolved target: a second-hop symlink (link to a link) is rejected — flatten the chain to one hop.
Example fix
// before
params := binding.AuditParams{Label: "config"}
// after
params := binding.AuditParams{Label: "config", AllowSymlinkPath: true} Defensive patterns
Strategy: validation
Validate before calling
// preflight: detect symlink and flatten to real path if policy disallows links
if info, err := os.Lstat(p); err == nil && info.Mode()&os.ModeSymlink != 0 && !params.AllowSymlinkPath {
real, err := filepath.EvalSymlinks(p)
if err != nil { return err }
p = real // or copy the file over the link
} Type guard
func isSymlinkErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "is a symlink (not allowed)")
} Try / catch
if err := binding.AssertSecurePath(p, params); err != nil {
if isSymlinkErr(err) {
if real, e := filepath.EvalSymlinks(p); e == nil {
return binding.AssertSecurePath(real, params)
}
}
return err
} Prevention
- Decide a single policy for symlinks and set AllowSymlinkPath consistently.
- Replace dotfile-manager symlinks with copies when audits must pass.
- Point bindings at the real target path rather than the link.
- Keep symlink chains to one hop — even allowed mode rejects link-to-link.
When it happens
Trigger: AssertSecurePath -> resolveSymlinkIfAllowed where linfo.Mode() has os.ModeSymlink and params.AllowSymlinkPath is false — the file being audited is a symlink (e.g. dotfile managers, synced folders, or package-managed symlinks).
Common situations: Dotfile managers (stow, chezmoi, ln -s) symlink config files into a repo; cloud-sync folders replacing files with symlinks; CI checkout creating symlinks; user manually linking a shared config across projects.
Related errors
- %s: path must be absolute, got %q
- %s: cannot stat %q: %w
- %s: path %q is a directory, not a file
- unsafe output path: %w
- %s %q has multiple hard links, so writing it would also rewr
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/971ee6ac28975ce6.
Report an issue: GitHub.