Skip to content

Instantly share code, notes, and snippets.

@pjeby
Created January 19, 2026 20:17
Show Gist options
  • Select an option

  • Save pjeby/3dd65992fae3d2973fc34a6fb7f82e4e to your computer and use it in GitHub Desktop.

Select an option

Save pjeby/3dd65992fae3d2973fc34a6fb7f82e4e to your computer and use it in GitHub Desktop.
Fix for fnm (Fast Node Manager) issues with repath/hash -r and symlink accumulation (bash only, sorry)
#!/usr/bin/env bash
#
# This script fixes some outstanding issues with FNM (fast node manager's)
# "multishell" symlink system. Specifically:
#
# 1) that FNM generates a symlink for every shell it's ever run in, and *never
# cleans them up*, eventually leading to inode exhaustion.
#
# 2) that many shells (especially bash) do not change their cached PATH lookups
# when a symlink on PATH is changed, rather than PATH itself being changed.
#
# So, this script fixes these issues (at least for bash) by
#
# 1) wrapping the `fnm` command to detect when it has been run, and update the
# PATH if needed
#
# 2) reading the FNM-generated symlink to get the node version and update the
# PATH with it
#
# 3) deleting the symlink after it's read
#
# In this manner, it prevents symlink accumulation because the symlink never
# exists for more than a fraction of a second after fnm creates it. And the
# rehashing problem doesn't happen because the PATH is changed, which allows the
# shell to notice its cache is out of date.
#
# To use the script, add a line like this to your .bashrc:
#
# source /wherever/you/put/this/script
#
# right after your `eval "$(fnm env)"` or equivalent, then restart your shell.
# (You can also just source the script in your current shell instead of
# restarting.)
#
# NOTE: Because this workaround deletes the fnm-generated symlink as soon as
# it's created, the `fnm current` command will NOT work properly in
# non-interactive shells (i.e., in scripts), unless you source this file there
# too. (We recommend using `node --version` or similar instead of using `fnm
# current` in your scripts anyway.)
#
# Also, because the symlink isn't used, it's impossible for a script to change
# the active node version of a parent shell or script. (This is actually a
# *feature*, for the most part, but if you currently have scripts that rely on
# being able to change a parent shell's node version, they won't work if you're
# using this.)
#
#EOF
if [[ $0 = $BASH_SOURCE ]]; then
# Script must be sourced, not executed; output docs to stderr
sed -n -e '2, /^#EOF/ { /^#EOF/d; s/^# \?//; p }' "$0" >&2
exit 64
fi
# Wrap fnm as a function that does repathing after successful `fnm use`
# and supports `fnm current`
fnm() {
case "${1-}" in
current)
# We replace the built-in `fnm current` because it relies on the symlink
# we're deleting
fnm_repath
case "$PATH" in
*"$FNM_BASE/node-versions/"*)
: "${PATH#*"$FNM_BASE/node-versions/"}"; echo "${_%%/*}"
;;
*"$FNM_BASE/aliases/"*)
local alias="${PATH/*"$FNM_BASE/aliases/"/"$FNM_BASE/aliases/"}"
alias=${alias%%:*}
linkpath="$(readlink "$alias")"
if [[ $linkpath ]]; then
: "${linkpath#*"$FNM_BASE/node-versions/"}"; echo "${_%%/*}"
else
echo "none"
fi
;;
*) echo "none"
;;
esac
;;
*)
PATH="$PATH:$FNM_MULTISHELL_PATH" command fnm "$@" && fnm_repath
;;
esac
}
# If FNM_MULTISHELL_PATH exists, read where it points to and put that on the
# PATH in the first spot where a previous entry pointing into FNM_BASE is found,
# or at the head of the PATH if no such entry exists. This lets you put things
# in front of FNM on the PATH and keep them there even when swapping versions.
fnm_repath() {
# Don't repath if symlink already removed
[[ ! -d "$FNM_MULTISHELL_PATH" ]] || {
# Get the new path
local linkpath="$(readlink "$FNM_MULTISHELL_PATH")"
[[ ! "$linkpath" ]] || {
# Got a path; move to head of PATH and remove the symlink
local f p="" parts
IFS=: read -ra parts <<<"$PATH" # split on ':'
# Remove any path pointing into the FNM directory, replacing it
# with the link path if found
for f in "${parts[@]}"; do
if [[ "$f" = "$FNM_BASE"/* ]]; then
# Replace old path w/new if this is the first one found
[[ ! $linkpath ]] || { p+="$linkpath:"; linkpath=; }
else
p+="$f:"
fi
done
# Put the link path at the start of PATH if there was no old path
# to replace
PATH="${linkpath:+"$linkpath":}${p%:}"
# remove the symlink so there's no symlink buildup
rm "$FNM_MULTISHELL_PATH"
}
}
}
# Remove the multishell path and set up the base
case ${OSTYPE-} in
cygwin|msys)
# Remove the multishell path from PATH
PATH=${PATH#"$(cygpath "$FNM_MULTISHELL_PATH")":}
# Prefix for PATH matching/updating
FNM_BASE="$(cygpath "$FNM_DIR")"
;;
*)
# Remove the multishell path from PATH
PATH=${PATH#"$FNM_MULTISHELL_PATH":}
# Prefix for PATH matching/updating
FNM_BASE="$FNM_DIR"
;;
esac
# Now initialize the PATH and get rid of the symlink
fnm_repath
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment