Skip to content
Writing
4 min readEngineering

The Best Way to Learn a Codebase Is to Break Someone Else's

What maintainers of kitty, calibre, libtorrent and Fallow taught me in review threads, usually by explaining why my fix was wrong in a way I hadn't considered.

I started sending pull requests to open-source projects in March 2026, mostly to tools I use every day. At the time of writing that's 188 merged, across Python, C++, Rust, Go and shell. (The full list is generated from GitHub, so it can't flatter me.)

I learned more from the review threads than from the code I wrote, mostly from maintainers telling me, politely, that I'd missed something about their project that nobody could have told me in advance.

The bug that was a decision

In kitty's drag-and-drop kitten, local copies used hard links when they could. That looked like a bug to me: edit the destination and you've edited the source too. I wrote it up as a fix.

Kovid Goyal's answer was that he'd chosen hard links deliberately, for performance, and that for a move they were always right. For a copy it was debatable, so perhaps an option. The PR became an opt-in independent copy mode with the default left alone, and it merged.

Since then, before I "fix" something odd in a mature codebase, I check whether it's odd on purpose. The git blame or the maintainer usually knows.

The fix that was too expensive

In calibre I wrote a shared guard to keep user-supplied paths inside the library root. My first version resolved every path with realpath().

The review pointed out that realpath() does filesystem I/O, and that many of the paths I'd touched didn't need it. Inside calibre's own library directory you can assume there are no symlinks, because calibre doesn't create them. A security fix that slows down every path check has a cost, and knowing which invariants a codebase already guarantees lets you avoid most of it.

The optimization that could go further

In libtorrent I sped up the piece picker's bitfield updates for sparse bitfields. Arvid Norberg benchmarked it and called it a significant improvement to piece-picker performance, then suggested going further: make the visitor a member of the bitfield type so it can skip zeroes a word at a time instead of a byte at a time. He also caught that I'd used std::countl_zero without including <bit>, which only showed up when the installed headers were compiled on their own. I wouldn't have known to look for either.

Maintainers are people with too little time

Some of the threads that changed how I contribute weren't about code at all.

In Docling I opened a PR for an issue someone else had already fixed the night before. The maintainer thanked me anyway and asked that next time I comment on the issue before starting, so they could assign it, which is obvious in hindsight. I do open source in the gaps around a day job, and claiming the issue first is how you stop two people spending those gaps on the same thing.

In Fallow, one of my PRs sat for a while with a failing check I had no way to fix myself. When it merged, Bart Waardenburg apologized for the wait and explained what that check was, so it wouldn't look like my problem next time. On another PR he pushed his own review fixes to my branch and noted that my newest commits had replaced his local versions of the same fixes. I've learned a fair amount about running a project from being on the receiving end of that.

What carried over between projects

Reading before writing carries over. For my first large Fallow PR, which taught it to understand Vue and Nuxt conventions (about 1,100 lines across 44 files), I read the whole analysis pipeline before changing anything: the graph builder, the import resolver, the framework detection. Most of the time went into working out what the codebase assumed; the change itself came after.

Idioms don't. My early Rust had Python in it: deep nesting, mutable state, strings where the codebase used enums. Nobody rewrote it for me; reviewers showed the idiomatic version and why it was preferred, and that's why it stuck.

If you want to start

Pick a tool you use every day and wait for it to annoy you. Read the issue tracker to see whether someone's already on it, say you're taking it, and read the surrounding code before you touch anything. The fix might take an hour, and understanding the code well enough to make it might take a day. I've learned most from the day.

And when a maintainer says no, read the reason twice. It's usually the most useful thing in the thread.

Next essayEvery Abstraction I RegretA short list of abstractions I added too early, why they got worse over time, and the heuristics I use now to avoid repeating it.