Start

Install

Install

fs-safe is published to npm as @openclaw/fs-safe. It targets Node 22 or newer, ships ESM only, and works on macOS, Linux, and Windows.

#Package managers

pnpm add @openclaw/fs-safe
npm install @openclaw/fs-safe
yarn add @openclaw/fs-safe
bun add @openclaw/fs-safe

#Node version

Minimum Node 22. The package uses fs.promises, fs.constants.O_NOFOLLOW where available, and node:stream/promises. Earlier Node releases will fail at import time.

Verify the runtime:

node --version
# v22.0.0 or newer

#Bun runtime

Bun 1.4.2 can run the same public APIs and load the matching native package. On macOS and Linux, fs-safe uses its Rust N-API addon to call the system realpath implementation for native resolution. Ordinary resolution follows Node's component walk, including lexical normalization of expanded symlink targets, in Rust, with a 1,024-link expansion limit that returns ELOOP for excessive or cyclic expansion. This works around Bun path-resolution defects that otherwise reject restrictive permissions and confuse literal POSIX backslashes with directory separators. The OS still resolves symlinks and canonical file names; fs-safe retains its confinement and file-identity checks.

This works with bun --jitless and needs no runtime FFI or JIT. Use native mode auto or require with the matching addon installed. FS_SAFE_NATIVE_MODE=off still disables all addon loading; auto without the addon falls back to Bun's resolver. Those configurations retain Bun 1.4.2's limitations with restrictive permissions, sockets, literal backslashes, and symlink/parent traversal. Use Node if you need full compatibility without the addon. On Bun POSIX, require also rejects canonicalization when the addon or its canonicalizer is unavailable.

Node and Windows use their existing runtime canonicalizers. On Windows, Bun's recursive directory creation receives an absolute spelling that preserves raw path components, working around its rejection of existing relative . and .. directories. Windows native descriptor-relative operations require Bun to expose the paired libuv descriptor bridge from its host executable; a missing or partial bridge fails explicitly with ENOTSUP. Public paths and caller-supplied filesystem adapters remain unchanged.

The upstream fix is tracked in Bun #42374. The adapter can be removed when the supported Bun baseline includes that fix. Run native compatibility checks with pnpm test:bun:native after building the package and addon. See contributing for the Node/pnpm toolchain and the broader diagnostic suite.

#TypeScript

Types ship with the package — no @types/openclaw__fs-safe needed. The exports map in package.json provides typed entries for every subpath:

import { root, FsSafeError } from "@openclaw/fs-safe";
import { writeJson } from "@openclaw/fs-safe/json";
import { extractArchive } from "@openclaw/fs-safe/archive";

A working tsconfig.json for consumers:

{
  "compilerOptions": {
    "target": "es2022",
    "module": "node18",
    "moduleResolution": "node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

#Subpath exports

Use the main entry for the common surface, or the focused subpaths when you want a leaner import or to depend on a narrower contract:

SubpathContents
@openclaw/fs-safeCommon root, config, output, lock, native-mode, and error exports.
@openclaw/fs-safe/rootroot(), Root, RootDefaults, and root-walk types.
@openclaw/fs-safe/copyDirectory copying and cloning, clone-source creation, filesystem probes, and clone metadata.
@openclaw/fs-safe/guestGuest filesystem protocol: Python source and exit constants for caller-owned transports.
@openclaw/fs-safe/configProcess-global native helper and lock defaults.
@openclaw/fs-safe/pathisPathInside, safeRealpathSync, isWithinDir, error helpers.
@openclaw/fs-safe/outputGuarded staging/finalization for libraries that require an absolute output path.
@openclaw/fs-safe/jsontryReadJson, readJson, readJsonIfExists, writeJson, sync variants.
@openclaw/fs-safe/storefileStore(), fileStoreSync(), and jsonStore<T>().
@openclaw/fs-safe/secretSecret file read/write helpers.
@openclaw/fs-safe/pipeNative-owned, reopenable anonymous pipes for one-shot child input.
@openclaw/fs-safe/atomicreplaceFileAtomic, writeTextAtomic, replaceDirectoryAtomic, movePathWithCopyFallback.
@openclaw/fs-safe/durabilityPinned directories, strict sync, durable directory creation, exclusive publication, and streaming SHA-256.
@openclaw/fs-safe/temptempWorkspace, withTempWorkspace, sync variants, resolveSecureTempRoot.
@openclaw/fs-safe/secure-temp-rootSecure temp root resolution without workspace/store imports.
@openclaw/fs-safe/secure-filereadSecureFile for pinned absolute file reads with permissions checks.
@openclaw/fs-safe/file-lockacquireFileLock, withFileLock, createFileLockManager, and related lock types.
@openclaw/fs-safe/watchGuarded filesystem observation with advisory native hints and polling.
@openclaw/fs-safe/permissionsPOSIX mode helpers, Windows ACL inspection/remediation, raw owner/ACE facts, and private-directory creation.
@openclaw/fs-safe/walkwalkDirectory, walkDirectorySync, related types. Budget-bounded, not root-bounded.
@openclaw/fs-safe/archiveextractArchive, readArchiveEntry, kind resolution, policy types, limits, and preflight helpers.
@openclaw/fs-safe/advancedLower-level composition helpers: path scopes, root-file open, install paths, local-root readers, temp-file targets, sibling-temp writes, regular-file helpers, pathExists, withTimeout, and related advanced types. This surface is less stable than the focused public subpaths.
@openclaw/fs-safe/errorsFsSafeError, FsSafeErrorCode.
@openclaw/fs-safe/typesShared types: DirEntry, PathStat, BasePathOptions, …
@openclaw/fs-safe/test-hooksTest-only hooks for injecting races; registration requires NODE_ENV=test or VITEST=true.

#Runtime dependencies

@openclaw/fs-safe bundles an import-free WASM build of its Rust TAR parser and zstd/bzip2 codecs for guarded archive extraction and bounded entry reads. Plain TAR, gzip, zstd, and bzip2 work in off and missing-native auto, including installs with all optional dependencies omitted; gzip uses Node's built-in decoder. These archive fallbacks need no runtime interpreter, download, or consumer compiler. ZIP fallback uses lazily loaded optional jszip and reports a missing-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability. Native require remains strict, and an available native operation's failure never triggers a WASM retry.

There are no peer dependencies. Exact-version optional packages carry the native targets and npm-compatible OS, CPU, and Linux libc filters install only the matching binary. Windows ARM64 Node selects @openclaw/fs-safe-win32-arm64-msvc; x64 Node on the same host selects the x64 package. Consumers do not run a Rust build, download code at runtime, or execute a postinstall step. Omitting optional dependencies keeps fallback-capable operations working in auto or off. Native-only features, including strict owned-tree temp cleanup, retained-directory staging, and atomic rename-noreplace (including the default no-clobber Root.move()), remain unavailable. Operations without a safe fallback fail with helper-unavailable when the matching package is absent, incompatible, or disabled. No-clobber Root.move() preserves a native loader failure in the error's cause, including the original missing-library or incompatible-glibc diagnostic.

Upgrading an existing 0.5 consumer? Follow Migrating to 0.6 before deploying with native mode require or native-only features.

#Older Linux kernels and seccomp

The native addon supports kernels without openat2 (before Linux 5.6) and containers returning ENOSYS or probe-time EPERM for that syscall. Beneath opens use a guarded no-follow component walk and report best-effort. Nested no-clobber moves retain native parent admission and exact identity checks; keep native mode enabled. If renameat2(RENAME_NOREPLACE) is also unsupported, Linux auto uses move fallbacks, including a directory rename that can replace a concurrently created empty directory. Native require refuses these fallbacks. Strict bounded cleanup still requires openat2 with RESOLVE_NO_XDEV and reports helper-unavailable without it. See Linux capability behavior and limits.

#Windows security fallback

Windows pathname permission inspection, raw owner/DACL inspection, private-directory creation, and secure-file reads work without the addon in auto or off mode using the packaged windows-security-helper.exe. This prebuilt AnyCPU console application targets .NET Framework 4.6.2 or newer (CLR v4.0.30319), with the modern path handling used by Windows PowerShell 5.1. It runs as native x64 on x64 Windows and native ARM64 on Windows 11 ARM with .NET Framework 4.8.1, including when the calling Node is x64. No compiler, .NET SDK, or PowerShell is needed by this fallback at runtime.

Each check starts a separate helper process. The compatibility owner/ACL query passes ["owner", targetPath] as arguments and preserves .NET's named security lookup and ACL normalization. Other paths and expected identities stay in environment variables, and descriptor checks inherit the actual borrowed handle through stdin. No security facts are cached or checks combined. The fallback preserves private DACLs at creation and inspects the same open handle that supplies secure-file bytes. Each capability emits a path-free FS_SAFE_NATIVE_FALLBACK warning once per feature in each JavaScript context. It names the native availability reason and expected package version; see troubleshooting fallback warnings.

Release Windows native addons and windows-security-helper.exe are Authenticode-signed by OpenClaw Foundation with a timestamp. The executable is included in the main npm package and covered by its npm provenance, just like its JavaScript. Source lives in the repository; consumers receive only the executable. Missing or disallowed execution fails closed with a rebuild/reinstall hint. Signing does not guarantee acceptance by every Smart App Control, AppLocker, or WDAC policy; local policy can still block execution in user directories. There is no PowerShell retry. Such hosts can also restrict PowerShell to Constrained Language Mode, where the former Add-Type fallback was unavailable. The package does not bypass policies or add exclusions.

Native require still rejects a missing binding or capability, and an available native operation's error never triggers a command retry. See Permissions, Secure file reads, and reproducible helper builds.

#Native helper policy

#Supported native platforms

Prebuilt bindings cover Linux x64/arm64 (GNU glibc 2.28 or newer, or musl), macOS x64/arm64, Windows x64/arm64, and FreeBSD 14.4+ x64/arm64. FreeBSD provides the documented subset; its other operations retain guarded fallbacks in auto. The GNU baseline includes RHEL 8, Rocky Linux 8, and AlmaLinux 8. A compatible Node 22+ runtime and the kernel/filesystem features required by each operation are still necessary; a loadable addon alone does not guarantee every native capability. Systems older than glibc 2.28 are outside the GNU binary support floor.

A missing or incompatible optional binding (including ERR_DLOPEN_FAILED from glibc) does not prevent importing fs-safe. In auto, supported JavaScript/WASM fallbacks remain available. Native-only operations such as the default no-clobber Root.move() still fail closed with helper-unavailable; require also rejects fallback-capable operations and retains the binding load error as the cause.

#Loading modes

Configure auto (default), off, or require before first use with configureFsSafeNative() or FS_SAFE_NATIVE_MODE. See Native helper policy for mode selection, loader lifetime, and strict failure behavior. Temp workspace cleanupSafety is a separate policy; see the temp workspace contract.

#Verify the install

import { root, FsSafeError } from "@openclaw/fs-safe";
import os from "node:os";
import path from "node:path";

const dir = path.join(os.tmpdir(), "fs-safe-smoke");
await import("node:fs/promises").then((fs) => fs.mkdir(dir, { recursive: true }));

const fs = await root(dir);
await fs.write("hello.txt", "ok\n");
console.log(await fs.readText("hello.txt"));

try {
  await fs.write("../escape.txt", "x");
} catch (err) {
  if (err instanceof FsSafeError) console.log("blocked:", err.code);
}

If the script prints ok followed by blocked: outside-workspace, your install is healthy.

#Next

  • Quickstart — write, read, atomic, temp.
  • Security model — what the boundary defends against.
  • Errors — the closed code union you'll be catching.