Production-grade Node-API bindings for the FUSE 3 high-level API on Linux and macOS.
Install the stable package from npm:
npm install fuse-napiThe public API follows semantic versioning. See CHANGELOG.md for release notes and migration details.
This project starts from the exact published source of
@cocalc/fuse-native@2.4.3.
It preserves that package's callback-based JavaScript API while using
FUSE_USE_VERSION=31. The native addon uses Node-API, with no direct V8 API
dependency.
The addon dynamically links an external FUSE 3 library:
- Linux supports both the long-lived
libfuse3.so.3ABI and thelibfuse3.so.4ABI introduced by libfuse 3.18. - macOS uses the
libfuse3runtime installed by macFUSE.
Neither libfuse nor macFUSE is bundled or installed by this package. See UPSTREAM.md for reproducible provenance and COMPATIBILITY.md for callback and mount-option details.
| Platform | Minimum runtime | Architectures | Tested Node.js |
|---|---|---|---|
| Linux | glibc 2.31 and libfuse 3.10.3+ | x86-64, arm64 | 22, 24, 26 |
| Alpine Linux | Alpine 3.23 and libfuse 3.17.3+ (musl) | x86-64, arm64 | 22, 24, 26 |
| macOS | macOS 12 and macFUSE 5.3.1+ with libfuse 3 | Intel x86-64, Apple Silicon arm64 | 22, 24, 26 |
Release prebuilds are compiled on a glibc 2.31 Linux baseline, Alpine 3.23 for
musl, and with MACOSX_DEPLOYMENT_TARGET=12.0. Musl binaries carry an explicit
.musl.node filename tag and are selected automatically in Alpine containers.
The release workflow rejects binaries that raise a supported baseline
accidentally.
Building from source requires a C/C++ toolchain, pkg-config, and the system
libfuse 3 development package. Running a prebuild still requires the system
libfuse 3 runtime.
On Debian or Ubuntu:
sudo apt-get install build-essential fuse3 libfuse3-dev pkg-configOn Alpine Linux:
apk add build-base fuse3 fuse3-dev linux-headers pkgconf python3Install macFUSE
before installing or loading fuse-napi:
brew install --cask macfuseApprove the macFUSE system extension in System Settings when prompted and restart macOS if requested. Apple Silicon systems can additionally require enabling kernel extensions in Startup Security Utility from macOS Recovery. Installation alone is not sufficient until macFUSE is enabled.
No privileged package configuration command is installed or run. Host FUSE installation and system-extension approval remain explicit administrator tasks.
fuse-napi requires macFUSE 5.3.1 or newer and verifies the installed product
version before every mount. It uses macFUSE's libfuse 3 compatibility API and
its default VFS backend. The public custom-loop API returns transport-owned buffers, while
current macFUSE releases expose the matching release function only as a
runtime symbol. fuse-napi resolves and verifies that capability before every
mount, so an incompatible macFUSE update fails safely with EMACFUSEABI.
It does not implement FSKit directly. If the dylib or runtime is unavailable,
installation/loading fails with an actionable macFUSE error.
npm ci
npm testnpm test performs real mount operations and therefore needs /dev/fuse plus
mount privileges on Linux, or an installed and approved macFUSE extension on
macOS. npm run test:unit runs only the non-mounting dependency, errno,
lifecycle, and option tests. npm run test:fuzz deterministically fuzzes the
public validation boundary, and npm run test:soak repeatedly mounts,
exercises, and unmounts the filesystem.
Recovery from a deliberately crashed FUSE daemon has a stronger privilege
contract because deterministic cleanup requires CAP_SYS_ADMIN on hosted
Linux runners. It is isolated from the ordinary suite and can be run with:
sudo env "PATH=$PATH" npm run test:privileged-recoveryThe GitHub CI workflow is started manually with workflow_dispatch; pushes
and pull requests never trigger it. The manually started release workflow can
reuse the same matrix through workflow_call. In addition to the
platform/Node.js matrix, it builds against libfuse 3.18, exercises modern
syscalls, runs static analysis, deterministic fuzzing, ASan/UBSan, and a mount
soak test. The normal Linux matrix exercises the ordinary public API paths as
the unprivileged runner user, then runs the isolated crashed-mount recovery
suite behind an explicit non-interactive root boundary on every supported
OS/Node.js combination. ASan/UBSan covers that privileged recovery path as a
separate command, and the soak gate uses the same deterministic mount
capability boundary. Only the required execution configuration is forwarded
explicitly across each boundary. The libfuse 3.18/SONAME 4 gate exercises its
clean tarball consumer through a dependency-free syscall runner, so
development dependencies cannot mask packaging defects.
The test suite is green on Linux arm64 with libfuse 3.10.3 and on Apple Silicon with macFUSE 5.3.3. Hosted CI is configured to build both macOS architectures but cannot load the macFUSE kernel extension. Real macOS mount integration is therefore a documented manual release check on physical Intel and Apple Silicon hosts. The automated release workflow performs exact-tarball mounts and a readlink smoke test on Linux x64/arm64; maintainers repeat the exact-tarball smoke test manually on both macOS architectures before publication.
Change the package import; existing operation callbacks and mount methods are retained:
constFuse=require('fuse-napi')Error constants are negated host errno values. Linux values remain unchanged; macOS receives the corresponding Darwin values. On macOS, filesystems that want NFC and NFD names to compare equal must normalize lookup keys themselves.
In order to create a FUSE mountpoint, you first need to create a Fuse object that wraps a set of implemented FUSE syscall handlers:
constfuse=newFuse(mnt,handlers,opts={})Create a new Fuse object.
mnt is the string path of your desired mountpoint.
handlers is an object mapping syscall names to implementations. The complete list of available syscalls is described below. As an example, if you wanted to implement a filesystem that only supports getattr, your handle object would look like:
{getattr: function(path,cb){if(path==='/'){cb(0,stat({mode: 'dir',size: 4096}));return;}if(path==='/test'){cb(0,stat({mode: 'file',size: 11}));return;}cb(Fuse.ENOENT);}}opts can include:
{debug: false,// Enable detailed tracing of operations.force: false,// Recover a disconnected mount before remounting.mkdir: false,// Create the mountpoint before mounting.directIo: false,// Set fuse_config.direct_io for every opened file.maxRead: 262144,// Set max_read and the matching init connection limit.timeout: 15000,// Operation and mount-start timeout in milliseconds.maxConcurrency: 4,// Fixed native request-worker count (1 through 64).nullPathOk: false,// Accept null paths for unlinked handle operations.noPath: false,// Avoid path reconstruction for handle operations.onError: (error,operation,args)=>{// Report exceptions thrown by an operation implementation.}}force only detaches a disconnected FUSE mount; it does not replace a healthy
mounted filesystem. Every unmount attempt is followed by a bounded stability
check, so the replacement mount does not race a lazy or concurrent detach. A
non-zero helper result is treated as an idempotent success only when repeated
observations prove that the mount is already detached. If the helper fails and
the mount remains attached, mounting fails with EFUSEUNMOUNT while retaining
both the helper and observation errors. A successful helper whose detach is
still not stably observable after 15 seconds fails with EFUSEUNMOUNTWAIT.
Unmount helpers inherit ordinary process settings such as PATH, but dynamic
loader injection variables (LD_* and DYLD_*) are removed before executing
the system helper. This keeps sanitizer, instrumentation, and application
preloads from changing the privileged unmount boundary. Helper standard output
and error output are retained on helperError when unmounting fails.
On macOS, volume presentation can additionally be configured with:
{displayFolder: true,name: 'Folder Name'}timeout can also be an object such as
{ default: 15000, read: 30000, init: 5000 }. Set an individual value or
default to false to disable that timeout deliberately.
Each operation callback is accepted only once. A timeout, synchronous exception, or rejected promise is translated to a FUSE error and cannot leave the native worker blocked. Return values, buffer lengths, directory entries, extended attributes, statistics, and mount options are validated before they cross the native boundary. Unknown option and operation names are rejected so configuration mistakes cannot silently change filesystem behavior.
Fuse.validateOptions(options) performs the same synchronous validation
without creating a filesystem instance. Invalid combinations throw a
TypeError with a stable code and an options array. The FUSE 3 preflight
rejects removed or internal options (nonempty, fd, and user_id),
platform-specific misuse, unsafe module identifiers, and conflicting cache,
access, block-device, or inode-retention policies. For example:
Fuse.validateOptions({directIo: true,nonempty: true})// TypeError with code ERR_FUSE_OPTION_REMOVEDRemove nonempty; use directIo as shown when direct I/O is required. See
the 2.0.0 section in CHANGELOG.md when upgrading from the
1.x line.
For mount options with a native snake-case spelling, both the JavaScript name
and the historical libfuse name are accepted when the underlying FUSE 3
concept still exists (for example, directIo/direct_io and
allowOther/allow_other). Inputs are normalized to the JavaScript name, and
conflicting aliases are rejected.
await Fuse.checkEnvironment(options) performs the production runtime
preflight without mounting. It verifies the loaded libfuse version and
capabilities and, on Linux, fusermount3, read/write access to /dev/fuse,
and user_allow_other when allowOther or allowRoot is requested. Mounting
performs this check automatically. Failures have stable codes such as
EFUSEHELPER, EFUSEDEVICE, EFUSEALLOWOTHER, EFUSEVERSION, and
EMACFUSEVERSION, and EMACFUSEABI. On macOS the preflight reads the installed
macFUSE product version and rejects mounts below 5.3.1.
The native request loop uses exactly maxConcurrency workers instead of
libfuse's dynamically growing multithreaded loop. This bounds native
threads, outstanding JavaScript callbacks, and memory use under load. Mount
startup runs outside the JavaScript event loop and remains subject to the
configured init deadline.
Every distinct path resolved by the kernel creates a reachable high-level
libfuse node until the kernel sends FORGET. release closes a file handle;
it does not evict that namespace entry. Filesystems that expose a continuous
stream of one-shot names can ask the kernel to drop an inactive entry without
deleting the underlying file:
fuse.invalidateEntry('/archive/file-123',err=>{if(err)console.error(err)})Call invalidateEntry only after the client-side close() has returned, or
from an independent housekeeping timer. Notifications run serially on a
bounded, dedicated native worker, so a kernel reverse-invalidation lock cannot
block the JavaScript event loop or exhaust libuv's shared worker pool. Never
call it from getattr, open, release, or another filesystem operation callback;
the method rejects operation-local calls with EDEADLK because Linux reverse
invalidation can otherwise deadlock with the originating request. The method
accepts nested paths, resolves the parent node, and sends portable
FUSE_NOTIFY_INVAL_ENTRY. macOS mounts require macFUSE 5.3.1+, whose VFS
backend actually evicts the notified name-cache entry. The method returns
ENOSYS if the connected kernel does not support the notification. At most
1,024 notifications wait per mount; a larger concurrent burst returns EAGAIN
instead of growing native memory without bound. A later access performs a fresh
lookup, so choose a retention window that balances memory against metadata traffic.
For a larger usage example, see CoCalc's WebSocketFS FUSE integration.
The portable callback surface from 1.x remains supported on FUSE 3.
Deprecated getdir and utime are represented by readdir and utimens.
FUSE 3's merged getattr/fgetattr and truncate/ftruncate operations are
routed to the existing handlers based on whether a file handle is present. In
general the callback for each op should be called with
cb(returnCode, [value]), where the return code is a number (0 for OK and
< 0 for errors). See below for a list of POSIX error codes.
File handles, file positions, sizes, inode counters, and other 64-bit values
are passed as a number while exactly representable and as a bigint
otherwise. Implementations must preserve a bigint file handle and return it
unchanged to their own storage layer.
During an operation, fuse.context() returns a frozen snapshot containing
uid, gid, pid, umask, and the portable fuse_file_info fields. The
context is isolated across promises and other asynchronous work with
AsyncLocalStorage; outside an operation it returns null. When
nullPathOk or noPath is enabled, the affected handle-based callbacks must
accept null as their path. On Linux, pid identifies the calling thread and
can differ from process.pid when Node.js performs filesystem I/O in its
libuv worker pool.
TypeScript: see index.d.ts.
Called on filesystem init.
Enhanced, mutually exclusive alternative to init. connection is a frozen
snapshot of the portable FUSE 3 connection fields:
protoMajor, protoMinor, asyncRead, maxWrite, maxReadahead,
maxRead, capable, want, maxBackground, and congestionThreshold.
The callback may return a conservative configuration containing maxWrite,
maxReadahead, maxBackground, congestionThreshold, want, and/or
asyncRead.
Requested limits may not exceed the values supplied by the kernel, want
must be a subset of capable, and the congestion threshold may not exceed
the background-request limit. Omitting the configuration preserves all
libfuse defaults.
Set maxRead through the constructor options. When present, fuse-napi passes
the same validated unsigned 32-bit value as both max_read=<n> and
connection.maxRead; this is required by libfuse 3. It is intentionally not
an initWithConfig result field because changing it after the mount options
have been processed would make the two libfuse values inconsistent. When the
option is omitted, fuse-napi leaves conn->max_read to libfuse and kernel
negotiation. The kernel may enforce a lower effective request size.
Called before the filesystem accessed a file
Called when the filesystem is being stat'ed. Accepts a fs stat object after the return code in the callback.
ops.statfs=function(path,cb){cb(0,{bsize: 1000000,frsize: 1000000,blocks: 1000000,bfree: 1000000,bavail: 1000000,files: 1000000,ffree: 1000000,favail: 1000000,fsid: 1000000,flag: 1000000,namemax: 1000000})}Called when a path is being stat'ed. Accepts a stat object (similar to the one returned in fs.stat(path, cb)) after the return code in the callback.
ops.getattr=function(path,cb){cb(0,{mtime: newDate(),atime: newDate(),ctime: newDate(),size: 100,mode: 16877,uid: process.getuid(),gid: process.getgid()})}Same as above but is called when someone stats a file descriptor
Called when a file descriptor is being flushed
Called when a file descriptor is being fsync'ed.
Same as above but on a directory
Called when a directory is being listed. Accepts an array of file/directory names after the return code in the callback
ops.readdir=function(path,cb){cb(0,['file-1.txt','dir'])}Enhanced, mutually exclusive alternative to readdir for large or remote
directories. fd is the handle returned by opendir; offset is an opaque
signed 64-bit resume value. Return
cb(0, names, stats, nextOffsets), where every name has a corresponding
non-zero next offset. The kernel can resume at the last accepted offset when
its output buffer is full.
Called when a path is being truncated to a specific size
Same as above but on a file descriptor
Called when a symlink is being resolved. Accepts a pathname (that the link should resolve to) after the return code in the callback
ops.readlink=function(path,cb){cb(null,'file.txt')// make link point to file.txt}Targets longer than the kernel-provided buffer are truncated and NUL-terminated as required by the FUSE high-level callback contract.
Called when ownership of a path is being changed
Called when the mode of a path is being changed. Always called with mode a number (not a string).
Called when a new device file is being made.
Called when extended attributes is being set (see the extended docs for your platform).
value is a request-owned copy and can safely be consumed asynchronously.
Copy it only when your storage layer requires independent mutability or
lifetime management.
The position argument is mostly a legacy argument only used on MacOS but see the getxattr docs on Mac for more on that (you probably don't need to use that).
Called when extended attributes is being read.
Return the extended attribute as the second argument to the callback (needs to be a buffer).
If no attribute is stored return null as the second argument.
The position argument is mostly a legacy argument only used on MacOS but see the getxattr docs on Mac for more on that (you probably don't need to use that).
Called when extended attributes of a path are being listed.
Return a list of strings of the names of the attributes you have stored as the second argument to the callback.
Called when an extended attribute is being removed.
Called when a path is being opened. flags in a number containing the permissions being requested. Accepts a file descriptor after the return code in the callback.
vartoFlag=function(flags){flags=flags&3if(flags===0)return'r'if(flags===1)return'w'return'r+'}ops.open=function(path,flags,cb){varflag=toFlag(flags)// convert flags to a node style string...cb(0,42)// 42 is a file descriptor}The callback may alternatively return
{ fd, directIO, keepCache, nonseekable }. This sets the corresponding
portable fuse_file_info result bits while retaining primitive file-handle
results for compatibility. opendir and both create variants accept the
same result object.
Same as above but for directories
Called when contents of a file is being read. You should write the result of the read to the buffer and return the number of bytes written as the first argument in the callback.
If no bytes were written (read is complete) return 0 in the callback.
The buffer is owned by the JavaScript request. It remains valid if an operation times out or teardown starts; completed bytes are copied back to the kernel only after the callback result has been validated.
vardata=Buffer.from('hello world')ops.read=function(path,fd,buffer,length,position,cb){if(position>=data.length)returncb(0)// donevarpart=data.slice(position,position+length)part.copy(buffer)// write the result of the read to the result buffercb(part.length)// return the number of bytes read}Called when a file is being written to. You can get the data being written in buffer and you should return the number of bytes written in the callback as the first argument.
The write buffer is a request-owned copy, so asynchronous consumers never reference kernel request memory after a timeout.
ops.write=function(path,fd,buffer,length,position,cb){console.log('writing',buffer.slice(0,length))cb(length)// we handled all the data}Called when a file descriptor is being released. Happens when a read/write is done etc.
Same as above but for directories
Called when a new file is being opened.
Enhanced, mutually exclusive alternative to create. It also receives the
original FUSE/POSIX open flags. Its callback accepts either a primitive file
handle or the file-info result object described under open.
Called when the atime/mtime of a file is being changed. atime and mtime
are signed integer milliseconds since the Unix epoch and can be bigint
outside JavaScript's safe-integer range.
Mutually exclusive, lossless alternative to utimens. Both times are frozen
{ seconds, nanoseconds } values. Nanoseconds are preserved exactly and may
be Fuse.UTIME_NOW or Fuse.UTIME_OMIT; the native
flag_utime_omit_ok bit is enabled only for this variant. Stat timestamps may
also be returned in this timespec form.
Handle-aware alternative to both utimens variants. It receives a nullable
path, the open file handle, and the same lossless timespec values as
utimensWithTimespec. Use this variant when nullPathOk is enabled.
Handle-aware alternative to chown. The path can be null when
nullPathOk is enabled.
Handle-aware alternative to chmod. The path can be null when
nullPathOk is enabled.
Called exactly once when an initialized filesystem exits through an orderly libfuse teardown. Teardown waits for this callback, subject to the configured operation deadline. JavaScript cannot be called once the Node-API environment itself is already shutting down.
Handles portable POSIX record locks. lock is a frozen object with type,
whence, signed start, signed length, and pid. For F_GETLK, return an
updated lock as the second callback argument.
Handles BSD flock operations for filesystems that need remote locking.
Without this callback the kernel can still provide local locking.
Maps a file block to a device block. Return the mapped 64-bit index as the second callback argument. This is meaningful for block-device-backed mounts.
Handles bounded, well-formed FUSE ioctls. argument and file handles remain
lossless, and data is request-owned. Return a same-length output Buffer.
Unrestricted retry/iovec ioctls are rejected with EOPNOTSUPP because their
borrowed-pointer protocol cannot be represented safely by this callback API.
Payloads larger than 1 MiB are rejected before entering JavaScript.
Returns a snapshot readiness event mask.
Mutually exclusive, notification-capable alternative to poll. Return the
initial readiness mask through cb. When handle is non-null, call
handle.notify() after readiness changes so the kernel re-evaluates the
poll, then call handle.close() when no further notification is needed.
Handles are idempotent and are closed automatically during teardown.
Mutually exclusive alternative to write implementing FUSE write_buf.
Generic memory/file-descriptor vectors are flattened into a request-owned
Buffer before JavaScript is called. Return the validated byte count.
Mutually exclusive alternative to read implementing FUSE read_buf.
Return cb(0, buffer) with at most length bytes; native storage is allocated
with the ownership rules required by libfuse.
Allocates a signed 64-bit byte range for an open file.
Implements FUSE 3 copy_file_range. Return the copied byte count directly to
cb; negative values are treated as errno results. Paths can be null for
handle-based requests, and all offsets and handles retain 64-bit precision.
Implements FUSE 3 lseek, including SEEK_DATA and SEEK_HOLE. Return
cb(0, resultingOffset).
Called when a file is being unlinked.
Called for an unflagged rename. A flagged request is rejected with
EOPNOTSUPP rather than silently losing its semantics.
Mutually exclusive alternative to rename that receives the native FUSE 3
rename flags, including Linux RENAME_NOREPLACE.
Called when a new link is created.
Called when a new symlink is created
Called when a new directory is being created
Called when a directory is being removed
MIT for these bindings.
The bindings retain their upstream MIT license and attribution. The external libfuse and macFUSE installations retain their own licenses.
Security issues should be reported privately according to SECURITY.md. Maintainer release procedures are documented in RELEASING.md. Contributions are accepted through pull requests under the maintainer-controlled process in CONTRIBUTING.md.