Aug 19, 2026 · 5 min · Dev Guides

Claude Code uses Bun written in Rust now

Claude Code uses Bun written in Rust now

Claude Code uses Bun written in Rust now

A coding agent can execute thousands of tiny operations during one repository-wide task: start a subprocess, scan files, parse JSON, apply a patch, and repeat. If each operation adds only 20 milliseconds of avoidable startup or serialization overhead, 10,000 operations consume more than three minutes before the model has generated a single additional token.

That is why Claude Code’s use of Bun—and the movement of performance-sensitive implementation work toward Rust—matters. This is not primarily a language-fashion story. It is an architectural lesson about keeping the flexible parts of an AI application easy to change while making its mechanical hot paths fast, predictable, and distributable as native executables.

The headline needs one clarification: “uses Bun” and “written in Rust” describe different architectural layers, not a magical conversion of TypeScript into Rust. Bun remains the JavaScript runtime and packaging environment. Rust is useful for native components and infrastructure where memory control, startup behavior, concurrency, or binary distribution justify the extra complexity.

Why coding agents expose runtime overhead

A conventional API service may start once and handle requests for days. A local coding agent behaves differently. It repeatedly crosses boundaries between the model, the runtime, the filesystem, and external programs.

A typical loop looks like this:

  1. Send conversation state and tool definitions to Claude, GPT, or Gemini.
  2. Parse the model’s requested tool call.
  3. Search files or run a command.
  4. Capture stdout, stderr, exit status, and timing.
  5. Trim or summarize the result.
  6. Return structured output to the model.
  7. Repeat until the task is complete.

Model latency still dominates many turns, but local overhead becomes visible when the agent performs large numbers of small operations. In practice, the expensive mistakes are rarely sophisticated algorithms. They are spawning a process for every file, serializing oversized JSON objects, rereading unchanged content, or sending megabytes of terminal output back into the context window.

Bun is a sensible runtime for the orchestration layer because it can run TypeScript directly, provides familiar web APIs, starts quickly, and can package applications into executables. Rust is attractive below that layer because it offers native performance without garbage collection and makes resource ownership explicit.

The useful architecture: TypeScript outside, Rust inside

The strongest pattern is not “rewrite everything in Rust.” It is a layered design.

LayerGood implementation choiceWhyMain trade-off
Agent policy and promptsTypeScript on BunFast iteration, strong ecosystem, readable control flowRuntime and dynamic allocation overhead
Model adaptersTypeScript on BunAPIs change frequently; JSON is naturalProvider differences can leak upward
Filesystem search and parsingRust when profiling justifies itCPU efficiency and controlled memory useMore build and integration complexity
Process supervisionBun or RustBun is simpler; Rust provides tighter controlPlatform-specific behavior
DistributionBun executable plus native assetsConvenient installation and fewer prerequisitesLarger artifacts and target-specific builds
Model inferenceHosted Claude/GPT/Gemini APINo local accelerator managementNetwork latency, price, and provider limits

TypeScript should own behavior that changes frequently: prompts, tool schemas, approval rules, retries, and provider routing. Rust should own stable, measurable hot paths.

A common gotcha is moving code to Rust merely because it feels “systems-level.” If a model request takes four seconds and a local parser takes two milliseconds, cutting the parser to one millisecond accomplishes almost nothing. Profile the complete agent loop before choosing a boundary.

Measuring the boundary instead of guessing

Start with elapsed time, invocation count, and bytes transferred. Bun’s built-in APIs are enough for an initial measurement:

const started = performance.now();

const proc = Bun.spawn(["rg", "--json", "TODO|FIXME", "."], {
  stdout: "pipe",
  stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([
  new Response(proc.stdout).text(),
  new Response(proc.stderr).text(),
  proc.exited,
]);

console.error(JSON.stringify({
  tool: "repository_search",
  duration_ms: +(performance.now() - started).toFixed(1),
  stdout_bytes: Buffer.byteLength(stdout),
  stderr_bytes: Buffer.byteLength(stderr),
  exit_code: exitCode,
}));

Run a representative task and aggregate the results:

bun run agent.ts 2> timings.jsonl

jq -s '
  group_by(.tool)
  | map({
      tool: .[0].tool,
      calls: length,
      total_ms: map(.duration_ms) | add,
      output_mb: (map(.stdout_bytes) | add / 1048576)
    })
' timings.jsonl

Suppose local measurement shows 10,000 invocations at an average of 18 milliseconds. The arithmetic is straightforward:

10,000 × 18 ms = 180,000 ms = 180 seconds

That result does not prove Rust is required. Batching ten operations into one invocation may remove 90% of the process boundaries with far less engineering work. The optimization order I use in practice is:

A maintainable Bun-to-Rust boundary

For a first native component, a subprocess with newline-delimited JSON is often better than FFI. It is slower than an in-process call, but easier to debug, language-neutral, and isolated from crashes.

The Bun side can keep one Rust worker alive:

const worker = Bun.spawn(["./bin/repo-indexer"], {
  stdin: "pipe",
  stdout: "pipe",
  stderr: "inherit",
});

const writer = worker.stdin.getWriter();
const reader = worker.stdout
  .pipeThrough(new TextDecoderStream())
  .getReader();

export async function indexRepository(root: string) {
  const request = JSON.stringify({
    version: 1,
    operation: "index",
    root,
    max_file_bytes: 1_000_000,
  });

  await writer.write(new TextEncoder().encode(request + "\n"));

  const { value, done } = await reader.read();
  if (done) throw new Error("repo-indexer exited unexpectedly");

  return JSON.parse(value);
}

The Rust worker reads one request per line and emits one response:

use serde::{Deserialize, Serialize};
use std::io::{self, BufRead};

#[derive(Deserialize)]
struct Request {
    version: u32,
    operation: String,
    root: String,
    max_file_bytes: usize,
}

#[derive(Serialize)]
struct Response {
    version: u32,
    files_indexed: usize,
    skipped_files: usize,
}

fn main() -> anyhow::Result<()> {
    for line in io::stdin().lock().lines() {
        let req: Request = serde_json::from_str(&line?)?;

        if req.version != 1 || req.operation != "index" {
            anyhow::bail!("unsupported request");
        }

        let result = build_index(&req.root, req.max_file_bytes)?;
        println!("{}", serde_json::to_string(&result)?);
    }

    Ok(())
}

Treat this protocol as a real API. Version it, define maximum message sizes, distinguish recoverable errors from process failures, and test malformed input. Never interpolate model-generated strings into shell commands; pass arguments as arrays and validate paths against the repository root.

If profiling later shows JSON serialization is material, a binary protocol or in-process native interface may help. That decision also increases coupling, complicates cross-platform releases, and makes crashes more disruptive. Do not pay that cost preemptively.

Packaging is where the design becomes real

A fast development build is not enough. Developers expect a coding tool to install reliably on macOS, Linux, Windows, Intel, and ARM systems.

Bun can package the orchestration layer:

bun build src/cli.ts \
  --compile \
  --minify \
  --outfile dist/agent

Build the Rust component for each supported target:

cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-unknown-linux-gnu
cargo build --release --target x86_64-pc-windows-msvc

What actually happens when native code enters the release pipeline is that operational complexity moves from users to maintainers. You now need:

A single executable also does not automatically mean a single file. If the Bun program expects a Rust sidecar, the installer must place and locate both safely. Embedding the sidecar and extracting it at startup is possible, but then temporary-directory permissions, updates, and integrity checks become part of the design.

Applying the same pattern to Claude, GPT, and Gemini

Runtime architecture should not hard-code model policy. Keep a normalized request inside the agent and adapt it at the provider boundary:

type AgentRequest = {
  model: string;
  messages: Array<{ role: "user" | "assistant"; content: string }>;
  tools: Array<{ name: string; inputSchema: object }>;
  maxOutputTokens: number;
};

async function invoke(req: AgentRequest) {
  const provider = selectProvider(req.model);

  return provider.generate({
    messages: req.messages,
    tools: req.tools,
    maxOutputTokens: req.maxOutputTokens,
  });
}

That separation lets a team route a long-context repository analysis to Fable 5, use Claude Sonnet 4.6 for coding work, choose GPT-5.5 for another workflow, or test Gemini 3 without rewriting filesystem tools. AI Prime Tech can fit at this provider boundary when cheaper multi-model API access is useful; the agent should still preserve provider-specific capabilities rather than pretending every API is identical.

Token reduction usually saves more money than runtime micro-optimization. Consider an illustrative workload of 2,000 runs per month, each sending 80,000 input tokens. That is 160 million input tokens. If indexing and deduplication reduce each request to 50,000 tokens, usage falls to 100 million—a reduction of 60 million input tokens. Multiply that difference by the chosen model’s current per-million-token input price to calculate the direct saving.

Rust can help build the index quickly, but the economic benefit comes from sending better context, not from Rust itself.

Trade-offs that should remain visible

Bun plus Rust is powerful, but it is not free:

The best implementation is usually hybrid: ergonomic orchestration, native hot paths, explicit protocols, and provider-aware model adapters.

Practical takeaways

PN
Priya Natarajan · ML Platform Lead

Priya leads ML platform engineering and has shipped retrieval and agent systems at scale. She focuses on prompt engineering, RAG, context management, and getting the most performance per dollar from frontier models.

Get cheaper Claude API access

One API key for Claude Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5, plus GPT & Gemini — up to 80% off official pricing, pay-as-you-go.

Get Your API Key →
AI Prime Tech is an independent third-party API gateway. Claude™ and Anthropic® are trademarks of Anthropic, PBC. No affiliation or endorsement is implied.