Why Claude Code runs on Bun: runtime tradeoffs in TypeScript CLI tooling
Anthropic recently shipped Claude Code — their agentic CLI coding assistant — on Bun instead of Node.js. For most product announcements, the runtime choice would be a footnote. Here it's worth unpacking, because the tradeoffs Anthropic navigated are exactly the ones you hit when building or evaluating TypeScript-heavy developer tooling: startup latency, bundling strategy, native module compatibility, and what "good enough" dependency management actually looks like in 2025.
This isn't a Bun vs. Node benchmarking post. It's an examination of why the decision makes sense for a CLI tool specifically, what it signals about the broader ecosystem, and where the tradeoffs still bite you.
Why runtime choice matters more for CLIs than for servers
For a long-running server process, Node.js startup cost of 50–150ms is irrelevant — you pay it once. For a CLI invoked dozens of times per development session, cold-start latency is a first-class UX concern.
Bun's startup time is consistently in the 5–15ms range for a simple script. Node.js lands closer to 50–80ms before your first line of application code runs. That delta is imperceptible in a single invocation. Run a CLI 30 times in a session and you've saved a couple of seconds — more importantly, you've removed the subjective sense of lag that makes a tool feel heavy.
This is the same reason Deno has gained traction in scripting contexts despite losing the server-side battle to Node. Fast startup is a feature, and for AI-assisted tooling where the human is waiting in a tight feedback loop, it matters.
Bun's bundler as a distribution primitive
Bun ships a first-party bundler. For a CLI, this is significant. The standard Node.js distribution story for a TypeScript CLI involves:
- Compile TypeScript with
tscoresbuild - Bundle with
esbuildorrollupto collapse the dependency graph - Either ship
node_modules(large, fragile) or use a tool likepkgornexeto produce a single executable - Handle platform-specific native binaries separately
Bun collapses steps 1–3 into a single command:
bun build ./src/index.ts --compile --outfile claude-code
The --compile flag produces a self-contained binary that embeds the Bun runtime. No Node.js installation required on the target machine. For a CLI distributed via npm (npm install -g @anthropic-ai/claude-code), this matters less — you can assume Node is present. But for future distribution channels (Homebrew, direct download, CI runner images), single-binary output is a meaningful operational simplification.
Bun's bundler also strips unused exports aggressively. A TypeScript codebase pulling in large SDKs — the Anthropic SDK, tree-sitter bindings, various language servers — can produce a substantially smaller artifact than a naive tsc + ship-everything approach.
The dependency management story is genuinely better
Bun's package manager is faster than npm by a wide margin — typically 10–25× on a cold install, faster still on cache hits. For a tool like Claude Code that's installed globally and occasionally updated, this shows up as a noticeably snappier install.
Bun uses a binary lockfile (bun.lockb) that's more compact and faster to parse than package-lock.json, but it's not human-readable. If you care about auditing lockfile diffs in code review, you'll need to run bun install --frozen-lockfile in CI and accept that the lockfile diff in your PR is opaque.
More substantively: Bun is node_modules-compatible. It doesn't invent a new module resolution scheme — your existing package.json works. Native modules compiled against Node's ABI mostly work, with exceptions for modules using non-public V8 internals. That's where the real compatibility risk sits.
Where native modules still cause pain
Bun uses JavaScriptCore (JSC) instead of V8. For pure JavaScript and TypeScript, this is transparent. For native addons compiled as .node files via node-gyp, the situation is more complicated.
Modules that use the Node-API (N-API) surface — the stable ABI layer introduced specifically for this kind of portability — generally work fine under Bun. Modules that reach into V8 internals or use older nan-based bindings may not.
For a coding assistant CLI, the relevant native dependencies are typically:
- Tree-sitter language parsers (N-API based, generally fine)
- OS keychain access (platform-specific, usually N-API)
- File system watchers
Anthropic presumably validated these before shipping. But if you're evaluating Bun for your own TypeScript tooling and you depend on something like sharp, better-sqlite3, or canvas, verify native compatibility explicitly before committing.
// Quick compatibility check — run under both Bun and Node, compare output
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
try {
const nativeAddon = require('./build/Release/addon.node');
console.log('Native addon loaded:', Object.keys(nativeAddon));
} catch (err) {
console.error('Native addon failed:', (err as Error).message);
}
TypeScript ergonomics: where Bun earns its keep
Bun executes TypeScript directly without a compilation step — no ts-node, no tsx, no esbuild-register. You write a .ts file and run it:
bun run src/index.ts
This is not TypeScript type-checking. Bun strips types and runs the resulting JavaScript. But for iterating on tooling internals, removing the compilation step from the inner loop is a meaningful quality-of-life improvement.
For production builds of a distributed CLI, you still want tsc --noEmit as a type-check step in CI. The Bun model is: use JSC for fast execution, use TypeScript's type checker separately for correctness. These are separable concerns, and Bun's approach of not conflating them is arguably more honest than ts-node's.
A CI configuration that reflects this split:
jobs:
build:
steps:
- name: Type check
run: npx tsc --noEmit
- name: Test
run: bun test
- name: Build binary
run: bun build ./src/index.ts --compile --outfile dist/claude-code
What this signals for the TypeScript tooling ecosystem
The Claude Code runtime decision reflects a broader shift in how TypeScript-native tooling is evaluated. Two years ago, "build on Node.js" was the obvious default. Today, the question is more deliberate: what does this tool actually need from a runtime?
For CLIs and developer tools specifically, the evaluation matrix looks like:
- Startup latency: Bun wins, meaningfully
- TypeScript execution: Bun wins (no compilation step)
- Native module compatibility: Node.js wins, but the gap is narrowing
- Ecosystem breadth: Node.js wins (npm packages, community modules, established tooling)
- Bundling and distribution: Bun wins for single-binary output
- Long-running server processes: Roughly equivalent; Node.js has more production mileage
Anthropic's choice is defensible on every axis that matters for a CLI: fast startup, TypeScript-native execution, and streamlined distribution. The tradeoffs they accepted — opaque lockfiles, some native module risk — are manageable at their scale and usage pattern.
Practical takeaways if you're building TypeScript tooling
If you're maintaining or evaluating a TypeScript CLI or developer tool today:
Audit your native dependencies first. List every package that includes a .node binary. Check the Bun compatibility tracker or run bun install && bun run src/index.ts against your entry point and see what breaks. Native compatibility issues surface immediately.
Use Bun's test runner if you switch. bun test is Jest-compatible (it understands describe, it, expect) and significantly faster. It's the easiest win from a Bun migration.
Keep tsc --noEmit in CI regardless. Bun's type-stripping is not type-checking. Don't let the absence of a build step create a false sense of type safety. The type checker is a separate tool; treat it as one.
Single-binary output is worth evaluating seriously. If your CLI is distributed outside npm — as a GitHub release artifact, inside a Docker image, via Homebrew — bun build --compile simplifies your distribution pipeline in ways that matter operationally.
The broader lesson from Claude Code's stack choice is that runtime selection for TypeScript tooling is now a real engineering decision with real tradeoffs, not a default. Node.js is still the right answer for many contexts. For a fast, TypeScript-heavy CLI where startup latency and developer ergonomics are primary concerns, Bun is increasingly a serious contender — and Anthropic just made that case with a high-visibility production deployment.
Top comments (0)