Codex ships a hidden 1.7 GB of binaries. Do you know what's in your .NET artifact?
Simon Willison found a full LibreOffice, Python and Node hidden inside the Codex app. A practical look at what actually ends up in your .NET deployment, and how to take back control.
Jean-Pierre Broeders
Freelance .NET Developer
Simon Willison went digging through his cache folder this week and found something funny. The Codex desktop app, now rebranded to ChatGPT, quietly parks 1.7 GB of loose binaries in there. A full Python install at 440.6 MB. A full Node.js at 446.4 MB. A headless LibreOffice at 429.7 MB. Poppler at 187.9 MB, and git for another 148.1 MB. All tucked away under ~/.cache/codex-runtimes, where no user ever looks.
His note hit the Hacker News front page within hours. Half the thread laughed at the size. The other half understood exactly why it was there: the app has skills that need to read and convert documents, and for that you happen to need LibreOffice and Poppler. Both reactions are correct. And that is the lesson, one that has nothing to do with OpenAI and everything to do with your own deployments.
Because the question is not whether it is silly for a chat app to drag a word processor around. The question is: do you know what is inside your artifact when you push to production on Friday? Most .NET teams I walk into do not. They know their own code. The rest is a black box with a green checkmark in front of it.
Why those binaries are there, and why that can be the right call
Let me be fair to the Codex approach before I start preaching. A tool that has to open documents on any machine has two options. Option one: assume the user has LibreOffice, or Python, or the right version of git. Option two: bring it yourself, pinned to a version you have tested.
Option two costs a gigabyte. Option one costs you a support ticket a week, because half the machines have nothing installed and the other half have the wrong version. Reproducibility is worth real money. If your application needs a specific ffmpeg to work, bundling it is often the sober choice, even if it bloats your image.
The real problem in the Codex story is not the size. It is that nobody knew until someone happened to clean up their disk. There was no visible decision, no line in the release notes, nothing. That is precisely the trap .NET projects fall into, except in our case no Simon Willison ever comes along to find it.
The .NET equivalent: your publish output
Start at the beginning. What happens when you publish? That depends on a choice a lot of people never make out loud.
A framework-dependent build assumes the .NET runtime is already on the target machine. Small, fast, but you are at the mercy of what is installed.
dotnet publish -c Release
# output: dozens of small dlls, no runtime included
A self-contained build brings the whole runtime along. Now you need nothing on the target, but your artifact gets a lot bigger, and every dotnet patch Microsoft ships is now yours to redeploy, because your copy does not age out on its own.
dotnet publish -c Release -r linux-x64 --self-contained true
# output: 70+ MB, full runtime included
This is your version of the LibreOffice story. Self-contained is the reproducible choice, and on a Docker image where you already control everything it is often dead weight. Inside a container you already have the runtime sitting in your base image. Self-contained on top of an aspnet image means you ship the runtime twice: once in Microsoft's layer, once in your own output. I see it more often than I would like.
Your Docker image is the real artifact
For most of my clients the container is the thing that goes to production, not the loose dlls. And that is where weight stacks up in layers you never see if you only read your Dockerfile lines.
The classic mistake is building and running in the same image. You grab the SDK, you compile, and you ship the whole lot. The .NET SDK is close to a gigabyte. You need it in production for precisely nothing. A multi-stage build fixes that:
# Build stage: the heavy SDK, stays out of production
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
# Run stage: runtime only, nothing more
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "MyApi.dll"]
The build stage with its 900 MB SDK disappears completely. Only what you explicitly copy lands in the final result. This is the floor, and there are still teams running that step on the SDK in production.
The next win is your base image. The default aspnet:9.0 is fine, but for a while now Microsoft ships chiseled images: a stripped Ubuntu with no shell, no package manager, just what the runtime needs.
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS final
Smaller, and a lot less attack surface right away, because there is no bash and no apt for an attacker to play with. The catch you need to know: no shell means no docker exec to poke around, and you debug with different tools. That is a fine trade for production, but know you are making it.
Trimming and Native AOT: bring less along
If you really want to cut, trimming goes a step further. The linker throws out any code you never call, including inside your NuGet dependencies.
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
</PropertyGroup>
Sounds free. It is not. Trimming and reflection are old enemies. If a library looks up types at runtime through Type.GetType or through serialization by name, the linker has no idea that code is still needed, so it deletes it. Your build passes, your container starts, and then it falls over three screens deep in a NullReferenceException that seems to come from nowhere. Test a trimmed build as hard as a real release, not a quick run on your laptop.
Native AOT goes further still. You compile ahead of time to a native binary, no JIT, with a fast cold start and a small footprint. For CLI tools and cold-start-sensitive functions that is gold.
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
But AOT is strict. No generating code at runtime, so a lot of classic reflection-based frameworks will not run without source generators. For a tight API project on System.Text.Json source generation it works beautifully. For an old codebase full of dynamic tricks you will fight it. Pick AOT deliberately for the spot where it fits, and do not force it everywhere.
Measure it, or you are just guessing
The whole point of the Codex story is that nobody measured what was there. You do better. Two commands and you already know more about your own image than most teams do.
# Which layer carries how much weight?
docker history my-api:latest
# What is inside per file, and what is wasted?
dive my-api:latest
dive is the tool I install most often on a new project. It shows, per layer, which files get added and which you copy for no reason, plus an efficiency score. The first time you run it on a grown image is almost always a shock. Somewhere there is a COPY . . grabbing your entire .git folder, the frontend's node_modules and three stale test reports.
Do not forget your .dockerignore. It is the cheapest win there is:
.git
**/bin
**/obj
**/node_modules
*.md
Dockerfile
Then look at your NuGet tree. Every PackageReference drags in transitive dependencies you never chose.
dotnet list package --include-transitive
That list is your LibreOffice. There is usually something in there you stopped using after last year's refactor, or a package that pulls half a JSON stack for a feature you could have written yourself in twenty lines.
What I would do
Treat your artifact as something you know, not as something the build pipeline spits out for you. Concretely: a multi-stage build so the SDK never reaches production, a chiseled base image when nothing stops you, and a .dockerignore that actually holds. Run dive once a quarter and after every big dependency change. Reach for trimming and AOT where the win is worth the risk, on CLIs and functions, and not everywhere on principle.
And when you do carry something heavy because you have to, the way the Codex folks carry LibreOffice, write down why. One line in your README or your Dockerfile. Then it is a decision and not a surprise someone stumbles on a year later while cleaning up their disk.
The 1.7 GB in Codex is not the scandal some on HN took it for. It is a mirror. Most of us know just as little about what sits inside our own image, only nobody ever looks.
Frequently asked questions about lean .NET artifacts
When should I pick self-contained over framework-dependent? Pick self-contained when you have no control over what is installed on the target, for instance a tool your customers run themselves. If you run inside a Docker container with a .NET base image you already have the runtime, so framework-dependent is lighter, otherwise you ship the runtime twice.
Will trimming break my application? It can, and often only at runtime. The linker removes code it never sees called, and reflection or serialization by type name slips right past it. Turn trimming on, but test the trimmed build as strictly as a real release before it goes to production.
What are chiseled images and what do they cost?
Chiseled images are stripped Ubuntu variants from Microsoft with no shell and no package manager, which makes them smaller and cuts their attack surface. The cost is that you can no longer open a bash in the container with docker exec to look around, so you debug with tooling from the outside.
How do I quickly see what makes my image so big?
Run docker history for a per-layer breakdown and dive for a file-level view with an efficiency score. You almost always find a too-broad COPY grabbing a .git folder or node_modules, plus transitive NuGet dependencies you surface with dotnet list package --include-transitive.
Further reading
- One Binary to Ship Them All: What Claude Code's Move to Bun-in-Rust Means for Your CLI
- The ripgrep musl crash: why your .NET container on Alpine can betray you
- Docker Compose in production: what works and what doesn't
Conclusion: The 1.7 GB in Codex is not an OpenAI blunder but a mirror for your own deployments. Know what is inside your artifact, measure it with dive, cut deliberately with multi-stage builds and chiseled images, and write down why you carry anything heavy.
Sources: Simon Willison on Codex and LibreOffice · Hacker News discussion.
