Using Claude Code as a learning companion

I built a Virtual Machine Monitor from scratch in Python that boots the Linux kernel. I did it in my spare time while running a startup, using Claude Code as my learning companion.

I’ve always loved understanding how things work under the hood, but building Veleiro leaves very little time for deep technical exploration. One thing I’ve wanted to learn ever since it came out in 2018 is Firecracker. From the docs:

Firecracker is a virtual machine monitor (VMM) that uses the Linux Kernel-based Virtual Machine (KVM) to create and manage microVMs.

Back in 2018, I read a bit of the source code, and got hooked for a while. I even landed a few (small) PRs as I read the code:

Then, I reimplemented this article about the KVM API adding my own commentary:

After some time though, things got pretty busy at work and I stopped looking into it. Recently, I bought a lifetime license to iximiuz Labs to learn more about containers (fully recommend buying this, btw!). I did a few courses and then read a blog post about Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes. I immediately remembered how fun it was when I was learning Firecracker and how I would love to understand the internals. However, I definitely didn’t have time to look too much into it on the side while building a startup.

Claude Code to the rescue

Given that I have my Claude Code Max subscription, I decided to use it to learn this. But I didn’t want Claude to just teach me passively or implement things for me. Instead, I wanted to go step by step, building everything together while truly understanding how it all works. This was my initial prompt:

We are going to build a VMM from scratch using Python. The idea is to execute the project in phases that build on each other with the ultimate goal of booting a full linux kernel. The main point of this is to learn, so efficiency / performance and security are not the main goals of the project. However, it is very important that we call out where something isn’t efficient / performant / secure enough and how we would improve it to make it so.

We will use cffi to declare the KVM functions, but they will be mostly passthroughs to make things easier with structs / alignment / etc. But the core goal is to use Python for the main implementation.

Since we are in an M4 Mac, we will target ARM 64 (we will use Lima for testing) to prevent cross-compilation or any other weird issues we might hit. We will use /dev/kvm for this.

With all of this in mind, I want you to come up with a good plan on what we need to do to make this happen. Remember, the main goal is to learn, so adding weird acronyms without explanation or variable names that are not descriptive is unacceptable. We MUST have proper documentation end-to-end so that I can understand it. We will build it together and will understand how everything works.

After a few back and forths, I had a plan for how this should all work. This was laid out as several markdown files (or chapters) that build on each other. After that, I had the agent write all of the chapters. It did a pretty good job on the first ones, and then got lazy on the last few, but it was good enough as a start.

Once I had the first draft of the chapters, I asked it to do one more thing to be able to implement this together:

Now, I need you to give me a handoff script (markdown file) for an agent to pick up the work. The idea is that I will hand a specific chapter (so leave that as a placeholder) and then the idea is to work through the chapter together slowly while learning / asking questions. The chapter at the end should be changed to:

  1. Include any additional questions / clarifications that we had (incorporated to the chapter, not as a FAQ or whatever).
  2. Fixes to the code to make sure it works (with explanations / etc).

This was the magical piece. It produced a great prompt for me to use on each chapter. I only changed the chapter and it would read the chapter and be ready for my questions. After that, I read the chapter and gathered questions, clarifications, or things I wanted to dive deeper into. Once I was through, I fired off my questions, and it did some research and came up with good answers. We kept going as deep as I wanted until I was satisfied. Then, it adjusted the content to incorporate all of my questions and feedback. The chapter became tailored to my level of knowledge.

Once we were through with the reading phase, I asked it to write the code in the chapter verbatim. At the beginning, things worked pretty much in the first shot. However, as complexity started to build up, things started to become more complicated and sometimes it would even get stuck.

Stuck? Look at Firecracker

Sometimes the agent would get very stuck and you could see it struggling and even throwing stuff to the wall to see if something would stick. For example, when implementing interactive mode, the shell kept hanging. The agent tried fix after fix, but it was just brute-forcing solutions without really understanding the problem. I could see it going in circles.

That’s when I remembered that we had Firecracker and we could use it as a guide! So I pointed the agent to Firecracker’s source code and told it to use it as a guide to get unblocked and it was able to fix the issues. After that, I asked it to explain why our initial attempt didn’t work and later asked it to update the original tutorial to incorporate all of our new knowledge.

Results

I learned a ton. Here are a few “aha” moments:

MMIO: When the guest writes to address 0x09000000, KVM realizes that’s not in the RAM we registered, traps back to userspace, and hands us the address and data in a shared memory structure. We emulate the UART, write back data for reads, then resume the guest. The entire trick behind device virtualization is intercepting memory accesses to addresses that don’t exist.

initramfs: I finally understood why it exists. The kernel needs drivers to mount a real filesystem, but the drivers are on that filesystem. initramfs breaks the chicken-and-egg problem by embedding a tiny filesystem directly into the boot image.

cffi: Python’s cffi library bridges the gap between Python’s readability and C’s raw power. We could see the exact KVM structure definitions inline with comments, making a complex kernel API understandable.

I’m sure the agent got a few things wrong (if you see something wrong, feel free to point it out to me), but I think that I’m way better off having learned what I did than having this frustration of never learning how this all worked.

Next steps

My plan is to polish the tutorials a bit more and publish them as blog posts in here as well. I also want to add other chapters such as finishing the virtio one and then adding networking, rootfs and even block devices.

In the meantime, here is the repo if you’re curious. I called it god because a VMM creates, controls, and terminates virtual machines.

Appendix

Booting the Linux Kernel and running a shell

Here is my VMM booting the Linux Kernel and executing a shell (trimmed for brevity).

$ sudo uv run god boot build/linux/arch/arm64/boot/Image --initrd build/initramfs.cpio.gz --debug --interactive
Booting Linux with 1024 MB RAM
Kernel: build/linux/arch/arm64/boot/Image
Initrd: build/initramfs.cpio.gz
Command line: console=ttyAMA0 earlycon=pl011,0x09000000

Registered device: PL011 UART at 0x09000000
GIC created: Distributor @ 0x08000000, Redistributor @ 0x080a0000
Generated Device Tree
Loaded kernel at 0x40000000 (3192840 bytes)
Loaded initramfs at 0x48000000 (521954 bytes)
Loaded DTB at 0x48080000 (1590 bytes)
vCPU configured: PC=0x40000000, x0(DTB)=0x48080000

============================================================
Starting Linux...
============================================================

GIC finalized
  [vCPU run #0]
  [vCPU exit: MMIO]
  MMIO[1]: R 0x09000018 (4B)
  [vCPU run #1]
  [vCPU exit: MMIO]
  MMIO[2]: W 0x09000000 (1B)
B...
  # (thousands of MMIO exits as the kernel boots)
...
Linux version 6.12.0 (nmesa@lima-god) #3 SMP Sat Jan  3 07:30:16 -05 2026
Machine model: linux,dummy-virt
earlycon: pl11 at MMIO 0x0000000009000000 (options '')
printk: legacy bootconsole [pl11] enabled
...
  # (kernel initialization messages)
...
Unpacking initramfs...
Freeing initrd memory: 508K
Freeing unused kernel memory: 384K
Run /init as init process
==========================================
  Welcome to our VMM!
  Linux 6.12.0 on aarch64
==========================================

BusyBox v1.36.1 (2026-01-02 08:56:50 -05) built-in shell (ash)

# ls
dev   root  proc  init  bin   sbin  etc   sys   tmp
# echo "Hello from my own VMM"
Hello from my own VMM