Bun Starts a Package Binary but the Command Is Not Found in `node_modules/.bin`

bin, bun, cli, package-json, packaging

bun run <package-binary> fails because node_modules/.bin/<name> does not exist, and Bun reports command not found: <name>.

What this error means

A package can ship executable entry points through its package.json bin field. Package managers create a launcher in node_modules/.bin that points to the real file inside the package directory.

When Bun installs a package that advertises a binary, it only exposes that command if the package metadata is valid and the bin target resolves to an actual file. If the bin path is missing, malformed, or points outside the package in a way Bun does not package, there is no launcher to place in node_modules/.bin. Then a call such as:

sh
bunx mycli

or

sh
bun run mycli

fails with a command not found error because the executable name was never linked into node_modules/.bin.

How Bun exposes package binaries

Bun reads the installed package’s package.json. If it finds a bin entry, it uses that to generate a runnable command name.

The bin field supports two forms:

A minimal single-binary package looks like this:

json
{ "name": "mycli", "version": "1.0.0", "bin": "dist/cli.js" }

That is shorthand for:

json
{ "name": "mycli", "version": "1.0.0", "bin": { "mycli": "dist/cli.js" } }

The left side is the command name. The right side is the file Bun should execute. After install, Bun creates a launcher under node_modules/.bin/mycli.

The wrapper is not the actual implementation. It is a small executable shim that resolves the package path and starts the declared file using the package manager’s execution rules. The shim lets you run the command without typing the full path into node_modules/mycli/dist/cli.js.

Why a missing or wrong bin path breaks discovery

Bun can only expose what is described in the package metadata. If the bin value points to a file that is not present in the published package, the install can complete, but there is nothing valid to link.

Common causes include:

For example, this package metadata is broken if the package is published without src/cli.ts:

json
{ "name": "mycli", "version": "1.0.0", "bin": { "mycli": "src/cli.ts" } }

Bun can read the declaration, but it still has to resolve the file in the installed package. If the file is absent, the executable cannot be linked into node_modules/.bin.

How node_modules/.bin is populated

The node_modules/.bin directory is a convention used by package managers for executable shims. During installation, each package binary is linked there by name.

With Bun, the typical result for a valid CLI package is:

text
node_modules/ .bin/ mycli mycli/ package.json dist/ cli.js

The .bin/mycli entry is what bun run mycli, bunx mycli, and other tooling rely on.

If the package declares multiple binaries, Bun creates one wrapper per entry:

json
{ "name": "toolkit", "version": "1.0.0", "bin": { "toolkit": "dist/toolkit.js", "toolkit-doctor": "dist/doctor.js" } }

This produces node_modules/.bin/toolkit and node_modules/.bin/toolkit-doctor.

If the consuming project expects toolkit but the package only publishes toolkit-doctor, the command lookup will fail even though the package installed successfully.

The exact package.json structure Bun expects

A package binary is discoverable when all of the following are true:

A reliable package layout for a Bun-compatible CLI looks like this:

json
{ "name": "mycli", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "bin": { "mycli": "./dist/cli.js" }, "files": [ "dist" ] }

And the CLI entry file should include a shebang if it is intended to run as a standalone executable:

ts
#!/usr/bin/env node import { main } from "./index.js"; await main();

For CommonJS, the same idea applies:

js
#!/usr/bin/env node const { main } = require("./index.js"); main();

The shebang matters because the wrapper needs an executable target. Bun can run JavaScript directly, but packaging conventions still depend on the binary target being a real launcher file.

Packaging rules that determine whether the binary ships

Publishing a package is not the same thing as checking out the repository. Bun installs from the published package contents, not from the full source tree.

That means files can disappear for several reasons:

A package like this may work in the repository but fail after publish:

json
{ "name": "mycli", "version": "1.0.0", "bin": { "mycli": "dist/cli.js" }, "files": [ "src" ] }

This excludes dist, so the binary target is absent from the tarball. Bun cannot create a launcher for a file that is not installed.

The reverse problem is also common. The package ships dist/cli.js, but the bin field still points at src/cli.ts. The install succeeds, but node_modules/.bin remains empty for that package because the target path is invalid.

How to verify what Bun installed

Inspect the installed package before looking at the consumer code.

sh
ls -la node_modules/.bin cat node_modules/mycli/package.json

You can also inspect the executable target directly:

sh
ls -la node_modules/mycli/dist/cli.js

If node_modules/.bin/mycli is missing, Bun did not create a launcher. That usually means one of these is true:

To check whether Bun sees the package as a binary provider, inspect the resolved metadata:

sh
bun pm ls

If the package is present but the command is absent from node_modules/.bin, the issue is in the package metadata or published contents, not in the consumer invocation.

A working minimal CLI package

This is a complete structure that Bun can install and expose correctly.

package.json:

json
{ "name": "mycli", "version": "1.0.0", "type": "module", "bin": { "mycli": "./dist/cli.js" }, "scripts": { "build": "tsc" }, "files": [ "dist" ] }

src/cli.ts:

ts
#!/usr/bin/env node import { greet } from "./greet.js"; const name = process.argv[2] ?? "world"; console.log(greet(name));

src/greet.ts:

ts
export function greet(name: string): string { return `hello ${name}`; }

tsconfig.json:

json
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "declaration": true, "strict": true }, "include": ["src"] }

After building and publishing, the installed package should contain dist/cli.js, and Bun should create node_modules/.bin/mycli.

How bunx differs from direct node_modules/.bin access

bunx can run a package binary without manually referencing the local shim, but it still depends on the same package metadata.

If bin is broken, bunx mycli fails for the same reason as node_modules/.bin/mycli missing: Bun has no valid launcher to resolve.

The distinction matters when diagnosing the problem:

That separation tells you where to look.

Fixing the package so Bun can generate the launcher

Prefer this structure:

json
{ "name": "mycli", "version": "1.0.0", "type": "module", "bin": { "mycli": "./dist/cli.js" }, "files": [ "dist" ], "scripts": { "build": "tsc", "prepack": "bun run build" } }

This setup makes the install artifact predictable:

If the CLI is meant to be the package’s primary executable, the command name should usually match the package name. That keeps consumer instructions simple and avoids mismatches between the package identity and the launcher name.

Workspace packages can hide this issue because a direct source checkout may contain files that the published tarball does not.

Use the same rules in workspaces that you would use for a published package:

A local link created with bun link or a path dependency still depends on the package’s bin metadata. If the path package omits the binary target, the consumer project will still not get a usable node_modules/.bin command.

Practical check list

Use this sequence when Bun cannot find the package binary:

sh
cat node_modules/mycli/package.json ls -la node_modules/mycli/dist/cli.js ls -la node_modules/.bin/mycli

Then confirm the package metadata contains a valid bin declaration:

json
{ "bin": { "mycli": "./dist/cli.js" } }

If the file is missing, fix the publish artifact. If the file exists but the launcher does not, fix the command name or the bin mapping. If both exist but execution fails, check the shebang and file permissions.

The preferred fix is to publish a built executable under dist, point bin at that file, and include the built output in "files". That makes Bun generate the node_modules/.bin launcher consistently and prevents missing-command failures from reappearing.