I Went Looking for Open Source Contributions. Four of the Seven Were Traps.
Filtering GitHub by "good first issue" mostly returns contribution farms and moods dressed up as tasks. Seven candidates survived a first look, three became pull requests, and the four I threw away are the more useful half of the story.
Every guide to open source contribution tells you to filter GitHub by good first issue and start typing. I did that. Of the first forty issues that search returned, roughly thirty-five were worthless — auto-generated filler in repos built to farm contribution counts, or feature requests in someone's personal side project with four stars.
Then I went hunting properly. Seven candidates survived a first look. Three became pull requests. Four I threw away, and the four I threw away are the more useful half of this post, because each one failed for a different reason that the search results could not have told me.
Here is the whole path, including the parts where I was about to submit something wrong.
The problem with the obvious search
Sort label:"good first issue" by newest and you get noise, because the label is free and the incentive to apply it is high. Two failure modes dominate.
Contribution farms. I hit a repo serving issues numbered in the twenty-nine thousands — "Add new Japanese Proverb 151", "Add new Japan Fact 297". These exist so people can inflate a contribution graph. A merged PR there is worth nothing to anyone reading your profile, because anyone reading your profile can tell.
Vague wishes in tiny repos. "Rebuild the README as the project's landing page." "Make it a full email client." These are not tasks, they are moods.
The fix is to stop sorting by recency and start filtering by whether a human maintainer is actually waiting for the change. That means looking at the repo before the issue.
Four candidates I threw away

1. The typo fix that four people had already fixed
A documentation typo in skrub, a well-regarded library in the scikit-learn orbit. Labelled good first issue, documentation. Markdown bold not rendering because of stray spaces around the asterisks. Perfect, right?
I checked the issue's comments before touching anything:
[rajalakshmikr] I like to claim this issue.
[lisaleemcb] @rajalakshmikr great, thanks!Claimed. Then I searched the repo for pull requests referencing the issue number:
prs found: 4
- closed 2156 Removed spaces
- closed 2138 docs: Fix typo in ApplyToCols
- closed 2149 docs: fix bold formatting in ApplyToCols docstring
- closed 2127 fix: make text boldFour people had already submitted this one-line fix. All four closed. The issue was tagged for a conference sprint, which is a strong signal: sprint-labelled issues are held for event attendees and get swarmed. My PR would have been the fifth, and its only effect would have been to waste a maintainer's afternoon.
The check: before writing code, search the repo for open and closed PRs mentioning the issue number. Closed ones tell you the issue is contested even when nothing is linked in the sidebar.
2. The bug that was already fixed, on the branch I was not looking at
An open issue on a 2.7k-star repo: setting the AI provider to anything other than Gemini crashed at startup with ModuleNotFoundError: No module named 'google'. Clean, well-diagnosed, unassigned since May.
I cloned the repo and went looking for the unconditional import. It was not there. The maintainer had rebuilt the whole AI layer on LangChain, and the refactor had dissolved the bug as a side effect.
That would have been the end of it, except the contributing guide said something I nearly skimmed past:
Pull requests should target thecommunity-versionbranch, notmain. PRs to other branches (especiallymain) are declined by default.
So I checked out community-version — and the bug was alive, exactly as reported, because that branch predates the refactor. Same repo, same issue number, opposite answer depending on which branch you look at.
The check: read the contributing guide before the code, and reproduce on the branch you will actually target. main is not always the answer.
3. The bug I could not fix without inventing a fact
An open issue complained that the tool's farewell message shipped a literal placeholder:
Support my work on <PATREON_LINK>. Together, we can help more job seekers.That string appears in seven files. A trivial find-and-replace — except there is no Patreon URL anywhere in the repository, and the project's FUNDING.yml has the field sitting unfilled:
github: GodsScion
patreon: # Replace with a single Patreon usernameThere are three defensible fixes: drop the sentence, point it at GitHub Sponsors, or insert the real Patreon URL. Only the maintainer knows which. Guessing a funding destination on someone else's behalf is not a contribution, it is a liability, and a PR that invents one puts the maintainer in the awkward position of reviewing a link to a payment page they did not choose.
The check: if the correct fix depends on a fact only the maintainer holds, the contribution is a comment on the issue, not a pull request.
4. The one I lost by an hour
A genuine bug in Armbian's config tool. Creating a WireGuard peer named my-laptop silently did nothing. The validation regex allowed hyphens and underscores:
if [[ ! "$peer" =~ ^[a-zA-Z0-9_-]+$ ]]; thenBut the underlying container only accepts alphanumeric peer names. I confirmed that against the upstream README rather than trusting the issue:
-e PEERS=1— Can also be a list of names:myPC,myPhone,myTablet(alphanumeric only)
Note the direction here, because it is counterintuitive. The bug is that the tool is too permissive. It accepts a name, hands it to the container, and the container quietly drops it. The fix is to tighten validation and fail loudly at the prompt, not to make hyphens work.
I made the change, then ran one last check for competing PRs before pushing:
#1001 Fix module_wireguard.sh allowed characters to match docker imageOpened an hour earlier. Nearly identical to mine, down to the regex. The issue had been filed that same morning, and fresh bugs in popular repos are a race.
The check: re-check for competing PRs immediately before you push, not just when you pick the issue. On a same-day bug, the window is measured in hours.
The three that survived
A crash nobody could work around cleanly
Back to that ModuleNotFoundError on community-version. The bot imported all three AI providers at startup:
from modules.ai.openaiConnections import ...
from modules.ai.deepseekConnections import ...
from modules.ai.geminiConnections import ...The Gemini module imports google.generativeai at the top level. So every user paid for Gemini's dependency, and anyone who had not installed it crashed before the program started — even with the provider set to OpenAI.
The fix gates each import on the configured provider. The runtime already branched on that value at every call site, so nothing else had to change. But there is a trap in it worth naming, because the obvious version of this fix is broken.
DeepSeek borrows ai_close_openai_client from the OpenAI module for teardown. Gate that module on provider == "openai" alone and you fix the crash while breaking DeepSeek shutdown. The correct condition is:
if selected_ai_provider in ("openai", "deepseek"):
from modules.ai.openaiConnections import ...I only found that by grepping for every use of the imported names instead of trusting the import list. When you make an import conditional, the question is not "which module declares this name" but "which code paths reach it."
A setting that silently did nothing
While reading that file I found a second bug nobody had reported. Whether a model supports the temperature parameter was decided by this:
return model_name in ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-4o-mini"]An allow-list of five names. Any other model got no temperature at all — silently. No warning, the setting simply evaporated.
The reason this matters is in the project's own config file, which suggests running local models through LM Studio or Ollama:
llm_model = "" # Examples: "gpt-3.5-turbo", "gpt-4o", "llama-3.2-3b-instruct",
# "qwen3:latest", "gemini-pro", "deepseek-llm:latest"Three of those suggestions accept temperature and none of them are on the allow-list. The project documented a configuration it then quietly ignored.
The logic is backwards: only OpenAI's reasoning families reject the parameter, so deny-list those and default to sending it. Two details make the difference between a fix and a new bug:
- Match the family exactly or as a
family-variant. A barestartswith("o1")also swallowsomni-model. - Pick the safe direction for anything ambiguous. Sending
temperatureto a model that rejects it is a hard API error; omitting it for a model that accepts it just falls back to a default. When unsure, omit.
I tested twenty model names against old and new. Six previously-broken cases fixed, zero regressions. That table went straight into the PR description, and it is the reason the PR is easy to review: the maintainer does not have to take my word for the blast radius.
The one-line change to an Apache project
The best find of the day was in Apache DataFusion Ballista. Two crates each declared a feature named arrow-ipc-optimizations, and the executor's did not forward to the core's:
arrow-ipc-optimizations = []Directly below it, spark-compat showed the intended pattern. The executor's feature gated one decode site; core's gated the other three. Build the executor standalone with that feature and you got one of four sites optimised, with nothing to indicate it. Today it works only because the client also depends on core directly, so core's half arrives through its own defaults — it works by accident.
The fix is one line. What made it a good PR was proving it, which cargo will do for you:
cargo tree -p ballista-executor --no-default-features \
--features arrow-ipc-optimizations -e features -i ballista-coreBefore, core's feature hung only off core's own defaults. After, there is a direct edge from the executor's feature. I pasted both trees into the PR. No prose argument beats the resolver's own output.
The filter, condensed
Before writing a line of code:
- Judge the repo before the issue. Real stars, commits this month, a contributing guide, merged PRs from outside contributors. A maintainer who merges strangers' work is the actual prerequisite.
- Search closed PRs for the issue number. Four closed PRs means contested, whatever the sidebar says.
- Read who is talking in the thread. "I am taking a look at this" from a maintainer means it is theirs. I skipped a Canonical issue for exactly this.
- Read the contributing guide before the code. It tells you the target branch, and sometimes that changes whether the bug exists at all.
- Reproduce it yourself. Two of my seven candidates were already fixed. Reproduction is the only thing that separates a real bug from a stale one.
Before you push:
- Verify the fix mechanically. A test matrix, a cargo tree, a before/after transcript. Something the maintainer can check without rebuilding your reasoning.
- Check for competing PRs again. An hour is enough to lose a race.
- Say what you did not do. My Ballista PR notes that the fix still does not give a full off switch, because Cargo features are additive. Naming a limitation is not weakness; it tells the reviewer you understood the shape of the problem.
What actually made these mergeable
Not the code. Every one of these fixes is between one and eighteen lines. The work was in the filtering and the verification — the two things a contribution-count leaderboard cannot see and a maintainer can see instantly.
A one-line PR with a cargo tree proving the resolver behaviour is a gift. The same one-line PR with "fixes the feature flag" as the body is a chore, because now the maintainer has to do the verification you skipped.
Three PRs from a day of looking. Four rejections. That ratio is the job.