Alles in een binary: wat de overstap van Claude Code naar Bun-in-Rust betekent voor jouw CLI

Claude Code draait nu als een enkele gecompileerde binary via Bun in Rust. Een .NET-freelancer over waarom distributie in een bestand wint van npm install -g, met Native AOT-voorbeelden.

Jean-Pierre Broeders

Freelance .NET Developer

19 juli 20268 min. leestijd
Alles in een binary: wat de overstap van Claude Code naar Bun-in-Rust betekent voor jouw CLI

Simon Willison heeft vandaag in de binary van Claude Code zitten wroeten en vond iets dat meteen de voorpagina van Hacker News haalde: Claude Code draait nu op Bun, gecompileerd in Rust (discussie, 257 punten, 342 reacties). Hij telde 563 Rust-bestandsnamen die in het uitvoerbare bestand zaten gebakken, dingen als src/bundler/bundle_v2.rs. De tool bundelt een preview van Bun (v1.4.0) die publiek nog niet eens getagd is.

En de clou? Anthropic schreef er zelf bij: "Startup got 10% faster on Linux but otherwise, barely anyone noticed. Boring is good."

Saai is goed. Die ene zin is het interessantste aan het hele verhaal, en daarom wil ik erover schrijven. Want het nieuws hier is niet Rust, en het is ook niet Bun. Het nieuws is dat een van de meest gebruikte developer-tools van dit jaar stiekem gestopt is met "een Node-script dat je installeert via npm" en veranderd is in een enkele binary die je downloadt en draait. Die verschuiving zit er in elk ecosysteem al jaren aan te komen, ook in .NET, en de meeste teams shippen hun interne CLI's nog steeds op de trage, fragiele manier.

Even de zaak maken voor de saaie route.

Het probleem met npm install -g

Heb je ooit een commandline-tool als npm-package uitgeleverd? Dan ken je de faalmodes uit je hoofd. De gebruiker heeft de verkeerde Node-versie. Zijn globale node_modules is half kapot van een afgebroken install. Een transitieve dependency trok een native module mee die op zijn machine niet compileert. De bedrijfsproxy blokkeert de registry. Het postinstall-script faalt zonder een kik. Je krijgt een bugmelding die zegt "hij doet het niet" en veertig minuten later blijkt hij op Node 16 te zitten.

Niks daarvan is de schuld van jouw tool. Het is het afleverkanaal. Een globaal geinstalleerd npm-package is geen ding, het is een graaf van honderden dingen die op install-tijd worden opgelost, op een machine die jij niet in de hand hebt. Elke knoop in die graaf is een kans dat de installatie breekt.

Diezelfde rot zit in Python (pip install, virtualenvs, systeem-Python tegen pyenv) en vroeger ook in .NET. Weet je nog, een tool uitleveren die precies de juiste runtime nodig had, en dan de support-ticket die begon met "hij zegt dat het framework mist"?

Een enkele self-contained binary schrapt die hele categorie problemen. Er is een bestand. Dat draait of het draait niet, en als het niet draait is de fout deterministisch en reproduceerbaar. Dat is wat Claude Code kocht met de Bun-compileerstap: bun build --compile neemt je entrypoint en de hele dependency-tree en last dat samen tot een uitvoerbaar bestand met de runtime erin. Geen Node op de doelmachine. Geen node_modules. Geen install-graaf.

Waarom dit een distributiekeuze is, geen taalkeuze

De reacties op Hacker News staken veel energie in "Rust goed, JavaScript slecht", en ik denk dat dat de kern mist. Anthropic heeft Claude Code niet in Rust herschreven. De applicatielogica is nog steeds TypeScript. Wat veranderde is dat de runtime die die TypeScript uitvoert nu een Rust-programma is (Bun), en dat het geheel vooraf gecompileerd wordt tot een draagbaar artefact.

Dat onderscheid wil ik senior engineers laten inprenten: je taalkeuze en je distributiekeuze zijn losse assen. Je kunt in een hoog-niveau taal schrijven en toch een enkele, bijna-native binary uitleveren. Die 10% snellere startup is leuk, maar de echte winst is dat het artefact nu atomair is. Je kunt het signen, hashen, cachen en op een machine zetten zonder enige voorwaarde vooraf.

Voor een .NET-freelancer is dit een bekend liedje, want .NET heeft precies deze tools al een tijd en een verrassend aantal teams gebruikt ze niet.

Dezelfde truc in .NET

Stel je hebt een interne CLI, een klein tooltje dat je team in CI en op laptops draait. De naieve manier om dat uit te leveren is "installeer de .NET SDK, dan dotnet tool install". Dat heeft dezelfde soort problemen als npm: juiste SDK-versie, juiste feed-toegang, juiste global tool cache.

Dit is het saaie, robuuste alternatief. Een self-contained single-file publish:

dotnet publish -c Release \
  -r linux-x64 \
  --self-contained true \
  -p:PublishSingleFile=true

Dat levert een uitvoerbaar bestand op met de runtime erin gebundeld. De doelmachine heeft niks nodig. Cross-compileren voor de platforms die je wilt doe je door de runtime identifier te wisselen: win-x64, osx-arm64, linux-arm64.

Je kunt verder gaan en de delen van het framework die je niet gebruikt eruit strippen:

<PropertyGroup>
  <PublishSingleFile>true</PublishSingleFile>
  <SelfContained>true</SelfContained>
  <PublishTrimmed>true</PublishTrimmed>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>

Trimmen haalt je binary van ~70MB naar iets in de lage tientallen megabytes, afhankelijk van wat je refereert. Maar trimmen is waar je moet opletten, want alles wat je via reflectie bereikt kan weggetrimd worden en dat merk je pas op runtime.

Native AOT: het dichtst bij wat Claude Code deed

Wil je het .NET-equivalent van "gecompileerde binary, snelle cold start, geen runtime om te JIT-en", dan is dat Native AOT. Dat compileert je app rechtstreeks naar native machinecode, vooraf. Geen JIT-warmup, geen IL, en het startverschil is precies het deel dat telt voor een CLI die je honderd keer per dag aanroept.

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net10.0</TargetFramework>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
  <StripSymbols>true</StripSymbols>
</PropertyGroup>
dotnet publish -c Release -r linux-x64

Het resultaat is een klein native bestand dat in enkele milliseconden opstart. Voor een tool die een developer voortdurend draait voel je dat cold-startverschil, precies zoals Anthropics "10% sneller op Linux". Niemand schrijft er een blogpost over, maar iedereen merkt gewoon niet meer dat de tool bestaat, en dat is het doel.

Native AOT heeft echte beperkingen. Geen code genereren op runtime, dus System.Reflection.Emit valt af. System.Text.Json heeft zijn source generator nodig in plaats van reflectie-gebaseerde serialisatie. Dit is het patroon dat ik gebruik zodat JSON blijft werken onder AOT:

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSerializable(typeof(BuildReport))]
[JsonSerializable(typeof(BuildStep))]
internal partial class CliJsonContext : JsonSerializerContext { }

public record BuildReport(string Commit, IReadOnlyList<BuildStep> Steps);
public record BuildStep(string Name, bool Ok, double Seconds);

// gebruik
var report = new BuildReport(
    Commit: "a1b2c3d",
    Steps: [ new("restore", true, 4.2), new("build", true, 11.8) ]);

string json = JsonSerializer.Serialize(report, CliJsonContext.Default.BuildReport);
Console.WriteLine(json);

De source generator zet de serialisatiecode neer op build-tijd, dus er is geen reflectie op runtime en de trimmer blijft van je model af. Het is iets meer poespas dan een object naar JsonSerializer.Serialize gooien. Daarvoor krijg je een binary die direct opstart en die je DTO's niet kan verliezen aan de trimmer.

Testen dat het artefact echt draait

De faalmodus met AOT en trimmen is dat het ding prima bouwt en dan op runtime ontploft omdat er iets weggetrimd is. Dus zodra je deze weg inslaat heeft je CI een smoke test nodig die de gepubliceerde binary draait, niet dotnet run. Dat zijn andere codepaden.

# .github/workflows/cli.yml
name: cli
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - name: Publish AOT
        run: dotnet publish src/MyCli -c Release -r linux-x64
      - name: Smoke test de echte binary
        run: |
          BIN=src/MyCli/bin/Release/net10.0/linux-x64/publish/MyCli
          "$BIN" --version
          "$BIN" build --dry-run

Draai het echte artefact tegen een paar echte commando's. Als er een reflectiepad is weggetrimd, vang je dat hier op voordat je gebruikers het doen. Ik ben hier precies op stukgelopen: een config-loader die reflectie over een enum gebruikte, werkte in elke test, ging alleen dood in de gepubliceerde AOT-binary. Een smoke test van tien seconden had het gevangen.

Wanneer de saaie route de verkeerde is

Ik ga niet doen alsof distributie via een enkele binary gratis is. Dat is het niet, en het is niet altijd de juiste keuze.

Is jouw tool zelf een Node- of Python-library die anderen als dependency importeren? Dan slaat een gecompileerde binary nergens op. Publiceer hem gewoon op de registry. De binary-aanpak is voor eindgebruiker-executables, de dingen die mensen draaien, niet de dingen die mensen import-en.

Self-contained binaries zijn ook groot en per platform. Je bouwt en bewaart nu een artefact voor elk OS en elke architectuur die je ondersteunt. Voor een tool die naar miljoenen machines gaat, zoals Claude Code, is die ruil overduidelijk de moeite waard. Voor een intern team van vijf man op identieke Linux-runners is een self-contained binary misschien overkill, waar een gewone framework-dependent publish plus een gedocumenteerde runtime-versie het werk doet. Stem de moeite af op de straal van de klap.

En trimmen plus AOT rekt je build op en beperkt je code. Leunt je CLI zwaar op reflectie, dynamische plugins of Reflection.Emit, dan kan hem in AOT persen je meer engineeringtijd kosten dan de snelle start waard is. Meet voordat je je vastlegt.

Wat ik hieruit meeneem

De reden dat dit verhaal de voorpagina haalde is niet dat Rust in de mode is. Het is dat een enorm populaire tool zijn distributie onzichtbaar maakte, en het team trots was dat niemand het merkte. Dat is de lat. Een CLI hoort een bestand te zijn dat je downloadt, dat op elke machine hetzelfde draait, dat snel genoeg opstart dat je vergeet dat er een startkost is.

.NET heeft de stukken hiervoor al jaren: single-file publish, trimmen en Native AOT. De meesten van ons blijven gewoon dotnet tool install shippen en eten de support-tickets. Pak je volgende interne CLI, publiceer hem self-contained, zet een smoke test op de echte binary in CI, en geef je team een bestand in plaats van een installatiehandleiding. Saai is goed.

Wil je op de hoogte blijven?

Schrijf je in voor mijn nieuwsbrief of neem contact op voor freelance projecten.

Neem Contact Op