| | |

Bun 06 ⚡ Running Scripts and Executing Binaries via bun run and bunx

⏱️ Estimated read: 11 min | |

Bun’s execution layer is split into two distinct commands that serve different purposes. bun run executes scripts defined in package.json or runs source files directly. bunx executes binaries from npm packages, installing them temporarily if they are not already present. The distinction matters: bun run operates within the project’s context, while bunx operates at the ecosystem level, fetching and running tools that the project may not depend on. Both commands inherit Bun’s startup speed—approximately 6ms on Linux compared to npm’s 170ms—which makes them viable for interactive use and CI pipelines where every millisecond compounds .

Key point: bun run <script> resolves against the scripts field first, then source files, then project binaries, and finally system commands. bunx <package> checks node_modules first, then installs from npm into the global cache. The --bun flag forces an executable to run under Bun’s runtime instead of Node.js, but it must appear before the executable name.


Why bun run and bunx exist separately

The project-context problem. A script defined in package.json is a command that belongs to the project. It expects to run from the project directory, with the project’s dependencies available, and with the project’s environment variables. bun run is designed for this: it reads package.json, locates the script, spawns a subshell, and executes the command with the project’s node_modules/.bin in PATH . The script runs in the context it was written for.

The one-off tool problem. A developer needs to run Prettier once to format a file, or Prisma to run a migration, or a scaffolding tool to create a new project. Installing these tools as project dependencies pollutes package.json and node_modules. bunx solves this by checking whether the package is already installed locally, and if not, installing it into Bun’s global cache temporarily . The tool runs, and the project remains unchanged.

The shebang problem. Many npm binaries declare #!/usr/bin/env node as their shebang. When Bun executes such a binary, it respects the shebang and spawns a Node.js process. This is correct for compatibility, but it means the binary runs under V8, not JavaScriptCore. The --bun flag overrides this behavior and forces execution under Bun’s runtime, which can be faster for JavaScript-heavy CLIs . The flag is a runtime override, not a package selector.

The resolution-order problem. bun run <name> resolves the name in a defined order: package.json scripts first, then source files with recognized extensions, then binaries from project dependencies, and finally system commands. If a script named build exists alongside a file named build.ts, the script wins. If a built-in Bun command shares the name, the built-in wins unless bun run is used explicitly . This order is deliberate: the script is the project’s intent, the file is the implementation, and the binary is the fallback.

The trade-off. bun run is fast because it resolves locally and avoids network requests. bunx is fast because Bun’s startup time is low and the global cache avoids repeated downloads. But bunx does contact the registry once a day to check for newer versions, and bun run cannot execute a tool that is not installed. The choice depends on whether the tool is a project dependency or a one-off utility.


a. bun run: scripts, files, and resolution order

The bun run command accepts either a script name or a file path. When given a name, it searches package.json scripts first. When given a path ending in a recognized extension (.js, .ts, .jsx, .tsx), it executes the file directly .

# Run a script defined in package.json
bun run dev

# Run a source file directly
bun run src/main.ts

# Omit "run" for scripts (but not for files with same-name built-ins)
bun dev

The script runs in a subshell. On Linux and macOS, Bun checks for bash, sh, and zsh in that order. On Windows, it uses Bun’s built-in shell. Lifecycle hooks are respected: pre<script> runs before <script>, and post<script> runs after. If the pre-script fails, the main script does not run .

The --bun flag forces the script to use Bun’s runtime for any Node.js CLIs it invokes. This is useful when a script calls vite or next, which are JavaScript files with Node shebangs. Without the flag, Bun spawns Node.js to run them. With the flag, they run under Bun .

# Force the script's CLIs to use Bun's runtime
bun run --bun vite

For monorepos, the --filter flag runs a script in matching workspaces. The pattern can be a package name glob, a directory path, or a dependency relation. The --parallel and --sequential flags control execution order across multiple scripts or workspaces, with prefixed output that identifies which script produced which line .

# Run "build" in all workspaces
bun run --filter '*' build

# Run "build" and "test" concurrently
bun run --parallel build test

b. bunx: one-off package execution

The bunx command (alias for bun x) executes a binary from an npm package. If the package is already in node_modules, Bun uses the local copy. If not, it installs the package into the global cache and runs the binary from there .

# Run a package binary
bunx cowsay "Hello world!"

# Run a specific version
bunx uglify-js@3.14.0 app.js

# Run a binary whose name differs from the package name
bunx -p @angular/cli ng new my-app

The --package (or -p) flag is required when the binary name does not match the package name. @angular/cli provides the ng binary, so bunx ng would fail without -p @angular/cli .

The --bun flag forces the binary to run under Bun instead of Node.js. It must appear before the executable name. Flags after the name are passed through to the executable .

# Force Bun runtime
bunx --bun my-cli

# Pass flags to the executable
bunx my-cli --foo bar

bunx checks the registry once a day for newer versions of the package. If the registry is unreachable, it warns and runs the cached version. bunx my-cli@latest checks on every run . The --no-install flag skips installation if the package is not already present, which is useful in CI environments where network access is restricted .


c. Passing arguments and environment control

Arguments after the script or executable name are passed through. For bun run, the arguments go to the script. For bunx, they go to the executable. Bun does not intercept them unless they appear before the name.

# Arguments go to the script
bun run test --coverage

# Arguments go to the executable
bunx prisma migrate --name init

The --env-file flag loads environment variables from a file before execution. This is available on bunx and can be combined with the other flags. The --cwd flag changes the working directory before running the command .

# Load environment and run from a specific directory
bunx --env-file=.env.test --cwd=./packages/api prisma migrate

For watch mode, Bun provides --watch and --hot. The --watch flag restarts the process on file changes. The --hot flag enables hot reload, which preserves state between reloads. These flags appear before run when using bun run .

# Watch mode for a script
bun --watch run dev

Complete Example Session

# ============================================
# PART 1: RUN A PACKAGE SCRIPT
# ============================================

bun run dev
# Executes the "dev" script from package.json.
# ============================================
# PART 2: RUN A SOURCE FILE
# ============================================

bun run src/server.ts
# Executes the TypeScript file directly.
# ============================================
# PART 3: FORCE BUN RUNTIME FOR CLI
# ============================================

bun run --bun vite
# Vite runs under Bun instead of Node.js.
# ============================================
# PART 4: RUN SCRIPT IN WORKSPACES
# ============================================

bun run --filter '*' build
# Runs "build" in every workspace.
# ============================================
# PART 5: RUN MULTIPLE SCRIPTS CONCURRENTLY
# ============================================

bun run --parallel build test lint
# All three scripts run with prefixed output.
# ============================================
# PART 6: RUN A ONE-OFF TOOL
# ============================================

bunx cowsay "Hello"
# Installs cowsay temporarily and runs it.
# ============================================
# PART 7: RUN WITH PACKAGE FLAG
# ============================================

bunx -p @angular/cli ng new my-app
# The "ng" binary is in @angular/cli, not a package named "ng".
# ============================================
# PART 8: FORCE BUN RUNTIME FOR BINARY
# ============================================

bunx --bun my-cli
# The --bun flag appears before the executable name.
# ============================================
# PART 9: PASS ARGUMENTS
# ============================================

bunx prisma migrate --name init
# Arguments after the executable name go to Prisma.

The nine parts trace the execution model: script execution, file execution, runtime override, workspace filtering, parallel execution, one-off tools, package flag, runtime override for binaries, and argument passing.


Quick Reference

bun run Resolution Order

PrioritySourceExample
1package.json scriptbun run build
2Source filebun run src/main.ts
3Project binarybun run eslint
4System commandbun run ls (only with run)

bunx Flags

FlagPurpose
--bunForce Bun runtime (before executable)
-p, --packageSpecify package when binary name differs
--no-installSkip installation if not present
--cwdChange working directory
--env-fileLoad environment variables

bun run Flags

FlagPurpose
--bunForce Bun runtime for CLIs
--filter <pattern>Run in matching workspaces
--workspacesRun in all workspaces
--parallelRun scripts concurrently
--sequentialRun scripts one after another
--watchRestart on file changes

Best Practices

✅ Do This:

bun run build
<!-- Use explicit run for scripts that share built-in names --> # ✅
bunx -p @angular/cli ng new my-app
<!-- Use --package when binary name differs -->            # ✅
bunx --bun my-cli
<!-- --bun before the executable name -->                 # ✅

❌ Don’t Do This:

bunx my-cli --bun
<!-- --bun is passed to the executable, not bunx -->      # ❌
bun run dev --watch
<!-- --watch is passed to the script, not Bun -->         # ❌
# Assume bunx uses the local package version when @version is specified
<!-- bunx pkg@2 downloads pkg@2, ignoring local -->       # ❌

Common Pitfalls

PitfallWhy It HappensFix
Script not foundName collides with built-in commandUse bun run <script>
Binary not foundPackage name differs from binaryUse -p <package>
--bun ignoredPlaced after executable nameMove before executable
Watch flag ignoredPlaced after runUse bun --watch run
Workspace script missingPattern does not matchVerify package name with --filter

Real-World Examples

1. Run Script

bun run dev

2. Run File

bun run src/server.ts

3. Force Bun Runtime

bun run --bun vite

4. Filter Workspaces

bun run --filter '*' build

5. Parallel Scripts

bun run --parallel build test

6. One-Off Tool

bunx cowsay "Hello"

7. Package Flag

bunx -p @angular/cli ng new my-app

8. Force Bun Runtime for Binary

bunx --bun my-cli

9. Pass Arguments

bunx prisma migrate --name init

10. Skip Installation

bunx --no-install eslint

Visual

Resolution Order for bun run

┌──────────────────────────────────────────────┐
│  bun run <name>                              │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  1. package.json script?                     │
│     ├─ YES → Execute script                  │
│     └─ NO  → Continue                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  2. Source file with extension?              │
│     ├─ YES → Execute file                    │
│     └─ NO  → Continue                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  3. Project binary?                          │
│     ├─ YES → Execute binary                  │
│     └─ NO  → Continue                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  4. System command? (bun run only)           │
│     ├─ YES → Execute command                 │
│     └─ NO  → Error                           │
└──────────────────────────────────────────────┘

bunx Execution Flow

┌──────────────────────────────────────────────┐
│  bunx <package>                              │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Is package in node_modules?                 │
│  ├─ YES → Use local binary                   │
│  └─ NO  → Install to global cache            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Shebang is #!/usr/bin/env node?             │
│  ├─ YES → Spawn node process                 │
│  └─ NO  → Execute with Bun                   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  --bun flag present (before name)?           │
│  ├─ YES → Force Bun runtime                  │
│  └─ NO  → Respect shebang                    │
└──────────────────────────────────────────────┘

Summary

ItemValue
Script executionbun run <script> or bun <script>
File executionbun run <file.ts>
One-off packagebunx <package>
Force Bun runtime--bun (before executable name)
Package flag-p <package> (when binary name differs)
Workspace filter--filter <pattern>
Parallel scripts--parallel
Watch modebun --watch run <script>
Startup time~6ms (Linux)

Key takeaways:

  • bun run resolves against the project, bunx resolves against npm. The script runs in the project’s context. The one-off tool runs from the global cache .
  • The resolution order for bun run is script, file, binary, system command. If a built-in command shares a name, bun run forces the script resolution .
  • --bun must appear before the executable name. Flags after the name are passed to the executable, not to Bun .
  • -p is required when the binary name differs from the package name. @angular/cli provides ng, so bunx ng fails without -p @angular/cli .
  • Lifecycle hooks are respected. pre<script> runs before <script>, and a failure in the pre-script prevents the main script from running .

Remember: bun run is the project’s command runner. bunx is the ecosystem’s tool runner. The first operates within the project’s dependencies and scripts. The second fetches tools that the project may never need again. The --bun flag is the runtime override that bridges them, forcing Node.js-targeted binaries to run under Bun’s engine. The startup speed makes both viable for interactive use, but the resolution order determines which tool actually runs.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!