Library / SDK
Schniz/fnm avatar
Schniz/fnm

fnm configures Node versions through shell integration

🚀 Fast and simple Node.js version manager, built in Rust

27,399 stars665 forksRustGPL-3.0

At a glance

What is it?
fnm is a Rust binary for switching Node.js versions. Its `fnm env` command prints shell code for the shell to evaluate, and its installer can add the integration to a profile.
Who is it for?
fnm fits someone switching Node versions across several projects on the same machine, or on more than one operating system, who wants a version manager that behaves the same on each. It does not fit a Windows Command Prompt workflow without writing a startup script, and the removal path documented for the default install is wrong for a custom `--install-dir`.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • Schniz/fnm27,399 ★ · +423
Star history
Repository2026-09-282026-11-13
Schniz/fnm27k27.4k

423 Stars in the last 46 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

fnm env prints code, and nothing happens until you evaluate it

This is the mechanism that explains every fnm support question. `fnm env` prints shell code that the calling shell must evaluate. The installer can also add this integration to a shell profile. The whole activation for Bash is one line:

bash
eval "$(fnm env --use-on-cd --shell bash)"

Zsh takes the same form with `--shell zsh`, Fish uses `fnm env --use-on-cd --shell fish | source` in `~/.config/fish/conf.d/fnm.fish`, and PowerShell pipes through `Out-String | Invoke-Expression` at the end of its profile file.

Because the shell name is a parameter rather than something fnm guesses, the documentation recommends passing it explicitly. The stated reasons are that it is a bit faster and that it avoids process tree detection, which is what happens when fnm has to work out your shell at runtime. That advice is worth following on a machine with a deep or unusual process tree, where inference is both the slower path and the one that can resolve to the wrong answer.

The shell must evaluate the integration before its environment changes take effect; open a new shell or source the configured profile as appropriate.

--use-on-cd is what makes a .node-version file do anything

fnm reads `.node-version` and `.nvmrc` files, but reading them is not the point. The `--use-on-cd` flag on `fnm env` is what makes the shell re-evaluate the directory on every change and switch the active Node version to match.

Leaving that flag out is a likely reason a correctly installed fnm appears inert. Without it, `node --version` inside a project directory returns whatever was selected last, and the file sitting next to your `package.json` is doing nothing. With it, `cd` into the directory and the version changes.

Creating the file is a shell redirect rather than a config step:

bash
$ node --version
v14.18.3
$ node --version > .node-version

Reading `.nvmrc` as well is a compatibility choice with real value during a migration, and it is the reason a team can switch version managers without touching every repository. What the tool does not do is cover everything: the configuration options that extend this behavior live in `docs/configuration.md`, which the documentation points to when it calls automatic version switching highly recommended. Anything beyond the file-based switching is documented there rather than on the main page, and the command reference lives in `docs/commands.md`.

Command Prompt needs a guard because for /F forks cmd

Windows Command Prompt is supported, and the documentation is upfront that it is not entirely covered. The setup is a startup script for cmd.exe or Windows Terminal containing this:

batch
@echo off
:: for /F will launch a new instance of cmd so we create a guard to prevent an infnite loop
if not defined FNM_AUTORUN_GUARD (
    set "FNM_AUTORUN_GUARD=AutorunGuard"
    FOR /f "tokens=*" %%z IN ('fnm env --use-on-cd') DO CALL %%z
)

That comment is the whole explanation. `FOR /F` evaluates its command in a child `cmd`, and if the child inherits the same startup script it evaluates it again, and again. Without the `FNM_AUTORUN_GUARD` variable the startup script recurses until the process table gives up. The misspelling of "infinite" in that comment is in the script itself.

Cmder needs its own variant for the same underlying reason, since it calls the batch file from a startup script of its own. It assumes `CMDER_ROOT` points at your Cmder installation, splits the logic into `%CMDER_ROOT%\bin\fnm_init.cmd`, and calls that from `%CMDER_ROOT%\config\user_profile.cmd` with `call "%CMDER_ROOT%\bin\fnm_init.cmd"`.

Five install routes, and only the script one configures your shell

Install options are Homebrew, WinGet, Scoop, Chocolatey, Cargo, a release binary, and the curl script. Five of them are a single line:

sh
brew install fnm
winget install Schniz.fnm
scoop install fnm
choco install fnm
cargo install fnm

Each of those is followed by the same instruction: set up your shell for fnm. The script route is different, because it appends the loader to your shell config for you, based on the user shell in `$SHELL`.

That asymmetry is the second common support question. Someone who installs through a package manager, sees the command succeed, and opens a new terminal gets no fnm-managed Node version, and reasonably concludes the install failed. It succeeded; the profile line is simply missing.

The script route also has its own cost. It requires `curl` and `unzip` to be present already, and it pipes a remote script into a shell. Three parameters let you adjust it. `--skip-shell` stops it appending anything to your config, which is what you want when upgrading so the loader is not duplicated, or when you intend to add the line yourself. `--install-dir` overrides the location, defaulting to `$XDG_DATA_HOME/fnm`, falling back to `$HOME/.local/share/fnm` on Linux and `$HOME/Library/Application Support/fnm` on macOS. `--force-install` exists only because script-based macOS installs are deprecated in favor of the Homebrew formula.

One inconsistency is worth knowing. Removal is documented as deleting the `.fnm` folder in your home directory. If you passed `--install-dir "./.fnm"`, that instruction does not describe where your files are.

Completions ship inside the binary, not as a separate file

Shell completions are generated by the binary itself rather than installed alongside it:

sh
fnm completions --shell <SHELL>

The supported values are `bash`, `zsh`, `fish` and `powershell`. The documentation says to follow your shell's own instructions to install them, which is the standard caveat rather than a gap, since where a completion script goes is defined by the shell.

The reason this matters is the single-file claim. fnm is described as a single file with easy installation and instant startup, and completions being generated rather than bundled as four separate files is what keeps that true. `Cargo.toml` lists `clap_complete` among the dependencies, which is the library that generates them, so a new subcommand picks up completion support without anyone writing a completion file by hand.

The single binary also explains the install matrix. `Cargo.toml` pulls in `zip` and `tar` with `flate2` and `xz2` for archives, `reqwest` for downloads, `node-semver` for version resolution, `indicatif` for progress output, and `miette` configured with its `fancy` feature for error rendering. A tool that has to download, extract and report on that work needs all of that, which is the tradeoff for having no runtime dependency on the host beyond the shell itself.

GPL-3.0 is the constraint to check before you ship it

The license is GPL-3.0, stated in `Cargo.toml` and repeated in the JavaScript manifest as `GPLv3`. For a developer tool that most people install on their own machine, that is unremarkable. It becomes a factor the moment you redistribute fnm as part of something else, and that is a question for whoever handles licensing in your organization rather than something this documentation answers.

The rest of the manifest is ordinary. The crate is edition 2021, has a `build.rs` build script, and depends on `embed-resource` at build time, which is how the Windows resources get compiled in. Windows-specific dependencies are isolated under a target section as `csv` and `junction`, the latter being what lets a version directory be linked rather than copied.

The JavaScript side exists to run the test suite and the documentation pipeline, not to ship anything to users. The root `package.json` is marked private, pinned to `[email protected]`, and its test script runs Jest with `NODE_OPTIONS='--experimental-vm-modules'`. Version bumps run through changesets via `version:prepare`, and `generate-command-docs` calls a script in `.ci/` to regenerate the command reference. Releases therefore follow a changelog with labels such as New Feature and Bugfix, which is a good sign for anyone tracking when a fix landed.

v1.38.1 to v1.39.0 took seventeen months

The release history is uneven, and it is the number to weigh against the project's reputation for speed. v1.38.0 shipped on 2024-11-14 and v1.38.1 two days later on 2024-11-17. The next release, v1.39.0, was published on 2026-03-06. Between a patch release and a feature release there was a gap of about sixteen months.

The repository itself is not idle. The last push was on 2026-10-26, and the top level shows the machinery of a maintained project: a `CHANGELOG.md`, a `.changeset/` directory, `renovate.json` for dependency updates, `rust-toolchain.toml` pinning the compiler, and a `.node-version` file so the repository's own Node-based tooling agrees with itself.

What that means for you depends on which build you install. `cargo install fnm` and the package manager routes give you the last published release, 1.39.0, and building from `master` gives you whatever came after. For a tool this small, the difference between the two is unlikely to matter. For a tool that manages your Node runtime, whether your version manager can keep pace with a Node release is a better question than whether its own commits are frequent, and the README does not state a Node release compatibility policy either way.

nvm is a shell library, fnm is a binary that emits one

The most asked question about fnm is how it compares to nvm, and the honest answer is that they have different architectures rather than different feature lists.

nvm is a shell function library. You clone a repository and source a script from your profile, and the version manager is source code living in your home directory that you can read and edit. fnm is a compiled Rust binary. It ships as one file, it does not touch your shell, and it emits the code that makes switching work, which you evaluate.

The `.nvmrc` support is the practical bridge between them. A project already carrying an `.nvmrc` works with fnm unchanged, so migrating a team means changing the tool on each developer's machine and not touching any repository. That is a smaller migration than the reverse, where a team standardizing on nvm loses any `.node-version` file.

The trade comes down to where you want the code. nvm invites you to read and edit your version manager; fnm asks you to trust a binary and paste one line. That is a reasonable preference to have either way, and it is worth deciding deliberately rather than by whichever tool the documentation you read first happened to describe.

Editorial conclusion

fnm fits someone switching Node versions across several projects on the same machine, or on more than one operating system, who wants a version manager that behaves the same on each. It does not fit a Windows Command Prompt workflow without writing a startup script, and the removal path documented for the default install is wrong for a custom `--install-dir`. Verify first by running `fnm env --shell bash` in a fresh shell and confirming the output is non-empty before you add anything to a profile, then confirm that changing into a directory with a `.node-version` actually changes `node --version`.

Frequently asked questions

Should I use FNM or NVM?

They differ in architecture. nvm is a shell function library you clone and source from your profile, while fnm is a single compiled binary that prints shell code for you to evaluate. fnm also reads .nvmrc files, so projects already using nvm work with fnm without modification.

Is fnm a fast node manager?

fnm is written in Rust and describes itself as a single file with instant startup, built with speed in mind. Its documentation also suggests passing an explicit shell to fnm env rather than letting it infer your shell at runtime, which it says is faster and avoids process tree detection.

How do I install FNM on Ubuntu?

Use the install script, which requires curl and unzip to be present first: curl -fsSL https://fnm.vercel.app/install | bash. The script appends the loader to your shell config based on $SHELL, so bash, zsh and fish are covered. Pass --skip-shell if you would rather add the eval line to your profile yourself.

what is schniz fnm

fnm is a cross-platform Node.js version manager written in Rust, working on macOS, Windows and Linux. It installs through a script, Homebrew, WinGet, Scoop, Chocolatey, Cargo or a release binary, reads .node-version and .nvmrc files, and ships shell completions for bash, zsh, fish and PowerShell.

Official sources

  1. License: GPL-3.0
  2. Project website
  3. README
  4. Releases
  5. Schniz/fnm on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/schniz-fnm.svg)](https://hysenlabs.com/projects/schniz-fnm)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/schniz-fnm.svg)](https://hysenlabs.com/projects/schniz-fnm)

Was this useful?