fs-safe
Trusted Node.js code that has to touch caller-controlled paths inside a directory it owns gets one boundary it can rely on. root() returns a handle that resolves every relative path against a real directory, refuses anything that escapes it, pins the file you opened, and verifies the write landed where you intended.
#Why
path.resolve(root, input).startsWith(root) validates a string. It does not pin the file you opened, defend against a symlink retarget between check and use, reject hardlinked aliases, or verify that a write landed where you intended after a rename. fs-safe does those things, packaged so every call site picks up the same defense without re-implementing it.
This is a library-level guardrail, not OS-level isolation. It does not replace containers, seccomp, or filesystem permissions — it is for code that already runs with the privileges of its workspace and wants to stop trivial path tricks from escaping it.
#Hello world
import { root } from "@openclaw/fs-safe";
const fs = await root("/safe/workspace", {
hardlinks: "reject",
symlinks: "reject",
mkdir: true,
});
await fs.write("notes/today.txt", "hello\n");
const text = await fs.readText("notes/today.txt");
const parsed = await fs.readJson<{ users: string[] }>("config.json");
await fs.copyIn("uploads/upload.png", "/tmp/upload.png");
await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true });
await fs.remove("notes/archive/today.txt");
#Pick your path
- First time? Install, then walk through the Quickstart. Five minutes from
pnpm addto a working root. - Designing a sandboxed feature. Read the Security model before you trust the boundary, and the Errors reference so you know what to catch.
- Replacing ad-hoc atomic writes. Jump to Atomic writes or, for keyed JSON state, JSON files.
- Extracting an upload. Start at Archive extraction — handles ZIP and TAR with traversal, link, count, and byte limits.
- Running an agent in a sandbox. Private temp workspaces plus secret files cover the common scratch-and-credentials shape.
- Looking up a name. Use the reference section in the sidebar — every public function has a page.
#What you get
| Surface | Use it for |
|---|---|
root() | One boundary for read/write/move/remove inside a trusted directory. |
pathScope() | Same boundary semantics over an absolute path you already trust. |
replaceFileAtomic | Sibling-temp + rename, fsync hooks, mode preservation, copy fallback. |
writeJson / readJson* | JSON state files with strict and lenient read variants. |
jsonStore | Single JSON state file with fallback, atomic writes, and optional locking. |
fileStore | Managed multi-file/blob store with modes, stream writes, copy-in, and pruning. |
tempWorkspace | 0700 scratch dir with auto-cleanup. |
extractArchive | ZIP/TAR extraction with size, count, link, and traversal limits. |
| Secret files | Mode-0600 credentials with size and TOCTOU defense. |
createSidecarLockManager | Cross-process file lock with retry and stale-lock recovery. |
FsSafeError | Closed code union you can branch on. |
#Status
Currently 0.x — APIs are stable in shape but may be tightened before 1.0. The CHANGELOG tracks visible changes. Issues and PRs at the GitHub repo.
Released under the MIT license.