[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81363":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":16,"stars30d":17,"stars90d":15,"forks30d":15,"starsTrendScore":18,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":21,"hasPages":23,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":28,"readmeContent":29,"aiSummary":30,"trendingCount":15,"starSnapshotCount":15,"syncStatus":31,"lastSyncTime":32,"discoverSource":33},81363,"fs-safe","openclaw\u002Ffs-safe","openclaw","Race-resistant root-bounded filesystem primitives for Node.js.","https:\u002F\u002Ffs-safe.io",null,"TypeScript",44,12,1,0,3,5,9,51.34,"MIT License",false,"main",true,[25,26,27],"file-access","safe","typescript","2026-06-12 04:01:33","# 🛡️ @openclaw\u002Ffs-safe\n\n![fs-safe banner](docs\u002Fassets\u002Freadme-banner.jpg)\n\n[![npm](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F@openclaw\u002Ffs-safe.svg?color=10b981&label=npm)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@openclaw\u002Ffs-safe)\n[![ci](https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Ffs-safe\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Ffs-safe\u002Factions\u002Fworkflows\u002Fci.yml)\n[![node](https:\u002F\u002Fimg.shields.io\u002Fnode\u002Fv\u002F@openclaw\u002Ffs-safe.svg?color=10b981)](https:\u002F\u002Fnodejs.org)\n[![license](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fl\u002F@openclaw\u002Ffs-safe.svg?color=10b981)](LICENSE)\n[![docs](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fdocs-fs--safe.io-10b981)](https:\u002F\u002Ffs-safe.io)\n\nCapability-style filesystem roots for Node.js apps that handle untrusted relative paths.\n\nThink Go's `os.Root` \u002F `OpenInRoot` or Rust's [`cap-std`](https:\u002F\u002Fgithub.com\u002Fbytecodealliance\u002Fcap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and refuses to escape — through `..`, symlink swaps, hardlink aliases, or TOCTOU rename races between check and use.\n\n```ts\nimport { root } from \"@openclaw\u002Ffs-safe\";\n\nconst fs = await root(\"\u002Fsafe\u002Fworkspace\");\nawait fs.write(\"notes\u002Ftoday.txt\", \"hello\\n\");   \u002F\u002F ok\nawait fs.write(\"..\u002Fescape.txt\", \"x\");            \u002F\u002F throws FsSafeError(\"outside-workspace\")\n```\n\nThat's the whole pitch. `root()` is the product; the rest of the package — JSON stores, atomic writes, secret files, archive extraction, temp workspaces — is supporting cast for the same boundary.\n\nFull docs and reference at **[fs-safe.io](https:\u002F\u002Ffs-safe.io)**.\n\n## Contents\n\n[Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Atomic writes](#atomic-writes) · [External outputs](#external-outputs) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)\n\n## Why this exists\n\nMost Node code that has to touch caller-controlled paths reaches for:\n\n```ts\npath.resolve(root, input).startsWith(root)\n```\n\nThat validates a *string*. It does not pin the file you opened, defend against a symlink retarget between check and use, reject hardlinked aliases of out-of-tree inodes, or verify that a write landed where you intended after a rename. The pieces to do those things exist scattered across the ecosystem — [`write-file-atomic`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fwrite-file-atomic) for atomic writes, `tar` \u002F `jszip` for archive extraction, various `safefs`-style convenience wrappers — but none of them give you one root handle with traversal-resistant semantics across every operation.\n\nThe same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot`](https:\u002F\u002Fgo.dev\u002Fblog\u002Fosroot); Rust has had [`cap-std`](https:\u002F\u002Fgithub.com\u002Fbytecodealliance\u002Fcap-std) for years. Node's `fs` is path-string-oriented and exposes flags like `O_NOFOLLOW` but not an ergonomic \"operate inside this root\" API. `fs-safe` fills that gap.\n\n| | Root boundary | Atomic writes | Symlink\u002Fhardlink defense | TOCTOU resistance | Archive extraction |\n|---|---|---|---|---|---|\n| `path.resolve().startsWith()` | string check only | – | – | – | – |\n| [`write-file-atomic`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fwrite-file-atomic) | – | ✓ | – | – | – |\n| Go [`os.Root`](https:\u002F\u002Fgo.dev\u002Fblog\u002Fosroot) \u002F Rust [`cap-std`](https:\u002F\u002Fgithub.com\u002Fbytecodealliance\u002Fcap-std) | ✓ | platform | ✓ | ✓ | – |\n| **`@openclaw\u002Ffs-safe`** | **✓** | **✓** | **✓** | **✓ (POSIX fd-relative)** | **✓ (ZIP\u002FTAR)** |\n\n## Not a sandbox\n\nThis is a **library-level guardrail**, not OS-level isolation. It does not replace containers, seccomp, AppArmor, 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. If your threat model is a hostile process, you need OS isolation; if your threat model is \"an agent, plugin, upload handler, or CLI will eventually be tricked into writing somewhere it shouldn't,\" `fs-safe` catches that.\n\n## Install\n\n```sh\npnpm add @openclaw\u002Ffs-safe\n```\n\nNode 20.11 or newer. Core root\u002Fpath\u002Fjson\u002Ftemp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP\u002FTAR support; installs that omit optional dependencies can still use every non-archive subpath.\n\nOn POSIX, `root()` uses one process-global persistent Python helper for the\nfd-relative operations Node does not expose ergonomically (`renameat`,\n`unlinkat`, recursive `mkdirat`-style walks, and parent-fd writes). Configure it\nbefore first use when you need a strict environment policy:\n\n```ts\nimport { configureFsSafePython } from \"@openclaw\u002Ffs-safe\";\n\nconfigureFsSafePython({ mode: \"auto\" });    \u002F\u002F default: use helper, fall back if unavailable\nconfigureFsSafePython({ mode: \"off\" });     \u002F\u002F never spawn Python; use best-effort Node fallbacks\nconfigureFsSafePython({ mode: \"require\" }); \u002F\u002F fail closed if helper cannot start\n```\n\nEquivalent env vars: `FS_SAFE_PYTHON_MODE=auto|off|require` and\n`FS_SAFE_PYTHON=\u002Fpath\u002Fto\u002Fpython3`. Without Python, `fs-safe` keeps lexical and\ncanonical root checks, no-follow opens, atomic temp+rename writes, and\npost-write identity verification. What you lose is the strongest POSIX\nfd-relative protection against a same-process-user racer swapping parent\ndirectories between validation and mutation. Windows already uses the Node\nfallback path. See the [Python helper policy](docs\u002Fpython-helper.md) for\ndeployment guidance.\n\n## Quick start\n\n```ts\nimport { root } from \"@openclaw\u002Ffs-safe\";\n\nconst fs = await root(\"\u002Fsafe\u002Fworkspace\", {\n  hardlinks: \"reject\",\n  symlinks: \"reject\",\n  mkdir: true,\n  mode: 0o600,\n});\n\nawait fs.write(\"notes\u002Ftoday.txt\", \"hello\\n\");\nconst text = await fs.readText(\"notes\u002Ftoday.txt\");\nconst config = await fs.readJson(\"config.json\");\nawait fs.copyIn(\"uploads\u002Fupload.png\", \"\u002Ftmp\u002Fupload.png\");\nawait fs.move(\"notes\u002Ftoday.txt\", \"notes\u002Farchive\u002Ftoday.txt\", { overwrite: true });\nawait fs.remove(\"notes\u002Farchive\u002Ftoday.txt\");\n```\n\n`root()` takes the trusted directory; relative paths in subsequent calls are resolved against it. Defaults you pass to `root()` apply to every call below; per-call options override them.\n\nWhen you need metadata or a `FileHandle`:\n\n```ts\nconst { buffer, realPath, stat } = await fs.read(\"notes\u002Ftoday.txt\");\nconst opened = await fs.open(\"notes\u002Ftoday.txt\");\n```\n\n`create()` is the don't-clobber variant of `write()` and throws `already-exists` when the target already exists:\n\n```ts\nawait fs.create(\"notes\u002FREADME.md\", \"seed\\n\"); \u002F\u002F throws if it already exists\n```\n\n`write()` replaces file contents by default; pass `{ overwrite: false }` or use `create()` when an existing file should be an error. `move()` defaults to no clobber because it can otherwise delete an unrelated target while also consuming the source. Pass `{ overwrite: true }` when replacing the target is intended.\n\nUse `ensureRoot()` when a computed relative directory target resolves to the root itself (`\"\"` or `\".\"`) and you want the operation to be accepted. `root()` still requires the trusted root directory to already exist.\n\n## Reading\n\nPick the narrowest read shape that gives you what you need:\n\n```ts\nawait fs.readJson(\"config.json\"); \u002F\u002F parsed value; validate it at your boundary\nawait fs.readText(\"notes\u002Ftoday.txt\");\nawait fs.readBytes(\"image.png\");\nawait fs.read(\"notes\u002Ftoday.txt\"); \u002F\u002F { buffer, realPath, stat }\nconst opened = await fs.open(\"large.log\"); \u002F\u002F FileHandle for streaming\n```\n\nFor streams, use `open()` and the returned `FileHandle`:\n\n```ts\nawait using opened = await fs.open(\"large.log\");\n{\n  const stream = opened.handle.createReadStream();\n  \u002F\u002F consume stream\n}\n```\n\nRoot reads default to `DEFAULT_ROOT_MAX_BYTES` (16 MiB). Pass a larger `maxBytes`\nfor expected large reads, or `Number.POSITIVE_INFINITY` when the caller has a\nseparate size budget.\n\n`reader()` returns a callback that reads absolute or relative paths through the same root boundary. It is useful for APIs that accept a `(path) => Promise\u003CBuffer>` loader. Absolute paths outside the root are rejected with `outside-workspace`. `readAbsolute()` has the same absolute-path behavior directly.\n\nWhen you need a writable `FileHandle`, use `openWritable()` and prefer `await using` for cleanup:\n\n```ts\nawait using opened = await fs.openWritable(\"logs\u002Fcurrent.log\", { writeMode: \"append\" });\n{\n  await opened.handle.appendFile(\"line\\n\");\n}\n```\n\n`nonBlockingRead` is the only I\u002FO scheduling knob in `RootDefaults`; it applies to read\u002Fopen operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks`, `symlinks`, and `denyMutations`.\n\n```ts\nconst locked = await root(\"\u002Fsrv\u002Fworkspace\", {\n  denyMutations: {\n    paths: [\"\u002Fsrv\u002Fworkspace\u002F.env\"],\n    prefixes: [\"\u002Fsrv\u002Fworkspace\u002F.ssh\"],\n  },\n});\n\nawait locked.write(\".env\", \"token\"); \u002F\u002F FsSafeError code \"denied-path\"\n```\n\n`stat()`, `exists()`, and `list()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operations that must be race-resistant at the point of use.\n\n## Subpaths\n\nThe main entry point is intentionally small: `root`, the root option\u002Fresult\ntypes, and `FsSafeError`. Use subpaths for everything else. Low-level helpers\nthat OpenClaw needs to compose higher-level APIs are grouped under\n`@openclaw\u002Ffs-safe\u002Fadvanced` instead of being separate public leaf contracts.\n\n| Subpath | Contents |\n|---|---|\n| `@openclaw\u002Ffs-safe\u002Froot` | `root()`, `Root`, `RootDefaults`, related types |\n| `@openclaw\u002Ffs-safe\u002Fconfig` | process-global Python helper configuration |\n| `@openclaw\u002Ffs-safe\u002Fpath` | canonical path checks: `isPathInside`, `safeRealpathSync`, `isNotFoundPathError`, `isSymlinkOpenError` |\n| `@openclaw\u002Ffs-safe\u002Fjson` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants |\n| `@openclaw\u002Ffs-safe\u002Foutput` | `writeExternalFileWithinRoot` for external libraries that need a temp output path |\n| `@openclaw\u002Ffs-safe\u002Fstore` | `fileStore`, `fileStoreSync`, and `jsonStore` |\n| `@openclaw\u002Ffs-safe\u002Fsecret` | strict and try-style secret file read\u002Fwrite helpers |\n| `@openclaw\u002Ffs-safe\u002Fatomic` | `replaceFileAtomic`, `replaceFileAtomicSync`, `replaceDirectoryAtomic`, `movePathWithCopyFallback` |\n| `@openclaw\u002Ffs-safe\u002Ftemp` | `tempWorkspace`, `tempWorkspaceSync`, `withTempWorkspace`, `resolveSecureTempRoot` |\n| `@openclaw\u002Ffs-safe\u002Fsecure-file` | fd-pinned absolute file reads with owner, mode, ACL, trusted-dir, size, and timeout checks |\n| `@openclaw\u002Ffs-safe\u002Ffile-lock` | `acquireFileLock`, `withFileLock`, `createFileLockManager`, and related lock types |\n| `@openclaw\u002Ffs-safe\u002Fpermissions` | POSIX mode and Windows ACL inspection plus remediation formatting helpers |\n| `@openclaw\u002Ffs-safe\u002Fwalk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |\n| `@openclaw\u002Ffs-safe\u002Farchive` | `extractArchive`, `resolveArchiveKind`, `ArchiveLimitError`, preflight helpers |\n| `@openclaw\u002Ffs-safe\u002Fadvanced` | lower-level composition helpers such as path scopes, root-file open, install paths, filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |\n| `@openclaw\u002Ffs-safe\u002Ferrors` | `FsSafeError`, `FsSafeErrorCode` |\n| `@openclaw\u002Ffs-safe\u002Ftypes` | shared types: `DirEntry`, `PathStat`, … |\n| `@openclaw\u002Ffs-safe\u002Ftest-hooks` | hooks the test suite uses to inject races; only active under `NODE_ENV=test` |\n\n## Failure semantics in the name\n\nWhen two helpers behave differently on the same input, the difference is in the name, not the docs.\n\n```ts\nimport { readJson, tryReadJson } from \"@openclaw\u002Ffs-safe\u002Fjson\";\n\nawait tryReadJson(\".\u002Fconfig.json\"); \u002F\u002F returns null on missing or invalid\nawait readJson(\".\u002Fmanifest.json\");  \u002F\u002F throws on missing or invalid\n```\n\nFor one-off structured reads under a trusted root, `readRootJsonObjectSync()`\nperforms the root-bounded open and JSON object validation in one step. Use\n`readRootStructuredFileSync()` when the parser lives outside fs-safe, such as\nJSON5-backed plugin manifests.\n\n## Atomic writes\n\n`replaceFileAtomic()` writes a sibling temp file, optionally fsyncs it, and renames it over the destination. Mode preservation, rename retry \u002F copy fallback on `EPERM`, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved.\n\n```ts\nimport { replaceFileAtomic } from \"@openclaw\u002Ffs-safe\u002Fatomic\";\n\nawait replaceFileAtomic({\n  filePath: \"\u002Fsafe\u002Fworkspace\u002Fstate.json\",\n  content: JSON.stringify(state, null, 2),\n  mode: 0o600,\n  syncTempFile: true,\n  syncParentDir: true,\n});\n```\n\n`replaceFileAtomicSync()` covers the synchronous case with the same options shape. Both accept an injectable `fileSystem` for tests.\n\n## External outputs\n\nUse `writeExternalFileWithinRoot()` when a browser download, renderer, media\ntool, or native library needs an absolute path to write to:\n\n```ts\nimport { writeExternalFileWithinRoot } from \"@openclaw\u002Ffs-safe\u002Foutput\";\n\nawait writeExternalFileWithinRoot({\n  rootDir: \"\u002Fsafe\u002Fworkspace\u002Fdownloads\",\n  path: \"reports\u002Ftoday.pdf\",\n  write: async (filePath) => {\n    await download.saveAs(filePath);\n  },\n});\n```\n\nThe callback receives a private temp file path, not the final destination. After\nthe callback returns, fs-safe finalizes the staged file with `Root.copyIn()`,\ncreating missing parents by default and rejecting traversal, symlink parent\nescapes, hardlinked final targets, and size-limit violations.\n\nUse it when the final filename is known before the external writer runs. If the\nfilename depends on sniffing the produced bytes, write to a private temp\nworkspace first, then finalize through the normal root APIs after validation.\n\n## Stores\n\nUse `fileStore().json()` for small state files that need explicit fallback\nreads, atomic writes, and optional sidecar locking around read-modify-write\nupdates:\n\n```ts\nimport { fileStore } from \"@openclaw\u002Ffs-safe\u002Fstore\";\n\nconst files = fileStore({ rootDir: \"\u002Fsafe\u002Fworkspace\u002Fstate\", private: true });\nconst store = files.json(\"settings.json\", { lock: true });\n\nawait store.updateOr({ enabled: false }, (current) => ({ ...current, enabled: true }));\n```\n\n`jsonStore({ filePath })` is the absolute-path convenience wrapper for the same\nprimitive.\n\nUse `update()` when missing state is part of your model; use `updateOr()` for\nthe common merge-into-defaults case. Standalone helpers use options bags\nbecause they do not carry a bound root and often need multiple authority, path,\nand policy knobs.\n\nSidecar locks fail closed on stale holders by default. Use the [file lock docs](docs\u002Fsidecar-lock.md) for the lower-level `remove-if-unchanged` recovery mode when your application can prove the old owner is gone.\n\nUse `fileStore()` for cache\u002Fblob\u002Fmedia-style directories where callers\nneed safe relative paths, size limits, atomic replacement, stream writes, and\nTTL cleanup behind one root. Pass `private: true` for credentials, auth\nprofiles, tokens, and per-agent private state; private mode keeps the same\nstore shape while routing writes through the secret-file atomic path.\n\n```ts\nimport { fileStore } from \"@openclaw\u002Ffs-safe\u002Fstore\";\n\nconst media = fileStore({\n  rootDir: \"\u002Fsafe\u002Fworkspace\u002Fmedia\",\n  maxBytes: 5 * 1024 * 1024,\n  mode: 0o600,\n});\n\nawait media.write(\"inbound\u002Fphoto.jpg\", bytes);\nawait media.writeJson(\"state\u002Fphoto.json\", { id: \"photo\" });\nconst cached = await media.readJsonIfExists(\"state\u002Fphoto.json\");\nconst opened = await media.open(\"inbound\u002Fphoto.jpg\");\nawait media.pruneExpired({ ttlMs: 10 * 60 * 1000, recursive: true });\n```\n\nThe `store` subpath also includes durable JSON queue helpers for the common\n\"one JSON file per work item\" pattern: atomic entry writes, pending-entry loads,\nacknowledgement via `.delivered` markers, failed-entry moves, and stale temp\ncleanup. Retry, dedupe, and transport semantics stay with the caller.\n\n`tempWorkspace()` exposes `write()`, `writeText()`, `writeJson()`, `copyIn()`, and `read()` for\nsingle-file scratch workflows without hand-rolled path joins, plus a `store: FileStore` view of\nthe workspace dir for the richer cases (`writeStream`, `readJsonIfExists`, `store.json\u003CT>(rel)`).\n\n`tempFile()` is the smaller one-file temp helper. It is intentionally an\nadvanced primitive: use `tempWorkspace()` for the stable temp surface and reach\nfor `tempFile()` only when you need a raw file target.\n\n```ts\nimport { tempFile } from \"@openclaw\u002Ffs-safe\u002Fadvanced\";\n\nawait using target = await tempFile({ prefix: \"download\", fileName: \"payload.bin\" });\nawait fs.promises.writeFile(target.path, bytes);\nconst checksumPath = target.file(\"payload.sha256\");\n```\n\n## Secure absolute file reads\n\nUse `readSecureFile()` when the caller gives you an absolute credential path\ninstead of a root-relative workspace path. It opens the file first, validates the\nsame handle it will read from, checks trusted directories, owner, POSIX mode or\nWindows ACLs, hardlink count, size, and optional timeout, then reads through the\npinned handle.\n\n```ts\nimport { readSecureFile } from \"@openclaw\u002Ffs-safe\u002Fsecure-file\";\n\nconst { buffer } = await readSecureFile({\n  filePath: \"\u002Fvar\u002Flib\u002Fapp\u002Ftoken\",\n  label: \"auth token\",\n  trust: { trustedDirs: [\"\u002Fvar\u002Flib\u002Fapp\"] },\n  io: { maxBytes: 16 * 1024, timeoutMs: 5_000 },\n});\n```\n\nUse `permissions: { allowInsecure: true }` only for migration or explicit local-development\nflows where a warning is preferable to refusing the file.\n\n## Directory walking\n\n`walkDirectory()` and `walkDirectorySync()` replace ad-hoc recursive\n`readdir()` loops with entry and depth budgets, a symlink policy, and stable\nrelative paths.\n\n```ts\nimport { walkDirectory } from \"@openclaw\u002Ffs-safe\u002Fwalk\";\n\nconst scan = await walkDirectory(\"\u002Fsafe\u002Fworkspace\", {\n  maxDepth: 4,\n  maxEntries: 10_000,\n  symlinks: \"skip\",\n  include: (entry) => entry.kind === \"file\",\n});\n\nfor (const file of scan.entries) {\n  console.log(file.relativePath);\n}\n```\n\nCheck `scan.truncated` before treating the result as complete.\n\n## Archive extraction\n\n`extractArchive()` handles ZIP and TAR behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets.\n\n```ts\nimport { extractArchive, resolveArchiveKind } from \"@openclaw\u002Ffs-safe\u002Farchive\";\n\nconst kind = resolveArchiveKind(uploadPath);\nif (!kind) throw new Error(`unsupported archive: ${uploadPath}`);\n\nawait extractArchive({\n  archivePath: uploadPath,\n  destDir: \"\u002Fsafe\u002Fworkspace\u002Fplugin\",\n  kind,\n  timeoutMs: 15_000,\n  limits: {\n    maxArchiveBytes: 256 * 1024 * 1024,\n    maxEntries: 50_000,\n    maxExtractedBytes: 512 * 1024 * 1024,\n    maxEntryBytes: 256 * 1024 * 1024,\n  },\n});\n```\n\nExtraction stages into a private directory and merges through the same safe-open boundary used by direct writes, so a symlinked entry can't trick the merge into following an out-of-tree path.\n\n## Advanced path scopes\n\nFor code that already has a trusted absolute path and wants lower-level boundary\nvalidation without going through `root()`:\n\n```ts\nimport { pathScope } from \"@openclaw\u002Ffs-safe\u002Fadvanced\";\n\nconst uploads = pathScope(\"\u002Fsafe\u002Fuploads\", { label: \"uploads directory\" });\nconst files = await uploads.files([\"photo.jpg\"]);\nconst target = await uploads.writable(\"report.pdf\");\n```\n\n## Errors\n\nEvery failure surfaces as an `FsSafeError` with a closed `code` union you can branch on:\n\n```ts\nimport { FsSafeError } from \"@openclaw\u002Ffs-safe\u002Ferrors\";\n\ntry {\n  await fs.write(\"..\u002Fescape.txt\", \"x\");\n} catch (err) {\n  if (err instanceof FsSafeError && err.code === \"outside-workspace\") {\n    \u002F\u002F handle\n  }\n  throw err;\n}\n```\n\nCodes are grouped by category:\n\n```ts\nif (err instanceof FsSafeError) {\n  if (err.category === \"policy\") {\n    \u002F\u002F Unsafe caller input or filesystem state.\n  } else {\n    \u002F\u002F Operational problem such as helper startup, timeout, or unverifiable permissions.\n  }\n}\n```\n\nCurrent `FsSafeErrorCode` values are `already-exists`, `denied-path`, `device-path`, `hardlink`, `helper-failed`, `helper-unavailable`, `invalid-path`, `insecure-permissions`, `not-empty`, `not-file`, `not-found`, `not-owned`, `not-removable`, `outside-workspace`, `path-alias`, `path-mismatch`, `permission-unverified`, `symlink`, `timeout`, `too-large`, and `unsupported-platform`.\n\n## Safety model\n\n- root-bounded APIs resolve paths against a configured root and reject canonical escapes\n- reads reject known unsafe device paths, open with `O_NOFOLLOW` where available, then verify fd identity matches the path identity before returning the buffer or handle\n- writes use pinned parent-directory helpers and atomic replacement on POSIX, with verified post-write identity\n- `remove`, `mkdir`, `move`, `stat`, `list`, and parent-fd writes use one persistent fd-relative Python helper on POSIX, with Node fallbacks when the helper is disabled or unavailable\n- archive extraction stages into a private directory and merges through the same boundary checks used by direct writes\n\n## Limitations\n\n- Windows uses the safest Node-level behavior available; some fd-relative POSIX hardening is unavailable there.\n- Hardlink rejection depends on platform metadata. Treat it as defense-in-depth, not authorization.\n- `fs-safe` does not validate file contents or archive payload semantics beyond filesystem safety constraints. Schemas, signatures, and authorization belong in the layer above.\n\n## License\n\nMIT.\n","@openclaw\u002Ffs-safe 是一个为 Node.js 应用提供防竞争且根目录受限的文件系统原语的库。其核心功能是通过 `root()` 函数创建一个安全的文件系统根，所有相对路径操作均在此根目录下进行，并防止通过 `..`、符号链接替换、硬链接别名或 TOCTOU 重命名竞争等手段逃逸。该库使用 TypeScript 编写，确保了类型安全。适用于需要处理不受信任的相对路径的应用场景，如用户上传文件、日志记录等，能够有效防止恶意文件访问和数据泄露。",2,"2026-06-11 04:04:45","CREATED_QUERY"]