---
title: "Bun Publishes a CLI That Runs Without `node_modules/.bin` When the Binary Field Is Wrong"
description: "How a Bun package can ship successfully but expose no working command because the bin entry points at the wrong file."
url: "/bun-publishes-a-cli-that-runs-without-node-modules-bin-when-the-binary-field-is-wrong"
canonical_url: "https://bfzli.com/bun-publishes-a-cli-that-runs-without-node-modules-bin-when-the-binary-field-is-wrong"
source_url: "https://bfzli.com/bun-publishes-a-cli-that-runs-without-node-modules-bin-when-the-binary-field-is-wrong.md"
type: "article"
updated: "2026-09-28"
date: "2026-09-28"
tags: ["bun", "cli", "publishing", "package-json", "npm"]
---

> Markdown copy of https://bfzli.com/bun-publishes-a-cli-that-runs-without-node-modules-bin-when-the-binary-field-is-wrong. Append `.md` to any page path on bfzli.com for its markdown twin. Full index: https://bfzli.com/llms.txt

# Bun Publishes a CLI That Runs Without `node_modules/.bin` When the Binary Field Is Wrong

`bunx my-cli` fails with `error: Command "my-cli" not found` after `bun install` or `npm install`, even though the package published successfully and the tarball contains the source files.

## What the installed command resolution depends on

A published CLI package is not executed from its source tree. Package managers create an executable shim when the package exposes a `bin` entry in `package.json`.

A minimal CLI package looks like this:

```json
{
  "name": "@acme/my-cli",
  "version": "1.0.0",
  "bin": {
    "my-cli": "./dist/cli.js"
  }
}
```

The key is the `bin` field value. Package managers resolve it to a file inside the package, then link a command name to that file:

- npm creates `node_modules/.bin/my-cli`
- Bun creates the same style of shim in its install layout
- pnpm creates symlinks in its virtual store and exposes the command on `PATH`

If the path in `bin` does not exist in the published package, the install may still succeed. The command does not. If the file exists but is not executable, or does not start with a shebang, the shim may exist but invoking it fails.

The install-time behavior is different from local source execution. `bun run src/cli.ts` can work because Bun runs the file directly. Installed package behavior depends on the package tarball and the `bin` metadata, not on your workspace layout.

## How `bin` is resolved

The `bin` field supports two forms:

```json
{
  "bin": "dist/index.js"
}
```

This maps the package name itself to a single executable.

Or:

```json
{
  "bin": {
    "my-cli": "dist/cli.js",
    "my-cli-dev": "dist/dev.js"
  }
}
```

This maps multiple command names.

During install, the package manager reads `package.json` from the unpacked package. It then checks the `bin` target path relative to the package root. That path must be present in the tarball or the package’s unpacked contents.

A wrong path is enough to break the command:

```json
{
  "name": "@acme/my-cli",
  "bin": {
    "my-cli": "./bin/index.js"
  }
}
```

If the real file is `dist/cli.js`, the package still publishes. The command shim points at a file that is missing after installation.

The same applies if the path is right in source but the build step does not run before publish. For example, `src/cli.ts` may exist in the repository, while the published package only contains `dist/` files generated by `prepack` or `prepare`.

## Why a missing shebang breaks execution

A CLI entry file needs an executable header when the package manager invokes it directly as a process target.

For Node-based CLIs, the standard first line is:

```js
#!/usr/bin/env node
```

For Bun-native CLIs, either of these is common:

```js
#!/usr/bin/env bun
```

or

```js
#!/usr/bin/env node
```

if the file is still meant to run under Node.

Without a shebang, the OS does not know which interpreter should execute the file. On Unix-like systems, direct execution usually fails with errors such as:

```text
env: node: No such file or directory
```

or:

```text
/usr/bin/env: ‘node’: No such file or directory
```

If the file is present but lacks execute permission, the failure is different:

```text
Permission denied
```

Package managers generally preserve the executable bit from the packaged file when they create the shim. That means the file must be marked executable in the repository or during the build step before packing.

## Bun publishing specifics

Bun can publish packages with `bun publish`, and it can also pack them with `bun pm pack` for inspection. The important part is that Bun publishes the contents of the package tarball, not the full workspace. That means any file needed by the CLI must be included in the packed result.

The usual setup is one of these:

- TypeScript source compiled to `dist/` before publish
- A direct `.ts` entrypoint that Bun can run, if the package is designed for Bun
- A generated JavaScript file with a shebang and executable bit

For a Bun-targeted CLI, this works:

```json
{
  "name": "@acme/my-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-cli": "./dist/cli.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "bun build src/cli.ts --outdir dist --target bun",
    "prepack": "bun run build"
  }
}
```

And the entry file should start with a shebang:

```ts
#!/usr/bin/env bun

console.log("hello from bun cli")
```

If the build output omits the shebang or strips the executable bit, the command may not work after install even though `bun run dist/cli.js` appears fine.

For Node compatibility, the same pattern applies, but the shebang should be `#!/usr/bin/env node`, and the code should be valid for the Node runtime that users actually have installed.

## Why local runs can succeed while installed runs fail

A local run uses the file path you point at:

```bash
bun run src/cli.ts
```

That bypasses `package.json` `bin` resolution entirely.

An installed command uses the package metadata:

```bash
bunx my-cli
```

or:

```bash
npm install
my-cli
```

In the installed case, the package manager has only the packaged files to work with. If the `bin` path is wrong, the shim points to nowhere. If the file is not executable, the shim exists but cannot launch it. If the file lacks a shebang, the kernel does not know how to run it.

This is why local testing is not enough. A CLI can be runnable from source and still ship a broken command name.

## A minimal broken package

This package can publish without error, but the command will not work:

```json
{
  "name": "@acme/my-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-cli": "./bin/cli.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "bun build src/cli.ts --outdir dist --target bun",
    "prepack": "bun run build"
  }
}
```

The bug is the `bin` path. The build emits `dist/cli.js`, but the manifest points at `bin/cli.js`.

A second broken case is a missing shebang:

```ts
export function main() {
  console.log("hello")
}

main()
```

The file compiles and executes under `bun run dist/cli.js`, but the installed command may fail because the shell shim invokes it as an executable file.

A third broken case is permissions. If the file is not executable, installation may still place the shim, but the final invocation fails when the OS refuses to launch the target.

## Correcting the package manifest

Point `bin` at the real emitted file, not the source file unless the package is deliberately shipping source and the runtime supports it.

```json
{
  "name": "@acme/my-cli",
  "version": "1.0.1",
  "type": "module",
  "bin": {
    "my-cli": "./dist/cli.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "bun build src/cli.ts --outdir dist --target bun",
    "prepack": "bun run build"
  }
}
```

Then ensure the file begins with a shebang:

```ts
#!/usr/bin/env bun

export function main() {
  console.log("hello")
}

main()
```

If you compile TypeScript with a separate tool, configure it to preserve the shebang or add it to the generated file. Many bundlers and transpilers will remove or relocate the first line unless configured explicitly.

If the package targets Node instead of Bun, use:

```ts
#!/usr/bin/env node

console.log("hello")
```

The runtime in the shebang must match what the installed command expects to execute.

## Making the file executable

On Unix-like systems, the command target should have executable permission before packing.

Verify with:

```bash
chmod +x dist/cli.js
ls -l dist/cli.js
```

You want to see an executable mode such as `-rwxr-xr-x`.

If the file is generated during build, add the permission step to the build or `prepack` process. For example:

```json
{
  "scripts": {
    "build": "bun build src/cli.ts --outdir dist --target bun && chmod +x dist/cli.js",
    "prepack": "bun run build"
  }
}
```

On Windows, executable bits are not enforced the same way, but the packaged tarball still needs a command file that works for Unix users. Publishing a cross-platform CLI usually means testing on a Unix-like environment as part of the release process.

## Verifying the package before publish

The most reliable check is to inspect the packed tarball, not the workspace tree.

With Bun:

```bash
bun pm pack
tar -tf acme-my-cli-1.0.1.tgz
```

Confirm that:

- `package.json` is present
- the file named in `bin` is present
- the file has the correct path
- the file is executable in the tarball contents

Extract and inspect the manifest:

```bash
tar -xOf acme-my-cli-1.0.1.tgz package/package.json
```

Then confirm the binary path exists inside the archive:

```bash
tar -tf acme-my-cli-1.0.1.tgz | grep 'dist/cli.js'
```

You can also install the tarball into a temporary project and test the command exactly as users will run it:

```bash
mkdir /tmp/my-cli-test
cd /tmp/my-cli-test
bun init -y
bun add /path/to/acme-my-cli-1.0.1.tgz
bunx my-cli
```

If the command is broken, this test fails in the same way a real install does.

## Common failure modes

A few combinations produce similar symptoms but have different causes:

- `bin` points at a file that is not in `files`
- `bin` points at source that is not published
- the generated file exists but lacks a shebang
- the generated file exists but is not executable
- the package name and command name do not match what you expect
- the package is tested with `bun run` but shipped as an installable CLI

The `files` field matters because it limits what enters the tarball. A correct `bin` path is useless if the file is excluded from publish.

For example:

```json
{
  "files": [
    "src"
  ]
}
```

with `bin` pointing at `dist/cli.js` will fail after packing because `dist` is not included.

## A safe release check

Use this sequence before publishing:

```bash
bun run build
chmod +x dist/cli.js
bun pm pack
tar -tf *.tgz
bun add ./acme-my-cli-1.0.1.tgz
bunx my-cli
```

That verifies the actual publish artifact rather than the local source tree.

If the CLI is intended to run under Bun, make sure the emitted file starts with `#!/usr/bin/env bun` or a compatible Node shebang if it is Node-compatible. If it is intended for Node users, ship JavaScript that Node can execute without Bun-only syntax.

## Practical takeaway

Prefer a `bin` entry that points at the real built file in the published tarball, with a correct shebang and executable bit, and verify the packed artifact before release. That combination prevents the common failure where a package installs successfully but exposes no working command because the binary path is wrong or the entry file cannot be executed.
