
# Mounting

**`mount()` from `mountx/auto` is the one you want unless you have a reason not to.**

It works out what this host can mount with, uses that transport, and hands back _that transport's own mount object_ with a `transport` tag defined on it — tagged, not wrapped. So `await using`, `unmount()` and every transport-specific member work exactly as they do when you import the transport directly.

```ts
import { mount } from "mountx/auto";
import { createMemoryDriver } from "mountx/drivers/memory";

await using mounted = await mount(createMemoryDriver(), "/mnt/point");

mounted.mountpoint; // "/mnt/point"
mounted.transport; // "fuse" | "nfs"
mounted.active; // false from the moment teardown starts
```

`mount()` resolves once the mountpoint is _usable_ — not merely once the process has started.

## Asking first

```ts
import { probeTransports } from "mountx/auto";

const probe = await probeTransports();
probe.chosen; // "fuse" | "nfs" | undefined
probe.preference; // the order it chose from, for this platform
probe.fuse.usable; // and .reason when it is not
probe.reason; // when nothing can mount: what each transport is missing
```

The probe is cheap enough to call before you decide whether to offer a mount at all, and specific enough to print: when nothing works it names what _each_ transport is missing rather than reporting the last failure. It also loads nothing heavy — it reaches only the NFS probe and the `fusermount3` check, neither of which pulls in a protocol codec.

## Two deliberate limits

- **No probe when you name a transport.** `{ transport: "nfs" }` skips it entirely, so the error you get is that transport's own — which is more specific than anything the chooser could say.
- **No fallback after a failure.** The probe decides once, from host facts. If the chosen transport then fails to mount, that error is what you get. Silently mounting the _other_ one would hand you a filesystem with different semantics than the error you never saw.

## The mount object

Shared by both transports:

| member                  |                                                                |
| ----------------------- | -------------------------------------------------------------- |
| `mountpoint`            | absolute path of the mountpoint                                |
| `source`                | what the mount table shows as the device                       |
| `active`                | `false` from the moment teardown starts, whoever started it    |
| `unmount()`             | idempotent, concurrency-safe, retryable after a failure        |
| `transport`             | `"fuse"` or `"nfs"` — the discriminant, added by `mountx/auto` |
| `[Symbol.asyncDispose]` | so `await using` works                                         |

Narrowing on `transport` reaches everything that transport has:

```ts
if (mounted.transport === "fuse") {
  mounted.session; // the FuseSession — its stats, inodes and handles
  mounted.fd; // the /dev/fuse descriptor
  mounted.closed; // resolves when the loop has ended; never rejects
  mounted.notifyInvalInode(2n); // drop the kernel's cache for one inode
  mounted.notifyInvalEntry(1n, "hello.txt"); // drop one name → inode mapping
} else {
  mounted.server; // the NfsServer behind it
  mounted.port; // the port both programs are on
}
```

## Unmounting

Three ways, and they all end in the same place:

```ts
// 1. explicit
await mounted.unmount();

// 2. scope-bound
{
  await using mounted = await mount(driver, "/mnt/point");
} // unmounted here

// 3. signals — SIGINT/SIGTERM, on by default
```

`unmount()` is idempotent and concurrency-safe: a second call is a no-op beyond awaiting the first. It always settles within `unmountTimeout` (default 10 s), and it throws if `umount(8)` refused — a busy mountpoint, essentially — or if the deadline passed and the connection had to be forced down. Both messages say how to recover, and a failed unmount can be retried.

The signal handlers are installed with the first mount and removed with the last. If nothing else in the process listens for the signal, the default action is **re-raised** once every mount is down, so the exit status stays honest. Turn them off with `signals: false`.

::caution
**Never call `process.exit()` with a mount up.** Node's exit path joins the libuv threadpool, and a live mount always has reads parked there — the process hangs. `await mounted.unmount()` and set `process.exitCode` instead. This is also why the signal handlers re-raise rather than exiting directly.
::

### Everything at once

```ts
import { liveMounts, unmountAll } from "mountx/auto";

(await liveMounts()).length; // every live mount in this process, tagged
const failures = await unmountAll(); // never rejects; returns what went wrong
```

Only transports you actually used are asked, so neither loads anything that was not already loaded.

## What teardown actually does

Worth knowing, because the failure modes are visible:

- **The mount table is the truth, not an exit status.** `umount(8)` returning 0 is not the same as the mount being gone, so mountx reads the table — and treats "could not read it" as **still mounted**, never as gone. Forcing down a mount that turns out to be absent is harmless; declaring success on a guess shuts the server down under a live mount.
- **The deadline escalates.** On expiry it goes to `umount -f`, and on Linux then `-l`. Every spawned `umount` is bounded by that deadline and abandoned if it outlives it — a `umount(8)` parked in the kernel does not die on `SIGKILL`.
- **Unprivileged teardown is weaker, and says so.** Both routes to force a FUSE connection down belong to root, so an unprivileged process can only ask `fusermount3 -u -z` and let the connection die with the superblock.
- **On macOS the escalation can be refused.** Network volumes sit behind a sandbox approval that is never _prompted_ for a command-line process. mountx names that case rather than blaming your driver, and tells you the mount survived. See [NFSv3 § macOS consent](/transports/nfs#the-macos-consent-gate).

## No mount stacking

Mounting over a live mountpoint is refused, in both directions: this process's own mounts, and any FUSE mount already in `/proc/self/mounts`. There is no option to override it.

## Options

Options that mean the same thing on both transports sit at the top level; transport-specific ones go in `fuse: {…}` or `nfs: {…}` and are applied _after_ the shared ones, so they win.

```ts
await mount(driver, "/mnt/point", {
  // shared
  readOnly: true,
  signals: true,
  unmountTimeout: 10_000,
  useDriverIno: true,
  onError: (error) => {},
  onTransportError: (error) => {},

  fuse: { attrTimeout: 10, readers: 2 },
  nfs: { exportPath: "/", nobrowse: true },
});
```

A block for the transport you did not get is simply ignored, which is what makes one call portable. See [Tuning](/guide/tuning) for what is worth setting, and the [reference](/transports/auto) for the full list.

::note
Importing `mountx/fuse` or `mountx/nfs` directly takes the same transport options at the **top level** rather than under `fuse: {…}`/`nfs: {…}` — `mountx/auto` is what nests them, because it is the thing that has to hold both.
::

## Next

- [Tuning](/guide/tuning) — cache timeouts, concurrency, and the rest of the knobs.
- [Troubleshooting](/guide/troubleshooting) — when it wedges, hangs, or leaves a stale path.
- [Transports](/transports) — the two in detail, and how to pin one.
