readSecureFile() is for absolute file paths that should be treated like credentials or other sensitive local inputs. It is stricter than fs.readFile() and different from root().read(): the file path is absolute, but the read is still fd-pinned and permission-checked before bytes are returned.
import { readSecureFile } from "@openclaw/fs-safe/secure-file";
const { buffer, realPath, permissions } = await readSecureFile({
filePath: "/var/lib/app/auth.token",
label: "auth token",
trust: { trustedDirs: ["/var/lib/app"] },
io: { maxBytes: 16 * 1024, timeoutMs: 5_000 },
});The helper:
- requires a local absolute path and rejects UNC/network paths by default
- rejects every non-regular preview and, by default, symlink paths
- opens POSIX paths no-follow and nonblocking before reading, then verifies the opened fd still matches the path and realpath; a FIFO swap cannot block before
timeoutMsowns the byte read - optionally requires the real path to live under one of
trust.trustedDirs - rejects hard-to-verify or unsafe permissions unless
permissions.allowInsecureis set - rejects files owned by another POSIX uid
- enforces
maxBytesbefore and after reading - closes the handle on success, error, and timeout
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless permissions.allowReadableByOthers is true. On Windows, the helper uses the ACL inspection helpers from permissions and refuses the read if ACLs cannot be verified.
Descriptor, pathname, and realpath identity checks use lossless bigint stats internally. The returned stat remains a normal Node Stats object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with path-mismatch. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
type SecureFileReadOptions = {
filePath: string;
label?: string;
trust?: {
trustedDirs?: string[];
allowSymlink?: boolean;
allowNetworkPath?: boolean;
};
permissions?: {
allowInsecure?: boolean;
allowReadableByOthers?: boolean;
};
inject?: {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
exec?: PermissionExec;
};
io?: {
maxBytes?: number;
timeoutMs?: number;
};
};io.maxBytes must be a non-negative safe integer or positive Infinity; zero is an active cap and Infinity disables the cap. Invalid limits reject before filesystem admission.
permissions.allowInsecure is a migration escape hatch. Prefer fixing permissions and using formatPermissionRemediation to show the user what to run. trust.allowNetworkPath is off by default because UNC paths are remote authority, not local filesystem input. inject is for tests and platform adapters; production callers usually leave it unset.
permissions.allowInsecure bypasses only permission checks. Neither it nor inject.platform changes filesystem identity verification, which always uses the actual process platform. trust.allowSymlink permits an alias but still requires its target and realpath to match the opened descriptor.
readSecureFile() throws FsSafeError with codes such as:
| Code | Meaning |
|---|---|
invalid-path |
filePath was not a local absolute path. |
not-found |
The path could not be stat'd before open. |
not-file |
The opened target is not a regular file. |
symlink |
The path is a symlink and trust.allowSymlink is false. |
path-mismatch |
The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
outside-workspace |
realPath is outside trust.trustedDirs. |
permission-unverified |
Required mode/ACL checks could not be completed. |
insecure-permissions |
Mode bits or ACLs grant broader access than allowed. |
not-owned |
POSIX owner uid is not the current process uid. |
too-large |
File size or bytes read exceeded maxBytes. |
timeout |
timeoutMs elapsed while reading. |
Windows inspection failures remain operational permission-unverified errors
and still refuse the read. Their message includes the underlying reason when
available. details includes ownerError for owner-query failures and, when
command diagnostics are available, command, durationMs, timedOut,
exitCode, signal, and stderr. Reasons and stderr are control-character
escaped and limited to 400 characters each (including a truncation marker).
No stdout or target file contents are copied into these display diagnostics.
The original inspection exception is retained as cause; built-in command
errors also retain their original execFile exception in the cause chain.
Treat causes as restricted local diagnostic data. No retries are performed,
and verification order and rejection conditions are unchanged.
- Permissions — standalone POSIX mode and Windows ACL checks.
- Secret files — mode-0600 credential read/write helpers.
- Reading — root-bounded relative reads.