Skip to main content

Prerequisites

  • Node.js 20+
  • pnpm (recommended, npm and yarn also 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):
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).
Non-FUSE users don’t need @zkochan/fuse-native, the base @struktoai/mirage-node package works without it. Only install the binding when you want a real mountpoint.

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:
Or, if you’re not using a pnpm workspace, in package.json:
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

If 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 whose fuse 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):
Programmatically, set 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.
macOS allows only one in-process FUSE mount. macFUSE registers a process-global signal source, so a second simultaneous mount in the same process fails. Multiple per-mount FUSE mounts work on Linux; on macOS, enable fuse on a single mount per workspace (or run extra mounts in separate processes).
Symlinks live in Mirage’s namespace, not in any backend. Links created with ln -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.