Node.js · TypeScript · MIT

Safe filesystem primitives for Node.js

Race-resistant root-bounded filesystem primitives for Node.js. One root() boundary that survives symlink swaps, traversal, hardlink aliases, and TOCTOU rename races between check and use.

Quickstart GitHub
pnpm add @openclaw/fs-safe
root()pathScope()replaceFileAtomicextractArchivewriteJsonAtomiccreatePrivateTempWorkspaceopenPinnedFileSyncFsSafeError

Other install options →

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 add to 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

SurfaceUse 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.
replaceFileAtomicSibling-temp + rename, fsync hooks, mode preservation, copy fallback.
writeJson / readJson*JSON state files with strict and lenient read variants.
jsonStoreSingle JSON state file with fallback, atomic writes, and optional locking.
fileStoreManaged multi-file/blob store with modes, stream writes, copy-in, and pruning.
tempWorkspace0700 scratch dir with auto-cleanup.
extractArchiveZIP/TAR extraction with size, count, link, and traversal limits.
Secret filesMode-0600 credentials with size and TOCTOU defense.
createSidecarLockManagerCross-process file lock with retry and stale-lock recovery.
FsSafeErrorClosed 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.