# Claude Code, Bun and TypeScript

> Source: <https://dev.to/dimidan/claude-code-bun-and-typescript-cjj>
> Published: 2026-07-27 12:50:21+00:00

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.

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 ships a first-party bundler. For a CLI, this is significant. The standard Node.js distribution story for a TypeScript CLI involves:

`tsc`

or `esbuild`

`esbuild`

or `rollup`

to collapse the dependency graph`node_modules`

(large, fragile) or use a tool like `pkg`

or `nexe`

to produce a single executableBun 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.

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.

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:

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);
}
```

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
```

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:

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.

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.
