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:
- Send conversation state and tool definitions to Claude, GPT, or Gemini.
- Parse the model’s requested tool call.
- Search files or run a command.
- Capture stdout, stderr, exit status, and timing.
- Trim or summarize the result.
- Return structured output to the model.
- 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.
| Layer | Good implementation choice | Why | Main trade-off |
|---|---|---|---|
| Agent policy and prompts | TypeScript on Bun | Fast iteration, strong ecosystem, readable control flow | Runtime and dynamic allocation overhead |
| Model adapters | TypeScript on Bun | APIs change frequently; JSON is natural | Provider differences can leak upward |
| Filesystem search and parsing | Rust when profiling justifies it | CPU efficiency and controlled memory use | More build and integration complexity |
| Process supervision | Bun or Rust | Bun is simpler; Rust provides tighter control | Platform-specific behavior |
| Distribution | Bun executable plus native assets | Convenient installation and fewer prerequisites | Larger artifacts and target-specific builds |
| Model inference | Hosted Claude/GPT/Gemini API | No local accelerator management | Network 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:
- Eliminate unnecessary operations.
- Batch related work.
- Cache immutable or content-addressed results.
- Bound output before serialization.
- Replace the remaining measured hot path with native code.
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:
- Target-specific artifacts and checksums.
- A strategy for libc and operating-system compatibility.
- Code signing or notarization where appropriate.
- Startup checks that produce actionable errors.
- End-to-end tests against the packaged artifact, not only source builds.
- A fallback when antivirus software, execution policy, or filesystem permissions block a bundled binary.
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:
- Two languages mean two toolchains, dependency graphs, and debugging workflows.
- Native releases multiply CI and platform-testing requirements.
- Rust improves memory safety, but unsafe dependencies and subprocess boundaries still require review.
- Bun compatibility with Node-oriented packages is broad, not conceptually guaranteed for every package and native addon.
- Provider abstraction reduces duplication but can hide unique tool-calling, caching, context, and streaming semantics.
- Local speed does not repair poor prompts, unbounded tool loops, or wasteful context construction.
The best implementation is usually hybrid: ergonomic orchestration, native hot paths, explicit protocols, and provider-aware model adapters.
Practical takeaways
- Interpret the Bun-and-Rust direction as layering, not a mandate for a full rewrite.
- Measure process count, elapsed time, output bytes, and token usage before optimizing.
- Batch work before replacing TypeScript with native code.
- Start Rust integration with a persistent, versioned subprocess protocol.
- Validate model-generated arguments and enforce repository path boundaries.
- Test packaged artifacts on every supported operating-system and CPU target.
- Keep Claude, GPT, and Gemini routing separate from the tool-execution engine.
- Optimize context selection first: reducing unnecessary tokens often has a larger latency and cost impact than shaving milliseconds from local code.
- Move a component to Rust only when the profile, distribution model, or reliability requirements justify owning another systems-language boundary.
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 →