Contribute upstream
Claude Code plugin: turn bugs in your dependencies into upstream contribution PRs — without leaving your project.
npx -y skills add shiminshen/oss-contribute --skill contribute-upstreamAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Turn a bug you hit in a third-party dependency (while working in your own project) into an upstream contribution PR — or a well-filed issue if a clean upstream repro isn't feasible. Handles the boring parts — reading CONTRIBUTING, duplicate search, package-manager setup, commit/PR conventions, changesets — and a local patch handoff so the consumer repo isn't blocked.
SKILL.md
23.6 KB, as published. Nobody here has run it
contribute-upstream
Invoke from inside the consumer repo (the one that depends on the buggy package), not the upstream itself. You do the boilerplate the user shouldn't have to memorise per-repo; the user provides the bug context.
Usage
/oss-contribute:contribute-upstream <package-name> # e.g. better-auth
/oss-contribute:contribute-upstream <package-name> <symptom-summary>
/oss-contribute:contribute-upstream # infer package from current stack trace / open file
Profile location
Read the profile in this order (used only for default GitHub account):
$CLAUDE_PLUGIN_DATA/profile.md~/.claude/plugins/data/oss-contribute/profile.md~/.claude/skills/oss-contribute/profile.md
Profile is optional for this skill — the GitHub-account question is asked interactively either way (see Phase 1 step 7).
Phase 1 — Pre-flight gate (BLOCKING)
Do all of the following before touching any code. If any step surfaces a blocker, stop and tell the user; do not proceed unilaterally.
-
Freshness re-check (FIRST, BLOCKING). Before any other Phase 1 work — before resolving the upstream repo, before dispatching the rules-of-the-road subagent, before reading anything — re-verify the issue is still ripe RIGHT NOW. Even if you just hand-picked it from
find-issuesPhase 4 minutes ago. State changes fast on Hot repos.Single batched call:
gh issue view <n> --repo <owner>/<repo> --json assignees,closedByPullRequestsReferences,stategh search prs --repo <owner>/<repo> --state open --limit 5 "#<n>"- One or two distinctive backticked identifiers from the issue body
Drop and surface to the user immediately if any of these is true:
- Issue now has an assignee (someone is on it — competing is rude)
closedByPullRequestsReferencesis non-empty (a PR is already linked)- A token-search PR hits the same fix surface
- Issue state is no longer
OPEN
Motivating case:
mastra-ai/mastra#16422(late assignee). Seereferences/case-studies.md#freshness--late-assignee-phase-1-step-0.
0a. Adjacent-PR check (BLOCKING). Search for any open PR in the same code area and inspect each. Two distinct drop signals — either is BLOCKING.
Dead-area signal (adjacent PR is stalled). Run gh pr view <pr> --json reviews,comments,reviewDecision,updatedAt — zero reviews + zero comments + REVIEW_REQUIRED for 30+ days means the area is reviewer-cold. A fresh PR faces the same fate. Surface and stop.
Moving-target signal (adjacent PR is active and reshapes a shared interface). After resolving the file(s) your fix will touch, list the interface symbols / imports those files depend on. For each, search:
gh search prs --repo <owner>/<repo> --state open <symbol-or-file-path>
For each hit, run gh pr view <pr> --json files,reviews,comments,updatedAt. If the PR is active (reviews/comments non-empty, updatedAt within ~14 days) AND its files list includes an interface file your fix imports — even when there is no textual overlap with your fix's files — surface to the user. The textual dup-PR search won't catch this because the PRs are about different surfaces; the risk is coordination (which signature do you call?), not duplication. Right call is usually one of: wait for the adjacent PR to land, comment on your issue asking the maintainer how to sequence, or abort.
Motivating cases: vercel/ai#13962 adjacent to stalled #12924 (dead area); topoteretes/cognee#2815 adjacent to active #2712 (shared vector_db_interface.py). See references/case-studies.md#adjacent-stalled-pr--dead-area-signal-phase-1-step-0a and references/case-studies.md#adjacent-active-pr-on-shared-interface--coordination-risk-phase-1-step-0a-inverse-shape.
0b. Already-fixed-on-main check (BLOCKING). Read the version the reporter is on from the issue body. Compare to current main's version + recent commits to the file path the reporter mentions. If a fix-shaped commit landed between the reporter's version and main, the bug may already be fixed — the reporter just needs to upgrade. Verify by running the existing tests for that file. If they pass on main, drop and offer to comment pointing the reporter to the version that fixes it.
Cheap check:
gh api 'repos/<owner>/<repo>/commits?path=<file>&per_page=15' \
--jq '.[] | {sha: .sha[:8], date: .commit.author.date, msg: .commit.message | split("\n")[0]}'
Motivating cases: assistant-ui#4009, mastra#16383, and the inverse drizzle-orm#5755. See references/case-studies.md#already-fixed-on-main--wrong-slice-of-the-version-axis-phase-1-step-0b.
0c. Label-policy check (BLOCKING). Some projects enforce invitation-only / internal-team-only conventions scoped to specific issue labels, not the whole repo. The prose-based check in step 2 misses these because CONTRIBUTING is silent on them — the convention is enforced by a maintainer closing external PRs with a brief "the team handles this label" comment.
Read the issue's labels (already in the step 0 gh issue view result). For each label, sample recent closed-not-merged PRs that linked an issue carrying that label:
gh search prs --repo <owner>/<repo> --state closed --limit 10 \
"is:unmerged label:<label>"
# Then for each hit:
gh pr view <n> --repo <owner>/<repo> --json closedAt,comments \
--jq '{closedAt, closer: (.comments | last | .author.login), msg: (.comments | last | .body | .[0:300])}'
Drop and switch to Phase 6 (issue-only / proposal comment) if two or more recent closures on the same label share a closer-comment shape like:
- "the team handles bugs marked with the
<label>label" - "this is an internal-team area / closed in favour of internal work"
- "thanks but we cannot accept external PRs for
<label>issues"
One closure could be idiosyncratic; two with matching wording is policy.
Motivating case: ChromeDevTools/chrome-devtools-mcp evals label. See references/case-studies.md#label-scoped-invitation-only-convention-phase-1-step-0c.
-
Resolve the upstream repo. From the consumer's
package.json+ lockfile, get the installed version and therepositoryURL. Disambiguate (workspace, fork, mirror) with the user if needed. -
Read the rules of the road. Strongly prefer dispatching this to a
general-purposesubagent — many file fetches, mostly null results, content dumps that pollute the main context. The subagent returns a compact verdict (≤15 lines) covering: contribution policy (open / discuss-first / invitation-only — HARD STOP if invitation-only), signed-commits requirement, CLA, branch target, changeset usage, packageManager + node version. Pass the upstreamowner/repoand the issue number for context.Fetch and read each of these paths; treat the first hit as authoritative for that doc type. Filenames vary in case and folder — try every variant before giving up:
- Contributing policy:
CONTRIBUTING.md,.github/CONTRIBUTING.md,docs/CONTRIBUTING.md,docs/contributing.md,docs/contribute.md. Many projects put the real policy atdocs/contributing.mdwhile the root file is missing or stub — keep searching after the first 404. CODE_OF_CONDUCT.mdSECURITY.md(also try.github/SECURITY.md) — if the bug is a security issue, STOP: most projects forbid public disclosure. Redirect to the project's security contact.- PR template:
.github/PULL_REQUEST_TEMPLATE.mdAND.github/pull_request_template.md(case matters on GitHub's API). AlsoPULL_REQUEST_TEMPLATE.mdat root. The template often restates policy ("invitation only", "must link issue", "do not open without issue first") — read it as policy, not just formatting. CLAUDE.md/AGENTS.mdat repo root (project-specific AI guidance).changeset/config.json(does the project use changesets?)package.json#packageManagerand.nvmrc
Invitation-only / closed-contribution check (HARD STOP). Some projects (e.g.
openai/codex) accept external PRs by invitation only and close uninvited PRs unread. Scan the contributing doc and PR template for phrases like "invitation only", "do not accept unsolicited", "closed without review", "external contributions are closed". If found, STOP — do not proceed to Phase 2 clone. Instead, switch to Phase 6 (issue-only path) and offer to comment on the issue with analysis + suggested fix, which is what these projects explicitly invite. Surface this clearly to the user before any further work. - Contributing policy:
-
Check signs of life. Recent merged PRs (last 30d), issue response cadence, last release. If the project looks dormant or hostile to outside PRs, surface that and ask whether to continue.
-
Check whether discussion is required. Some projects explicitly require an issue before a feature PR and will close cold feature PRs. Bug fixes are usually fine. For features, file or find the issue first.
-
Duplicate search. Title-keyword searches miss PRs whose title describes the implementation rather than the symptom. Search by tokens extracted from the issue body.
Dispatch all four token queries in parallel — ONE message with one Bash tool call per token type. Do not run them sequentially. Inspect results together; if any returns a PR hit, short-circuit and drop.
The token types:
- The issue number itself.
gh search prs --repo <owner>/<repo> --state open --limit 5 "<n>"— many PR descriptions reference the issue. - URL-encoded or other distinctive literals in the issue body —
%5F, error codes, magic strings. - Backticked code identifiers from the issue body — function names, file paths, type names.
- Error message fragments quoted in the body, if any.
Title-keyword paraphrases are a last resort, not a first resort. Motivating case: the
%5Fliteral-token search. Seereferences/case-studies.md#token-based-duplicate-pr-search--implementation-titled-prs-phase-1-step-5.Also run
gh issue view <n> --json assignees,closedByPullRequestsReferences,commentson every candidate issue. Outcomes:- Open issue, no assignee, no linked PR → add the user's extra context as a comment; ask in the same comment whether you can take it (some projects require an explicit "assign me" /
/assignbefore you start). - Open issue with an assignee but no PR yet → comment asking if they're still actively working on it before duplicating effort. Do not start work until you hear back or the assignee is removed.
- Open issue with a linked open PR → tell the user; offer to review/test that PR, not compete with it.
- Open PR found via
gh search prsfor the same fix → same: review/test, don't compete. - Closed PR → read why; that reason (scope, design objection, maintainer pushback) often blocks the same fix even if the bug is real.
- The issue number itself.
-
CLA check. Look for
.github/cla.yml,cla-assistant, or a CLA note in CONTRIBUTING. Surface CLA requirements before any code work. -
Pick the GitHub account AND the git commit identity (must match). Read the shared profile's
## Default GitHub accountAND## Git commit identity(thename:andemail:lines). Use both without re-prompting — even whengh auth statusshows multiple accounts logged in. The profile entries are the user's standing instructions; re-asking on every contribution treats stable preferences as ephemeral and creates friction. State the chosen account + name + email once in the Phase 1 step 8 summary so they're visible, but do not put them behind questions. The user will override per-invocation if they want a different identity ("use the company account this time"); otherwise honour the profile.Critical: commit identity must match GitHub account. The default
git config --global user.emailis typically the user's day-job email (e.g.[email protected]). If the OSS clone inherits this, the PR appears under the personal GitHub account BUT every commit is stamped with the company email — exactly the wrong signal, and visible forever ingit log. After the Phase 2 clone, set local repo identity explicitly:git -C <clone-dir> config user.name "<profile name>" git -C <clone-dir> config user.email "<profile email>"Verify before any commit:
git -C <clone-dir> config user.email # must match the profile's email, not the global oneIf the profile has no
## Git commit identitysection, fall back to asking the user explicitly — never silently inherit the global identity for an OSS clone. Only re-prompt for account/identity if the profile fields are absent, or if the user has corrected the choice within the same session. -
Confirm with user. Summarise findings (repo, version, dup status, conventions, CLA, branch target, changeset y/n, chosen GitHub account, chosen git identity) in ≤10 lines. Get explicit go-ahead before Phase 2.
Phase 2 — Setup
-
Re-verify duplicate (BLOCKING). Re-run the duplicate-PR search from Phase 1 step 5, right now, immediately before the clone. Hunt → contribute can take minutes to days; PRs land in that window. Cloning a large monorepo costs 15–25 minutes — confirm it's still worth doing.
If a new PR has appeared, stop and offer to review/test it instead of competing.
-
Clone the user's fork (create one with
gh repo forkfirst if absent) to a sibling directory of the consumer repo — never inside it. -
Set local git identity (BLOCKING before any commit). Immediately after clone, set the repo-local
user.nameanduser.emailfrom the profile's## Git commit identityso commits are not stamped with the user's global (typically work) identity:git -C <clone-dir> config user.name "<profile name>" git -C <clone-dir> config user.email "<profile email>" git -C <clone-dir> config user.email # verifyThis is the safety net for the Phase 1 step 7 decision. If the profile lacks the identity section, ask the user explicitly — never let the global identity be inherited by an OSS clone. Documented failure mode: PR appears from personal GitHub account but
git logshows every commit under the work email, visible forever. -
Install deps with the project's pinned package manager (
corepack prepare/corepack enableif needed). Honour.nvmrc. -
Run the project's baseline
testandtypecheckto confirm a clean starting state. If they fail on the default branch, surface that — do not try to "fix" baseline failures.
Phase 3 — Reproduce in the upstream's own test framework
This is the hardest step. In order:
-
Convention scan (BLOCKING before any code, including the failing test). Before writing a single line — even the failing test — load
references/convention-checklist.mdand run it against the file you'll modify and its closest test file. Capture the test-file structure, helper / mock-factory patterns, naming conventions, cross-cutting setup placement, comment density, lifecycle / async idioms, and import style. Also read recent merged PRs touching adjacent files (gh pr list --search "<file/path>" --state merged --limit 3) — they show what conventions the maintainers enforce in review. Prevention counterpart to Phase 4's audit; skipping it means the audit catches mistakes after they're written and produces review churn. -
Adapt the upstream's existing tests. Find the test file closest to the affected code and add a failing case that mirrors the consumer-side symptom — using the conventions captured in step 0.
-
Use the project's test harness — do not invent a custom setup.
-
Bail out if the bug depends on consumer-stack specifics the upstream can't reproduce (specific framework version, DB driver, env-specific behaviour). Switch to Phase 6 (issue-only).
-
Tractability gate (BLOCKING). Before writing the fix, ask: is the fix scope what the issue framing suggested? Two failure modes to catch:
- Feature gone, not bug. The issue says "X is missing/broken in version Y" but X is wholly absent from the new code paths. The "fix" would be a re-implementation, not a bug fix — switch to Phase 6 with analysis: "feature X is absent — intentional or oversight?" Motivating case:
drizzle-orm#5755. Seereferences/case-studies.md#tractability-gate--feature-gone-not-bug-phase-3-step-4. - Half-fix risk. The minimal fix (e.g. one type addition) makes TS stop erroring but leaves runtime semantics broken. Don't ship half-fixes — they hide the bug from users. Either fix both or switch to Phase 6.
- Feature gone, not bug. The issue says "X is missing/broken in version Y" but X is wholly absent from the new code paths. The "fix" would be a re-implementation, not a bug fix — switch to Phase 6 with analysis: "feature X is absent — intentional or oversight?" Motivating case:
Confirm the new test fails for the right reason before writing a fix.
Phase 4 — Fix
- Smallest possible diff. Bug fix only — no surrounding refactor, no scope creep, no comments restating the obvious.
- Run the targeted test → the full affected file → the project's
typecheck. - If the fix touches a public API, update
docs/per the project's docs convention. - Add a regression marker if the project uses one (e.g.
@see https://github.com/.../issues/<n>block above the test). - Convention audit (BLOCKING before commit). Re-load
references/convention-checklist.mdand run it against your diff. Verification counterpart to Phase 3 step 0's prevention scan — catch what slipped through. Motivating case: CopilotKit#4798. Seereferences/case-studies.md#convention-divergence-at-commit-phase-3-step-0--phase-4-audit.
Phase 5 — PR prep
-
Branch name. Match the project's pattern (
fix/<slug>,feat/<slug>, etc.). -
Commit format. Strict match to CONTRIBUTING (Conventional Commits, lowercase subject, scope,
!for breaking, etc.). -
Changeset. If the project uses changesets, write one. Style rules:
- Written for end users reading the changelog, not the reviewer.
- Describe the symptom users see, not the internal cause.
- No commit-style prefix.
- Pick bump type by user impact (patch / minor / major).
-
Branch target. Read CONTRIBUTING for
mainvsnextvsdevelopvsmaster. -
PR body. Follow the project's PR template. Always include: Summary · Closes #<issue> · Test plan · Breaking changes (if any).
-
Author identity. Use the GitHub account and git identity the user confirmed in Phase 1 step 7. Do not silently fall back to the active
ghaccount. -
Pre-PR confirmation gate (BLOCKING). Before calling
gh pr create, show the user:- Target repo and base branch (
upstream/owner:base) - Head ref (
<user>:<branch>) - PR title
- Full PR body
- List of commits that will be included
- List of files changed (summary, not full diff)
Ask for explicit confirmation. Do not open the PR until the user says yes. If they ask for edits, revise and re-show — never push or open the PR on your own initiative.
- Target repo and base branch (
-
Push from fork, open PR cross-repo only after step 7 confirmation:
gh pr create --repo upstream/repo --head user:branch.
Phase 6 — Issue-only or Propose escape hatch
When Phase 3 (repro bridge) or the Phase 3 tractability gate blocks the PR path, do not abandon the contribution — switch to the most useful artifact you can still produce. Pick one of two outputs:
6a — File a new issue
Use when no upstream issue exists yet (you discovered the bug from your consumer-side symptom).
- Use the project's bug-report template.
- Include: installed version, exact symptom, minimal consumer-side repro, expected vs actual, environment.
- Link related closed issues/PRs found in Phase 1.
- Do not open a PR in this path.
6b — Post a Proposal comment
Use when an upstream issue already exists but the fix is too large, too design-sensitive, or otherwise not session-sized. Maps to what Token-Steward calls the "Propose" action: a structured comment that gauges maintainer interest before anyone writes code.
Structure the comment as:
- Problem restated in one line — confirms you understood the report.
- Root-cause analysis — what's broken in the codebase, with file:line references where useful.
- Proposed approach — the design sketch, not the diff. 3–6 bullets max.
- Open questions for the maintainer — explicit asks: "Is this the right surface to fix?", "Should this be a breaking change?", "Is there a related refactor in flight?"
- What I'd need to ship it — assignment, API blessing, test-strategy guidance.
Hard rules for proposal comments:
- The comment is read-only output to the user first; do not post until the user confirms.
- Use the issue's existing thread; do not open a duplicate issue.
- Do not include the diff. Description sketches, not implementations — let the maintainer steer before code is written.
Phase 7 — Local patch handoff (consumer side)
The upstream PR may sit in review for days or weeks. Don't leave the consumer blocked:
pnpm patch <pkg>→ reapply the same fix as a local patch; commitpatches/*.patchto the consumer repo.pnpm.overrides→ point the consumer at the user's fork branch (github:<user>/<repo>#<branch>).- npm/yarn equivalents if the consumer uses those.
Leave a TODO in the consumer repo noting the upstream PR number so the patch can be removed once the fix is released.
Phase 8 — Respond to PR review
After the PR is open, this phase handles the round-trip until merge or close.
Trigger when gh pr view <n> --json reviews,comments,reviewRequests shows new activity since last check, or the user invokes /oss-contribute:contribute-upstream with the PR URL/number after it's open.
Procedure: Load references/phase-8-review.md. It contains the 7-step procedure (fetch → classify into apply/push-back/clarify/out-of-scope → summarise to user → apply approved → push to fork → reply on threads → re-request review) and 4 hard rules (no silent re-pushing, no mid-review rebase, no force-push unless asked, escalate when feedback conflicts with Phase 1 hard rules).
Hard rules
- Never push to the upstream's main repo. Always work from the user's fork.
- Never commit without explicit user instruction.
- Never amend or force-push a published commit unless the user explicitly asks.
- Never open a PR for a new feature or breaking change without an existing or freshly-filed issue, if the project's CONTRIBUTING requires one.
- No AI-attribution trailers on commits. Never add
Co-Authored-By: Claude/Generated-By/ any "AI assisted this commit" line. OSS maintainers treat these as noise at best, hostile at worst — the contribution must read as sole-authored by the GitHub user. Determine identity viagh api user --jq '.login'andgit config user.name/git config user.email. Zero exceptions, including in commit-message bodies and PR bodies. - Surface, don't suppress. If any phase reveals the contribution probably won't be accepted (dormant repo, scope mismatch, CLA blocker, duplicate open PR, scope-creep risk), say so before doing the work.
Output shape
At the end, produce:
- The upstream PR URL (Phase 5), issue URL (Phase 6a), or proposal-comment permalink (Phase 6b).
- The consumer-side patch/override instructions actually applied (if Phase 7).
- The post-review state if Phase 8 ran: open feedback threads remaining, last push SHA, current
reviewDecision. - A one-line tracker note: package, version, PR/issue #, what to remove once released.