Computering in 2026 disclaimer ▿
It feels hard to do anything, really, when it feels like a five alarm fire is burning down the institutions around you. My government is detaining toddlers and shooting civilians, threatening allies, destabilizing the world order for no reason, capturing people and locking them up. I thought we all learned about how to recognize and avoid this in school, but I guess we didn’t. I’m shocked and saddened at how the “don’t tread on me” folks largely became Nazi bootlickers. We collectively seem to have lost our humanity. How does one go on simply living and working with such a furious rage at the stupidity, senselessness, and ruthlessness of it all? Well, I can’t just say nothing.
Am I being melodramatic? It’s tough to overstate just how far the Overton window has shifted. I grew up in a world where everyone. everyone? everyone would be jaw droppingly aghast at a concise recitation of simple facts about current events. Go read a history book. Don’t let the shifting baseline tendency desensitize you!
I could say you should call your representative, but it’s unclear how much of an effect that can have. It doesn’t feel like that rises to the magnitude of the moment. What am I doing writing about computers? We should all be marching in the streets right now! I don’t even know what tactical thing to suggest people do, other than please take care of yourselves and take care of your neighbors.
But also!
Also, simultaneously, we’re living through one of the most dramatic changes in the history of computers. Once again, the shifting baseline tendency might get us, but holy crap computers can do so many more things than even a few years ago! It’s truly astonishing.
So, maybe in between marches you might want to read this.
I’ll begin with the punchline.
If you have Go installed on macOS, Windows, or Linux, you can run:
go run github.com/jtolio/tinyemu-go/temubox/example@2c8151233c2d
and get a full Linux environment with root access.
It requires no special permissions and it works anywhere Go does. There are no CGo calls. It is not using containers. It is running on your computer and is not a shell to elsewhere. It is using a modern kernel (6.6.0). When run on Linux, it can mount the host filesystem inside the environment and gets (some) network access (and theoretically could on macOS and Windows too).
(If you need to exit, type halt to shut it down.)
Neato, right?
Well, that’s not exactly true. Like all of the best things, actually Fabrice Bellard made a full system RISC-V emulator. My addition is that Claude ported it to Go.
Aside: Claude didn’t do this blog post either, mind you. This blog post was written the old fashioned way. I am not letting LLMs within miles of my blog. My blog is written by me. This is me.
But okay, inspired by Armin Ronacher, I got the above Go command working by asking Claude to port Fabrice Bellard’s TinyEMU RISC-V system emulator from C to Go. Claude ported the processor emulator, all of the VirtIO devices including P9 host/guest filesystem mounting, and even kind of ported the SLIRP-based networking stack.
I told Claude to skip TinyEMU’s KVM-based x86 support and SDL-based graphical package, but those would be fun additions for the future, eh?
There are certainly other RISC-V emulators in Go, but I don’t know of one that is pure Go and comes with VirtIO devices and networking support, etc. I’ll be happy to link to them here if corrected, but I certainly haven’t seen someone be able to run go run on only Go files and get a shell.
But perhaps that’s not even the point of this blog post.
Consider this blog post an experience report.
I thought porting TimyEMU would be a good experiment to see how hard it is to wrangle Claude in a complex multi-session problem space, which is all the rage these days. There’s already a clear, straightforward C implementation and there are many RISC-V compliance tests binaries available. While a complex problem, certainly, there are a lot of good constraints here. I’ve also had good experience porting large codebases from one language to another, and so I figured I knew some of the pitfalls to help Claude avoid (e.g. don’t try to fix bugs or add features while transliterating!)
Here’s my terrible, initial prompt:
hello! in this folder is the tinyemu project. it emulates x86 via kvm and riscv via direct emulation. how hard would it be to port the riscv part of this project to pure go (or go + assembly)? specifically, i would want all the functionality that this project offers for riscv emulation in a single statically linked go binary, via changing all the c code to go code where possible. can you ultrathink, do some research, and make a plan?
So it made this document: tinyemu-porting-plan.md
Seemed fine-ish. Good enough. Again, tons of RISC-V compliance tests are available, right? Claude is going to be able to get rapid feedback if something’s not working. Okay, so I asked it to break it into a collection of tickets using Beads, cleared context, then gave it this prompt:
Hello!
Today you will be working on a portion of our project to transliterate the RISC-V emulation parts of the TinyEMU project to Go. If you want to know about the overall plan, please read tinyemu-porting-plan.md
Today, please read `bd quickstart`, and take the next ticket!
When you’re done, commit it to the codebase with git.
A few notes about code expectations:
it is expected that each and every commit has good test coverage.
it is expected that all logic introduced has a clear mapping and association with the original tinyemu c code. we’re not building a new project here, we’re porting an existing one. please be very familiar with the c code for each ticket.
whenever a problem is discovered, it is expected that you write a test first to reproduce it, then fix the codebase to fix the test.
Thanks!
Every time Claude ran out of context (I have auto-compacting turned off), I would clear the session and give it the same thing again. To be fair, this is my general multi-session pattern: have a clear, defined work queue, a document describing the project for reference, and then having Claude churn through tickets.
About halfway through the initial batch of tickets, I was feeling great. It was exciting watching Claude make progress, write tests, check code coverage, and see that things were working. Honestly, I was glued to my screen. I didn’t know why people watch Twitch streams of people playing video games before, but I got it while watching Claude code this. It was fascinating. When it got the Linux kernel to boot I was ecstatic.
It was right after I saw Linux console output for the first time that things started to go sideways. Linux came up, but couldn’t mount initrd, and thus began the first experience of the most common pattern I have had on this (what I thought would be a) throw-away project.
The last 20% of a complex project is so friggin hard with Claude.
I haven’t been doing the coding! So I have no idea what’s wrong or how to fix it! I haven’t built up the context myself, so suddenly I’m staring at a foreign codebase trying to help this robot who is stuck, running in circles, unable to make correct tradeoffs. I have learned that if I read “this is getting complex, I’m going to back out these changes and try a simpler approach”, it is time to pull the plug.
So I prioritized testing. I extended the per-agent prompt with explicit commit instructions requiring high test code coverage for every part of the code with 75% code coverage, 80% for some key packages (the CPU instruction emulation, the MMU, etc). I told it to go find all the RISC-V compliance tests and get those to pass. It did find a lot of issues, but it still couldn’t get initrd to mount and for boot to complete.
It turns out that even though I continued to underscore the importance of matching the C behavior line by line, each agent session made its own decisions, and would often decide to ignore mine. I found many cases where the agent elected to add “better” error handling, or leave a comment saying “in production, we would…”. Argh! If I caught it I could press the agent into sticking with plan, but a lot of the time that Claude was running I was actually playing Mario Kart with my 8 year old.
Here’s an example of the sort of question that would arise that would cause it to ignore me: Is it more important that the Go code match the C code or the RISC-V specification? Well, following the RISC-V specification is honestly a sensible sounding answer, but if we go that route, the transliterated puzzle pieces of the TinyEMU code won’t fit together quite right. So, my opinion is to get everything working matching the working C code first, then start to do codebase cleanup and improvement. “File a ticket, Claude! But otherwise stick to plan.” I told Claude this. It didn’t stick. Keeping adhesion to or even articulating my priorities is hard. So, initially I struggled finding a good way to specify these things in a high level prompt.
I decided to put my approach on pause and try something else. I had Claude write a script that, using ctags, made review batches of every C function in the TinyEMU codebase. Then I had it make a script that made explicit tickets using that information to review and port ~200-300 lines of code at a time independently, function by function, along with explicit instructions that all Go code needs a comment referencing the corresponding C code’s location.
So it started porting the implementation of AES, SHA256, aaargh, oops. So I wrote some exclusions, and tried again.
This approach wasn’t perfect - occasionally it would have prompt-injected itself by leaving comments in the Go code about why it didn’t need to match the C code in ways I disagreed with, but were evidently still persuasive enough. For example, I had a couple of arguments about how to correctly transliterate a C buffer to a Go slice. But, this approach did get me past initrd. It found many locations where it said things like:
Deviations fixed! All three deviations were cases where Go was stricter than C TinyEMU (rejecting instructions that C allows). Per the project guideline “Match C behavior exactly. Even ‘improved’ error handling can break Linux boot”, Go was changed to match C’s more permissive behavior.
This new approach found problems or shortcuts taken with the MMU, the VirtIO interfaces, etc. Luckily the CPU was pretty well covered by the compliance tests.
A huge point I want to make: the fixes in this stage of the project were stupid, dumb fixes that if I had done the development myself I would have already had in the back of my mind, made a ticket for, or said “oh yeah” about immediately. But I couldn’t do that because I hadn’t done the development, so instead I spent a bunch of time poking around in the dark with no context. I didn’t even know to ask if Claude had decided to ignore me and write a “simplified implementation.”
So, while the first 80% felt like a rocket ship, the last 20% was a brutal slog that made me feel useless and frustrated.
And also, this approach revealed that none of the networking stack had been written at all. It had decided to delay implementing the networking stack until after Linux boot entirely.
So, I started the whole process over again with the networking stack. Aiieeeee! Just like init, I had early wins getting ICMP, ARP, and BOOTP working, but a full TCP handshake remained elusive for a week.
I know I’ve spent most of the time talking about Linux bring up, but at this point I’ve spent over 2/3rds of the time on this project trying to trick Claude into getting curl google.com to work, and it only barely works. Frankly, asking Claude to take a non-blocking C SLIRP networking library and transliterate it to Go is a recipe for disaster, and I regret having even attempted it.
Perhaps you remember the famous Steve Yegge posts where he accurately called the rise of Javascript, or the failure behind Google+. Steve Yegge’s maybe got his head on right, I said to myself and others for a decade or so.
So when he announced Beads, I thought, right on, this seems like a cool little tool for helping agents keep context across sessions.
Oh my gosh, don’t use Beads. You should use Ticket instead.
For a long time I thought that Claude was just super slow at executing ticket updates, but nope, it was Beads. Beads is so slow. It didn’t even write out ticket updates all the time, which felt like its one job. It installed hooks into everything. Wait a minute, it has a daemon that’s always running and that isn’t flushing to disk? There are 294k lines of Go in Beads? What is this even doing? Why is the repo 128MB? We’re just updating markdown files in a folder, right? What the heck.
Beads was measurably eating into my productivity, CPU time, and development latency.
I don’t have too many thoughts on Gas Town yet, and certainly I do wonder how much of my meta-level process described in this blog post could also be operated by Claude! Perhaps the sort of multi-session task of porting a CPU emulator from one language to another is a good fit for Gas Town, but my goodness, have some pride in your work! If Beads is the sort of thing the Gas Town approach generates, keep me the hell away from it. Here I am, trying to figure out how to restrain Claude to be less additive and be more subtractive, using Beads, like a fool. I think software developers are going to continue to be gainfully employed for a long time if we can’t figure out how to encourage LLMs to prioritize simplicity.
Ultimately, it was extremely challenging to get Claude to adhere to my goals consistently over many different sessions when Claude was faced with difficulties or setbacks. I can’t imagine a whole town of Claudes and meta-Claudes and “polecats” and “mayors” adhering to my goals better.
First off, this Go library exists and works now, so Claude did something! It built a thing that didn’t exist that I wanted! So, that’s certainly the bottom line here. It’s tough to overstate this point.
But to also complain, this was an exercise in frustration in new and exciting ways. After letting this go a few weeks, I have found a codebase that ticks many boxes (good code coverage, many unit tests, lots of comments and documentation), but seems like it’s been developed by multiple separate raccoons in a trench coat and clearly not a person with one cohesive vision. Maybe this would be less frustrating now that my expectations are more accurately calibrated. So perhaps this blog post can help you calibrate your expectations.
Here was my emotional timeline:
Repeat the following block ~3 times:
And then:
One interesting thing is that Claude was often surprisingly creative in good ways, even when I was trying hard for it not to be. For example, TinyEMU-Go handles a few things better than TinyEMU-C does. It correctly continues when the oom-killer gets triggered in a way that the original TinyEMU faults on.
Claude was sometimes fantastic at debugging. Claude was able to debug that I hadn’t reserved enough memory for the BIOS in the device tree by noticing a hex dump of RAM included “HTTP” in a place it shouldn’t have been, and as a result, TinyEMU-Go supports newer kernels than TinyEMU originally does and an OpenSBI BIOS.
Claude was also sometimes awful at debugging. The network stack needs to get thrown out. It’s just the wrong approach for Go entirely, and Claude does not have the context or sense to identify that it needs to throw it away and port the C code in a different way. Claude is maybe good at fixing things in well designed codebases, but it sometimes can’t debug itself out of a paper bag if it’s a paper bag it designed (and the code has a lot of inertia).
Finally, I really don’t like the API design and calling patterns Claude makes. It’s extremely uncanny-valley-feeling to program against an API Claude designed, even if transliterated from a C project by Fabrice Bellard. I kept expecting to see hands with 4 or 6 fingers. If you want to use this project, I’ve put a human-designed API around it in github.com/jtolio/tinyemu-go/temubox.
I think to distill my advice that I learned from this:
But most of the advice is basically standard software engineering best practices:
Ultimately, this project has simultaneously made me both more and less enthusiastic about coding with LLMs. By the end of this project, it was noticably less frustrating and resulted in higher quality output when I just did things myself; however, my throughput is significantly lower than an LLM. LLMs certainly provide some new capabilities, but if we want our code to not suck, we’re going to have to think really carefully about how to benefit.
Find the repo at https://github.com/jtolio/tinyemu-go