How bin linking works in node.js npm and yarn and monorepos

npm Jul 15, 2025

yarn <thing> pnpm run thing npm run thing are types of run commands we run all the time when working in node.js. But, how does that actually all work?

Let's take a quick look at it!

Let's start with PATH

In all the various operating systems, you can set your PATH environment variable.

The PATH tells your OS all the places it can look for executables, so you can just run things like node git yarn etc without having to specify the binaries full path on disk. Like c:\program files\some\ridiculously\long\path\git.exe

FeaturemacOS (zsh/bash)Linux (bash/zsh)Windows (10/11)
Temporary Changeexport PATH=$PATH:/your/path (in Terminal)export PATH=$PATH:/your/path (in Terminal)set PATH=%PATH%;C:\your\path (in Command Prompt)
Permanent (User)Add export PATH=... to ~/.zshrc or ~/.bash_profileAdd export PATH=... to ~/.bashrc or ~/.profileUse GUI: System Properties → Environment Variables → Edit Path under User section
Permanent (System)Add to /etc/paths or /etc/paths.d/ (requires sudo)Add to /etc/environment or /etc/profile (requires sudo)Use GUI: Edit Path under System variables (admin rights needed)
Apply Changessource ~/.zshrc or restart Terminalsource ~/.bashrc or restart TerminalRestart Command Prompt or reboot
Separator: (colon): (colon); (semicolon)
View Current PATHecho $PATHecho $PATHecho %PATH% (in CMD) or $env:Path (in PowerShell)

Each OS has a default list of places it looks for binaries. Ubuntu for example has these defaults.

/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin

On all OS's you can add custom folders to add binary locations to your PATH as well.

On Windows node adds itself to the PATH during install, which you can see via the settings GUI.

That way you can just run node in any cmd or powershell.

Node version managers such as NVM or NVM For Windows also add path variables to be able to find whichever version of node you've currently chosen with nvm use vXX.YY.

In Linux / macOS nvm will add an entry to your Bash or ZSH profile which runs the NVM startup script.

Which does all kinds of calculations, but ultimately ends up updating the PATH to point to where whatever version of node you're using is located. Usually somewhere in the ~/.nvm folder.

Let's talk about node_modules and npm

When you install a package with npm install -g or yarn install --global, you're installing those packages right next to wherever your current version of node exists in it's node_modules directory.

See the screenshot above and notice it lives right next to where NVM for Windows has set the node version.

Inside that directory you'll find all the packages that are installed globally.

And all of the "bins" live right next to node as well..

Those bins more often than not are just shims to call into each node_modules like ...
node node_modules/yarn/bin/yarn.js

Local Packages

Now, for locally installed packages, it's a bit different...

There are .bin folders are created during npm install and which are symlinks up and over to the packages which list "bin" in their package.json files.

So, for example, the typescript package has a couple of "bin" entries in it.

When npm install or yarn pnmp i runs, it will look for all those bins, and link them to their corresponding scripts, and on windows it does all kinds of extra files to make sure they're all callable in powershell, cmd, etc.

When using npm run or yarn run or even just yarn <bin-name> you're also able to create your own entries in the "scripts" and reference the binary name alone like...

{
  "scripts": {
    "build": "tsc -p tsconfig.web.json"
  }
}

Then you can run npm run build, and it will automatically add all the bins to the path, and allow you to run tsc

./node_modules/.bin
../node_modules/.bin
../../node_modules/.bin
... (up the directory tree)

Similarly, you can invoke the binary directly with npm exec, since again npm on the fly modifies the PATH variable, and adds the node_modules/.bin to it.

Monorepos

In monorepos using hoisted mode, if you want to define your own "bin" scripts, you can do so by following the same pattern.

{
  "name": "@foo/my-pkg",
  "bin": {
    "foo-cli": "bin/foo.js"
  }
}

You then get symlinks to all the various "bin" entries created in the root node_modules/.bin so you can just call yarn foo-cli from the root of your monorepo.

For example...

Having the ./bin/index.js will add an entry in the node_modules/.bin which points to the ./packages/midgard-yarn-strict/bin/index.js

So users of the monorepo can simply run npm exec midgard-yarn-strict or yarn midgard-yarn-strict.

All package managers have some form of this behavior including pnpm...

pnpm run | pnpm
Aliases: run-script
pnpm exec | pnpm
Execute a shell command in scope of a project.

Tags