larksuite/cli · error

%s %q is outside the built-in allowlist; allowed roots are %

Error message

%s %q is outside the built-in allowlist; allowed roots are %s (hint: save under one of the allowed roots; flags that support stdin can read an out-of-tree file via '-')

What it means

The VFS policy is allowlist-based: local paths must resolve inside one of the built-in allowed roots. checkAllow resolves the input, matches it against the roots (including by file identity for symlink/alias cases), and reports the allowed roots in the error, plus a hint that stdin ('-') can supply out-of-tree data.

Source

Thrown at internal/vfs/localfileio/policy.go:335

// checkAllow accepts paths under any built-in allow root; everything else is
// rejected with the full allowlist spelled out. Only the resolved form of the
// input participates: matching the pre-resolution literal would grant access
// to any symlink placed inside an allow root, no matter where it points.
func checkAllow(flagName, raw, resolved, cwd string) error {
	roots := allowRoots(cwd)
	for _, e := range roots {
		if matchResolved(resolved, e) {
			return nil
		}
	}
	// Identity matching also settles the permissive direction: when an
	// ancestor is the very same directory as an allow root, the target really
	// is inside it, whatever spelling reached it.
	if _, ok := matchByFileIdentity(resolved, roots); ok {
		return nil
	}
	return fmt.Errorf("%s %q is outside the built-in allowlist; allowed roots are %s "+
		"(hint: save under one of the allowed roots; flags that support stdin can read an out-of-tree file via '-')",
		flagName, raw, allowRootsLabel())
}

// matchResolved reports whether the fully resolved input path falls under the
// entry in either of the entry's namespaces. Comparing the input's resolved
// form against the entry literal is safe in both directions: it only matches
// when the real filesystem location truly is under that literal path.
func matchResolved(resolved string, e policyEntry) bool {
	return isUnderDir(foldCase(resolved), foldCase(e.resolved)) ||
		isUnderDir(foldCase(resolved), foldCase(e.literal))
}

// foldCase normalizes case on platforms whose default filesystems compare
// case-insensitively: NTFS on Windows and APFS/HFS+ on macOS, where
// ~/.SSH/id_rsa and ~/.ssh/id_rsa are the same file and a byte-for-byte
// comparison would walk straight past a deny root. Folding can over-match on
// the rarer case-sensitive volumes of those platforms, which errs toward

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Save/read the file under one of the allowed roots listed in the error message
  2. For input, use '-' (stdin) if the flag supports it: cat /etc/hosts | lark-cli ... --file -
  3. Copy or move the file into an allowed root before invoking the command
  4. Check for symlinks resolving outside the root and reference the real in-root path

Example fix

// before
lark-cli im file upload --file /var/tmp/report.pdf
// after
cp /var/tmp/report.pdf ~/Downloads/ && lark-cli im file upload --file ~/Downloads/report.pdf
Defensive patterns

Strategy: validation

Validate before calling

resolved, _ := filepath.EvalSymlinks(path)
for _, root := range allowedRoots { // from the error message or schema
	if strings.HasPrefix(resolved, root) { return true }
}
return false

Try / catch

if strings.Contains(err.Error(), "outside the built-in allowlist") {
	// parse allowed roots from the message, move the file under one, or pipe via '-'
}

Prevention

When it happens

Trigger: Passing a file path outside every allow root (e.g. /etc/... or an arbitrary user directory) to a file-accepting flag while the policy runs in allowlist mode; also occurs when a symlink resolves outside the roots.

Common situations: Reading an input file from a scratch directory, downloading to a random folder, or CI runners with temp dirs outside the allowed roots.

Related errors


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