Prerequisites
- Node.js 20+
- pnpm (recommended,
npmandyarnalso work, but the FUSE binding’s install script needs extra configuration with pnpm).
System FUSE
Install the OS-level FUSE kernel support first (see the support matrix for the full OS overview):- macOS FUSE Setup, macFUSE + kernel extension + Apple Silicon recovery mode steps.
- Linux FUSE Setup,
fuse3install and/etc/fuse.conf.
No Windows support in TypeScript.
@zkochan/fuse-native targets macOS
and Linux only; its legacy Windows path builds against the unmaintained
Dokany-based fuse-shared-library-win32, not WinFsp. Python’s FUSE mounts
run on Windows experimentally, see Windows FUSE Setup.Install the Node Binding
The Node package (@struktoai/mirage-node) ships FUSE support via an optional peer dependency on @zkochan/fuse-native, the pnpm author’s actively-maintained fork that works on Node 20+ and supports both macOS and Linux (thanks to @zkochan).
Allow pnpm to run the install script
@zkochan/fuse-native compiles native code on install. pnpm blocks install scripts by default, allow this one explicitly in pnpm-workspace.yaml:
package.json:
macOS 4+ Symlink Workaround
- macOS
- Linux
macFUSE 4 ships
libfuse.2.dylib instead of the legacy libosxfuse.2.dylib that @zkochan/fuse-native was built against. Create a one-time symlink:Verify
ws.fuseMountpoints prints a /tmp/mirage-fuse-XXXXXX path and no error is thrown, the binding is wired up correctly.
Mounts are async, so await ws.fuseReady() once after constructing the workspace; it resolves when every fuse mount is live (and rejects if one fails to mount). Until then ws.fuseMountpoints may be empty. (In Python, where mounts are synchronous, the constructor blocks until ready instead.)
Per-mount FUSE
FUSE is configured per mount. Each mount whosefuse is set is exposed at
its own mountpoint, showing only that mount’s subtree. The value is either
true (mount at a fresh temp directory) or a path string (mount there,
creating the directory if missing):
fuse per mount: { '/data': new Mount(dataResource, { fuse: '/tmp/data-repo' }), '/s3': new Mount(s3Resource, { fuse: true }) }.
ws.fuseMountpoints returns a { prefix: path } map of the live mountpoints.
Size semantics for API-backed files
Some resources (Linear, Trello, Slack, …) cannot report a file’s size without fetching its content, so over the FUSE mount those files behave like Linux/proc files: they stat as 0 bytes until first open and become
fully readable the moment anything opens them. See
Limitations → Size-unknown API files
for the per-tool table and the mechanics (direct_io + attr_timeout=0).
FUSE on Node has two runtime constraints worth knowing:
fs-monkey can’t patch ESM node:fs imports, and synchronous access to your own mount from the mounting process deadlocks the event loop that serves it. See TypeScript Limitations for details and workarounds.Symlinks and Permissions
Symlinks live in Mirage’s namespace, not in any backend. Links created withln -s in the Mirage shell (or with ln -s inside the mountpoint) appear as
real symlinks over FUSE: ls -l shows lrwxrwxrwx, readlink prints the
target, and reads follow the link. Absolute targets are displayed relative to
the link’s directory so they always resolve inside the mountpoint.
Permission bits shown over FUSE come from Mirage’s metadata (chmod,
chown, touch results), and they are display only: access control is
enforced by Mirage mount modes, not by the kernel. For that reason, never
mount with the default_permissions FUSE option. It would make the kernel
enforce the displayed bits, and a chmod 000 could lock Mirage out of its
own files.