Secret files
Helpers for reading and writing credentials. Files are written at mode 0o600, dirs at 0o700, with a maximum read size to avoid OOM on bogus input.
import {
readSecretFileSync,
tryReadSecretFileSync,
writeSecretFileAtomic,
DEFAULT_SECRET_FILE_MAX_BYTES,
PRIVATE_SECRET_DIR_MODE,
PRIVATE_SECRET_FILE_MODE,
} from "@openclaw/fs-safe/secret";
#When to use these vs writeJson
| Use these when | Use writeJson when |
|---|---|
| The file is a credential (token, key, password). | The file is application state. |
You want the parent directory created at 0o700 if missing. | You don't care about the parent directory mode. |
| You want a hard size cap on reads (to defend against bogus input). | You're reading bounded JSON state. |
Mode 0o600 is mandatory, not just nice. | Mode is whatever umask gives you. |
#Constants
DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024; // 16 KiB
PRIVATE_SECRET_DIR_MODE = 0o700;
PRIVATE_SECRET_FILE_MODE = 0o600;
The 16 KiB cap is intentionally aggressive — credentials should be small. If you need bigger, pass maxBytes explicitly.
#Reading
#tryReadSecretFileSync(filePath, label, options?)
The lenient reader. Returns the trimmed secret string, or undefined when the path is missing, empty, unreadable, too large, or rejected by the validation checks.
import { tryReadSecretFileSync } from "@openclaw/fs-safe/secret";
const token = tryReadSecretFileSync("/var/lib/app/auth.token", "auth token");
if (token) {
useToken(token);
} else {
await reauthenticate();
}
#readSecretFileSync(filePath, label, options?)
Strict reader. Throws when the file is missing, too large, empty, unreadable, or rejected by the validation checks. Use when failing loudly is the right call:
const token = readSecretFileSync("/var/lib/app/auth.token");
#Read options
type SecretFileReadOptions = {
maxBytes?: number; // default DEFAULT_SECRET_FILE_MAX_BYTES (16 KiB)
rejectSymlink?: boolean;
};
The reader trims the file content and rejects empty results. rejectSymlink blocks a symlink path before the pinned read.
#Writing
#writeSecretFileAtomic(params)
Async. Creates the parent directory at dirMode (default 0o700) if missing, writes content to a sibling temp file at mode (default 0o600), atomically renames over the destination, and re-asserts the file mode after rename.
import { writeSecretFileAtomic } from "@openclaw/fs-safe/secret";
await writeSecretFileAtomic({
rootDir: "/var/lib/app",
filePath: "/var/lib/app/auth.token",
content: token,
});
#Parameters
type WriteSecretFileParams = {
rootDir: string; // trusted root directory (created at dirMode if missing)
filePath: string; // absolute path; must be inside rootDir
content: string | Uint8Array;
mode?: number; // file mode for the new file (default PRIVATE_SECRET_FILE_MODE = 0o600)
dirMode?: number; // mode for the root and intermediate dirs (default PRIVATE_SECRET_DIR_MODE = 0o700)
};
The directory mode is asserted on each component along the path: rootDir, then any intermediate dirs, then the parent. The helper enforces that every component matches dirMode — wider permissions on an existing directory cause the write to fail. Audit and tighten existing secret directories yourself.
For more permissive credentials, override mode:
await writeSecretFileAtomic({
rootDir: "/var/lib/app",
filePath: "/var/lib/app/readonly.token",
content: token,
mode: 0o400, // tighter than the default
});
#Common patterns
#Load on boot, reauthenticate on miss
const token = tryReadSecretFileSync("/var/lib/app/auth.token", "auth token");
if (!token) await runOauthFlow();
#Refresh and persist a token
const fresh = await refreshToken(currentRefresh);
await writeSecretFileAtomic({
rootDir: "/var/lib/app",
filePath: "/var/lib/app/auth.token",
content: JSON.stringify(fresh),
});
#Compose with withTimeout
import { withTimeout } from "@openclaw/fs-safe/advanced";
await withTimeout(
writeSecretFileAtomic({ rootDir, filePath, content }),
5_000,
"persist auth token",
);
#Threat model notes
- These helpers protect the secret file from other processes with the same UID that respect filesystem permissions. They do not defend against root or against attackers who can read process memory.
- Validation failures are tripwires, not authorization. Investigate before clearing a rejected credential file.
- If the destination directory is on a tmpfs that does not honor mode bits, the helpers will set the mode bits but the OS may ignore them. Audit your platform.
#See also
- JSON files —
writeJsonacceptsmode: 0o600for non-secret JSON state. - Atomic writes — the lower-level
replaceFileAtomicused by these helpers. - Private state store — root-bounded JSON+text helpers without secret-file mode policy.