Writing a CONTRIBUTING.md that open source maintainers actually get used
Most CONTRIBUTING.md files are read once and ignored. Here's how to write one that reduces bad PRs and stays accurate as your workflow changes.
Most CONTRIBUTING.md files are written once, during a slow week, by whoever felt guilty about the empty file — and then never opened again. You can tell which ones these are: they open with “Thank you for your interest in contributing!”, list a git clone command, and then trail off into vague encouragement about “following best practices.” Nobody reads past line ten. The maintainer keeps rejecting PRs for the same three reasons, over and over, because the document that was supposed to prevent that never actually told anyone what those three reasons were.
A CONTRIBUTING.md is not a welcome mat. It is a filter. Its job is to turn a contributor’s first PR into something a maintainer can merge in one pass instead of three rounds of “can you also fix this” comments. Most files fail at that job, and the failure is structural, not a matter of good intentions.
Why most CONTRIBUTING.md files get ignored
The typical CONTRIBUTING.md fails for one of three reasons, usually all three at once.
- It’s aspirational, not operational. It describes the project’s values (“we care about code quality”) instead of the actual steps (“run
make lintbefore opening a PR”). Values don’t stop a bad PR. Commands do. - It’s stale the moment the toolchain changes. The file says
npm test, the project moved topnpmeight months ago, and now the first thing a new contributor does is fail on a command that doesn’t exist. That single broken command is enough to make them assume the rest of the document is unreliable too, so they stop reading it. - It’s written for an audience that doesn’t exist. It assumes either total beginners (three paragraphs on what a pull request is) or total experts (a single line: “see our style guide”), and misses the actual population of people opening PRs against your repo — a mix of first-timers and repeat contributors with very different needs.
Here’s the part maintainers underestimate: contributors don’t skip the CONTRIBUTING.md because they’re lazy. They skip it because the first time they tried to follow it, it was wrong, and the cost of a wrong instruction is higher than the cost of skipping the document entirely and guessing. Once someone’s mental model of your CONTRIBUTING.md is “unreliable,” you’ve lost the audience it was built for. Every PR after that goes back to being guesswork, and guesswork produces exactly the bad PRs the file was supposed to prevent.
The fix isn’t a longer document. It’s a shorter, more accurate one, kept accurate on purpose.
The sections that actually reduce bad PRs
A CONTRIBUTING.md earns its keep with four sections. Not ten, not two — four. Each one maps directly to a category of rejected PR.
Setup. This is the section that determines whether someone finishes their first PR at all. It needs to be copy-pasteable, not descriptive. “Install the dependencies” is not setup instructions. This is:
git clone https://github.com/yourorg/yourrepo.git
cd yourrepo
pnpm install
cp .env.example .env
pnpm dev
If a contributor has to guess a single flag or figure out which .env values are required, some fraction of them will quietly close the tab instead of asking. Test this section the way you’d test code: have someone with zero context run it top to bottom on a clean machine.
Code style. Don’t describe your style — enforce it and point to the enforcement. “We use 2-space indentation and prefer const over let” is a request a reviewer now has to manually check on every PR. “Run pnpm lint --fix before committing; CI will block on lint errors” removes the judgment call entirely. If you have an ESLint config, a .prettierrc, or a Rubocop file, link to it instead of restating it in prose — prose and config drift apart within a quarter.
Commit conventions. If you want a clean changelog or you’re generating release notes from commit history, say so explicitly with a format and an example, not a link to the Conventional Commits spec and a shrug:
feat(auth): add refresh token rotation
fix(api): handle null response from /v2/users
docs(readme): correct install command for windows
Review process. This is the section that’s missing from almost every CONTRIBUTING.md, and it’s the one that prevents the most frustration. Contributors don’t just need to know how to open a PR — they need to know what happens after. How many approvals are required? Does a maintainer need to add a label before CI runs? Is there a typical response time? A contributor who doesn’t hear back for eight days assumes their PR is being ignored; a contributor who’s told upfront “maintainers review PRs weekly, on Fridays” reads silence correctly and doesn’t ping the thread three times.
Everything else — a code of conduct, a list of good first issues, links to a Discord — is useful, but it doesn’t move the needle on PR quality the way these four do. Put them lower in the document or in separate files.
Writing for first-time contributors vs. repeat contributors
A first-time contributor and someone who’s landed twelve PRs in your repo need almost opposite things from the same document, and trying to serve both with the same paragraph is why so many CONTRIBUTING.md files feel simultaneously too basic and too sparse.
A first-timer needs orientation: which files matter, where tests live, what “small PR” means in your project’s context (is 200 lines small? 20?), and permission to ask questions in the open rather than guess silently. A repeat contributor already has all of that internalized. What they need is a fast reference — the exact lint command, the exact branch naming convention, the exact label that triggers a preview deploy — without wading through onboarding prose again.
The fix is structural, not a matter of writing two different documents. Put a short “first PR” path at the top — five steps, no more — and move the reference material (commands, conventions, flags) into scannable lists and tables below it, so a repeat contributor can skip straight past the narrative section without reading it.
❌ Before, a single wall of prose that serves neither audience:
To contribute, please clone the repository and install dependencies as described in the README. We ask that all contributors follow our coding standards and write clear commit messages. Once your changes are ready, open a pull request and a maintainer will review it when they have time. We appreciate all contributions, big and small, and look forward to working with you!
✅ After, split by audience, operational instead of aspirational:
New here? Do this first:
- Fork the repo, run
pnpm install && pnpm dev.- Pick an issue labeled
good-first-issue.- Open a draft PR early — even with failing tests — so we can course-correct before you’ve written 400 lines.
- Run
pnpm lint --fix && pnpm testbefore marking it ready for review.- One maintainer approval merges it. We review PRs every Tuesday and Friday.
Already contributed before? Skip to Commit conventions and CI checks.
The second version is shorter and does more work, because every sentence in it is an instruction, not a sentiment.
Linking CONTRIBUTING.md to issue templates and CI checks
A CONTRIBUTING.md that lives in isolation gets read once and forgotten. One that’s wired into the actual mechanics of opening a PR gets re-read at the exact moment it’s useful, because the workflow points back to it.
Three concrete connections are worth making:
- Issue templates should reference CONTRIBUTING.md directly, not just link to it in the repo’s About section. A bug report template can include a checkbox: “I’ve read CONTRIBUTING.md and confirmed this isn’t covered in the FAQ.” A feature request template can point contributors to the section on proposing larger changes before writing code, saving both sides a rejected 600-line PR.
- CI failure messages should point back to the relevant section, not just fail silently. If your lint check fails, a custom message like
Lint failed — see CONTRIBUTING.md#code-style for the fix commandturns a red X into a two-minute fix instead of a support request in the PR comments. - The PR template should mirror the review-process section exactly. If CONTRIBUTING.md says PRs need a passing test suite and a linked issue, the PR template’s checklist should ask for exactly those two things — not a generic “Have you tested this?” that doesn’t match what the maintainer is actually going to check.
The pattern here is that CONTRIBUTING.md shouldn’t be a standalone document a contributor has to remember exists. It should be the thing every other piece of contributor-facing tooling points back to at the moment it’s relevant — which is also the only way it stays load-bearing instead of decorative.
Keeping contributor docs current as your workflow changes
Every CONTRIBUTING.md starts accurate. The failure mode is always drift: the test runner changes, the branch naming convention gets renamed from feature/ to feat/, a new required CI check gets added, and the document quietly stops matching reality while nobody’s job is to notice.
This is the same rot that hits API docs and READMEs, and it has the same root cause — the update to CONTRIBUTING.md is a separate, forgettable step from the PR that changed the workflow. Someone updates package.json’s test script and merges it same-day; the CONTRIBUTING.md line that references the old command survives for months because updating it wasn’t part of the same PR and nobody’s specifically responsible for catching the mismatch.
A few things make this more durable than hoping someone remembers:
- Treat CONTRIBUTING.md changes as part of the PR that changes the workflow, not a follow-up ticket. If you’re renaming a script or adding a required check, the doc update belongs in the same diff.
- Put commands in one place and reference them everywhere else, so a change to the lint command means editing one line instead of grepping for every mention of
pnpm lintacross your docs. - Review contributor docs at the same cadence you review your CI config — if you’re already looking at
.github/workflowsfor an unrelated reason, glance at whether CONTRIBUTING.md still matches it.
This is exactly the gap GitDoc was built to close for the rest of your documentation, and CONTRIBUTING.md is a natural extension of that same problem: it’s just another file that describes your codebase’s behavior and goes stale the moment the codebase changes without it. We watch the repo for the changes that would make a doc wrong — a renamed script, a new required check, a changed branch convention — and draft the update instead of leaving it for someone to notice six months later. You can see how the sync loop works at gitdoc.ai.
A CONTRIBUTING.md doesn’t need to be exhaustive to work. It needs to be accurate, operational, and current — four sections that actually reduce rejected PRs, written for the two different audiences that will actually read them, wired into the tooling that surrounds a PR, and updated in the same breath as the workflow it describes. That’s a smaller, more maintainable document than most teams end up writing, and it’s the one contributors actually use.
GitDoc keeps your docs in sync with your codebase on every push. Start free →