I’ve contributed to large open-source projects before, but mostly from the security research side. This time, I ended up contributing from the development side.
I’ve now had a slight taste of both worlds. I’m obviously still very much at the beginning of both, but having been on both sides of the table has made the similarities, and the differences much more obvious to me.
So here’s what happened when I swapped my hacker hat for a developer one, and what I found familiar and what felt different.
Also, conveniently, this particular investigation is one I can actually talk about publicly. The other one is still sitting behind an embargo, because apparently security research has a very different concept of “done.”
It started because I was curious
I’ve been learning Rust recently. Since I’m on Arch Linux, I was compiling Zed, a popular code-editor, from source. Zed being written in Rust was part of what made the project interesting to me, so I started poking around the repository.
Eventually, I came across an issue: Some keybinds do not render on the tab switch page on some platforms.
The issue had already been found and opened by a Zed maintainer. I wasn’t discovering some mysterious bug nobody knew about. I just thought, “Okay, that’s weird. What’s actually causing it?”
It sounded small enough to investigate without knowing the entire codebase. So I started digging.
Following the bug through the documentation pipeline
The first thing I had to figure out was how Zed’s documentation actually gets from source to browser. There is a custom documentation preprocessor with constructs such as:
{#action ...}
{#kb ...}
which are processed before the final HTML is generated.
I followed the path through the docs preprocessor, keybinding lookup, generated HTML, and finally the JavaScript that processes the resulting <kbd> elements.
This part was interesting because the starting point was very different from security research.
When I’m hunting for a vulnerability, I don’t necessarily start with someone telling me “this exact thing is broken.” A big part of the job is understanding the attack surface, figuring out which parts of a product or service are reachable from the user side, identifying areas where weaknesses are more likely, and systematically exploring them.
Here, someone had already done that first part for me. The maintainer had already told me what was wrong. I had to figure out how, why, and where it was happening.
Going backwards before going forwards
There was already an older issue and a recently merged PR dealing with related missing keybindings. So I went through the older issue, the related PR, its commits, and eventually git blame. I wanted to know what had already been fixed, whether this was a regression, which lines came from the previous PR, and what assumptions the existing implementation was making. The timing was interesting because that previous PR had been merged only a few hours earlier. I also opened the live Zed documentation and inspected the generated HTML with DevTools.
For a while, the live site and my local build didn’t agree.
My local repository already contained the freshly merged change, while the deployed site hadn’t caught up yet. So, amusingly, my local documentation was temporarily slightly ahead of the real Zed website.
This kind of historical digging felt very familiar from security research too. When investigating a vulnerability, I’ll often look through older CVEs, previous versions, and the commits that patched them. Sometimes the patch reveals patterns, incomplete fixes, or similar assumptions elsewhere in the codebase.
History is evidence.
Finding the actual bug
Eventually I reached: docs/theme/plugins.js
The keybinding processing assumed that every <kbd> contained two values separated by “|”: macOS | Linux.
That works for: ctrl-tab|ctrl-tab
But some hand-written elements were simply:ctrl, tab, shift, enter, escape,etc.there was no tab. The code was:
function processKeybinding(element) {
const [macKeybinding, linuxKeybinding] =
element.textContent.split("|");
element.textContent =
isMac ? macKeybinding : linuxKeybinding;
element.classList.add("keybinding");
}
So for ctrl:
"ctrl"
↓
split("|")
↓
["ctrl"]
↓
linuxKeybinding = undefined
↓
empty keybinding pill
The fix was:
function processKeybinding(element) {
const [macKeybinding, linuxKeybinding = macKeybinding] =
element.textContent.split("|");
element.textContent =
isMac ? macKeybinding : linuxKeybinding;
element.classList.add("keybinding");
}
If a Linux-specific value exists, use it. If it doesn’t, fall back to the first value.
There was another thing hiding nearby
While investigating the tab-switcher documentation, I also noticed: tab_switcher::ToggleAll
I checked the repository, default keymaps, and the application itself. There wasn’t a default binding for it, so the preprocessor was correctly producing “No default binding”
I initially removed that reference because the rendered result looked strange. But the maintainer later re-added it intentionally so that if a default keybinding is added in the future, the documentation will automatically pick it up. That was a useful reminder: not every weird-looking thing you encounter while debugging is something you should fix. Sometimes you investigate it, understand it, and leave it alone.
Testing it properly
I still needed to verify the fix, which meant getting familiar with Zed’s mdBook setup and the specific version expected by the project. So I generated the required action metadata, built the documentation locally, and repeatedly changed, rebuilt, opened the local docs, inspected the output, and compared it with the expected behavior.
This felt very familiar from security research.
When I think I’ve found a vulnerability, I don’t just get one successful PoC and declare victory. I stress-test the hypothesis:
- What if the input changes?
- What if an assumption doesn’t hold?
- What if there’s another code path?
- What if the behavior only occurs under a particular configuration?
- What if my payload is triggering something completely different?
I can end up tweaking PoCs, payloads, inputs, and test conditions over and over again until the result holds consistently and I’ve ruled out the possibility that my original hypothesis is simply wrong.
My camera/mbedTLS investigation was a good example. Read it here if you are curious: I Found a MitM Vulnerability in a Camera's TLS Handshake (I Was Wrong): A Static Reversal of an mbedTLS Config Struct
For Zed though, the hypothesis was much simpler: the platform-specific keybinding code breaks when a <kbd> contains only one value. I wanted to make sure the fallback fixed that case without breaking the two-value case.
And it did.
Git became part of the investigation too
The code wasn’t the only unfamiliar part. I also had to get comfortable with the practical workflow of contributing to somebody else’s repository:
fork → remotes → feature branch → changes → commits → push → PR → review
Along the way I became much more comfortable with:
git log
git blame
git fetch
git remote
git reset --soft
I made some experimental commits while figuring things out, then cleaned up the history so the final branch contained a focused commit with only the changes I actually wanted. That was a small but useful difference from using Git primarily for my own projects. When you’re asking someone else’s maintainers to review your code, the branch itself is part of the contribution.
So how different was this from security research?
Less different than I expected, but the starting point and end goal were different.
In security research, I’m actively hunting for weaknesses. I have to understand the attack surface, identify interesting user-accessible areas, investigate them, develop and refine a PoC, reproduce the behavior, analyse accessibility and impact, and document the evidence well enough for the owner or maintainer to act on it.
Here, someone had already handed me the starting point. I knew what was wrong. I had to trace how, why, and where it was happening, understand the surrounding history, implement the fix, test it, and submit the change.
The overlap was in the investigation: unfamiliar code, execution paths, reproduction, history, and repeatedly testing whether my understanding was actually correct.
The difference was the problem I was trying to solve:
Security research asks “Can I find and prove a weakness?”
Development asks “Can I understand this problem and make the system behave correctly?”
Looking back
By now I’ve stepped into open-source codebases from both sides.
The workflows aren’t the same, but the investigative mindset carries over surprisingly well.
Be curious. Follow the evidence. Reproduce it. Try to prove yourself wrong. Understand the history. Keep the scope sane.
And I have a lot of respect for why the security side moves differently.
Once something potentially affects real users, devices, infrastructure, or other projects, there can be a lot riding on getting the details right. Reports may need private coordination, independent reproduction, impact validation, patch development, communication with affected parties, an an embargo before anything can be made public.
So yes, I’ve had security findings sit around for months while it all plays out. But that’s also part of what makes the process serious.
The funny contrast is this Zed bug, which I could investigate completely in public. It went from curiosity to PR #65105 to merge within a day. Meanwhile, there’s some security research contributions I’ve been sitting on for months that I still can’t talk about!
Different hats. Different responsibilities. Different timelines. But surprisingly similar worlds.
And I love them both.