larksuite/cli · error
%s %q is inside %s, which is protected by the built-in denyl
Error message
%s %q is inside %s, which is protected by the built-in denylist
What it means
The built-in denylist protects sensitive locations (e.g. config, credentials, CLI-owned state). checkDeny compares the resolved path's ancestors against deny roots — by device/inode identity where possible — and denyError reports the offending flag name, raw value, and protected label.
Source
Thrown at internal/vfs/localfileio/policy.go:293
roots := slices.Concat(denyRoots(), configDirDenyRoots(cwd))
for _, e := range roots {
if matchResolved(resolved, e) || isUnderDir(foldCase(absLiteral), foldCase(e.literal)) {
return denyError(flagName, raw, e.label)
}
}
// Name comparison alone cannot decide containment: case-insensitive and
// case-folding filesystems (APFS folds U+017F to "s", so ".ſſh" opens
// "~/.ssh"), Unicode normalization, and Windows short names all give the
// same directory several spellings. Ask the kernel instead — file identity
// has exactly one answer per directory.
if label, ok := matchByFileIdentity(resolved, roots); ok {
return denyError(flagName, raw, label)
}
return nil
}
func denyError(flagName, raw, label string) error {
return fmt.Errorf("%s %q is inside %s, which is protected by the built-in denylist", flagName, raw, label)
}
// matchByFileIdentity walks resolved upwards and reports the first root that
// is the very same directory as one of the ancestors, compared by device and
// inode rather than by name. Missing ancestors are skipped: a target that does
// not exist yet is decided by its nearest existing parent.
func matchByFileIdentity(resolved string, roots []policyEntry) (string, bool) {
p := resolved
for {
if fi, err := vfs.Lstat(p); err == nil {
for _, e := range roots {
if e.info != nil && os.SameFile(fi, e.info) {
return e.label, true
}
}
}
parent := filepath.Dir(p)
if parent == p {View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Move the file outside the protected root and pass the new path
- Check the label in the message to learn which protected root was matched and pick a different working directory
- If the operation is legitimate CLI-state management, use the dedicated CLI command for that state instead of a generic file flag
Example fix
// before --config /home/me/.lark-cli/config.yaml --upload /home/me/.lark-cli/cache.bin // after --config /home/me/.lark-cli/config.yaml --upload /tmp/exports/cache.bin
Defensive patterns
Strategy: validation
Validate before calling
// resolve symlinks and check the target is not under a protected root before invoking
resolved, _ := filepath.EvalSymlinks(path)
if strings.HasPrefix(resolved, protectedRoot) {
// pick another location
} Try / catch
if strings.Contains(err.Error(), "protected by the built-in denylist") {
// move the file out of the protected root and retry with the new path
} Prevention
- Keep working files out of CLI state/config directories and other known protected roots
- Remember denylist matching follows symlinks by identity — pointing a symlink into a protected root will be caught
- Use a dedicated scratch/export directory for files passed to file flags
When it happens
Trigger: Passing a file/directory inside a denylisted root to a flag that accepts local paths (flagName in the message), e.g. --config pointing under the CLI's own state directory or another protected root; matched even via symlink/alias spellings because matching resolves ancestors.
Common situations: Trying to overwrite CLI credentials/config through a data-upload flag, or a symlink pointing into a protected directory that the denylist catches.
Related errors
- %s: path must be absolute, got %q
- %s: cannot stat %q: %w
- %s: path %q is a directory, not a file
- %s: path %q is a symlink (not allowed)
- %s: path %q is world-readable (mode %04o)
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/0a371be2f319993d.
Report an issue: GitHub.